mirror of
https://github.com/praktimarc/kst4contest.git
synced 2026-09-13 12:45:32 +02:00
Compare commits
130
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
00496b56e9 | ||
|
|
5b7897c872
|
||
|
|
dfc51aa0ec | ||
|
|
7e0e338159 | ||
|
|
303bb21bd1 | ||
|
|
58341902f7 | ||
|
|
f3a1a73231 | ||
|
|
590ac608cd | ||
|
|
039e64eea0 | ||
|
|
0b6120defc | ||
|
|
756f65e825 | ||
|
|
1de8b77c52 | ||
|
|
6b15b98c12 | ||
|
|
b0c04d2238 | ||
|
|
01a2d95a8a | ||
|
|
db844ef4f7 | ||
|
|
8ee9d59d7a | ||
|
|
e7631ef688 | ||
|
|
08dff3e60d | ||
|
|
b6214fb893 | ||
|
|
823af70caa | ||
|
|
334e5b03a8 | ||
|
|
273becf793 | ||
|
|
32e28f8797 | ||
|
|
b3ff39a365 | ||
|
|
84ac5eb479 | ||
|
|
31bc42982d | ||
|
|
7acdfca4f5
|
||
|
|
c3fd931fee | ||
|
|
373a30933b
|
||
|
|
43538a277f | ||
|
|
5ec16fa458 | ||
|
|
9ef0aeef1c | ||
|
|
46d722967a
|
||
|
|
fc246a5607
|
||
|
|
8cdeb2ad63 | ||
|
|
df0e535fb3
|
||
|
|
e85d4c2fe2
|
||
|
|
360e55633b
|
||
|
|
367967518e
|
||
|
|
d8859247c6 | ||
|
|
bf1b72a6b9 | ||
|
|
9545e708fd | ||
|
|
37121949e4 | ||
|
|
d70d8d7a24 | ||
|
|
6a715567dd | ||
|
|
a5f96324e6 | ||
|
|
a3e9581c4b | ||
|
|
3285960ee3 | ||
|
|
d532f935d6 | ||
|
|
3988a6d456 | ||
|
|
598be64306 | ||
|
|
7fcea718ec | ||
|
|
fd7d0a6807 | ||
|
|
e424b44cb2 | ||
|
|
dc703fca9e | ||
|
|
77c9d34d64 | ||
|
|
efd6ed6299 | ||
|
|
de8e8ba471 | ||
|
|
eaf611b203 | ||
|
|
e339b6fccf | ||
|
|
bd9b42d4a5 | ||
|
|
275ce48769 | ||
|
|
d201997587
|
||
|
|
250290a1eb | ||
|
|
052cfea7ad
|
||
|
|
d930b57196 | ||
|
|
eb29d56f4e | ||
|
|
eb2a7f88a4 | ||
|
|
8c3e4c15af | ||
|
|
c8b1f97872 | ||
|
|
b47fd21dd0 | ||
|
|
f009ed4b3d | ||
|
|
47126f4ae1
|
||
|
|
7d3177ba8a | ||
|
|
ff71623b90
|
||
|
|
ead985a741
|
||
|
|
ef1eeeed36
|
||
|
|
4a34e28a84
|
||
|
|
38ef50cee8
|
||
|
|
d45a637d00 | ||
|
|
1ec2b1a450 | ||
|
|
19e8cff6b6 | ||
|
|
62999cfed8
|
||
|
|
49524146e8
|
||
|
|
dd2ac3bc69
|
||
|
|
098dd31b58 | ||
|
|
8ecbf7dfa0 | ||
|
|
1b30de34cc | ||
|
|
65b2e561f4 | ||
|
|
6907af5c81 | ||
|
|
59b8e2fbbb | ||
|
|
5838d3a999 | ||
|
|
6d1e08711a | ||
|
|
67b91a766a | ||
|
|
f83cdbd804 | ||
|
|
9359568a33 | ||
|
|
8f46909ebf | ||
|
|
59638f6ab3 | ||
|
|
59aade8004 | ||
|
|
42255e2b41 | ||
|
|
217c7e8451 | ||
|
|
a99d611ea8 | ||
|
|
7b0c86a5a0 | ||
|
|
1bd338c7ee | ||
|
|
dfe5e28877 | ||
|
|
248a7c0462 | ||
|
|
db83d6a9df | ||
|
|
006ee7b060 | ||
|
|
8ae8c9996f | ||
|
|
9d6b2536dd | ||
|
|
4d903f5344 | ||
|
|
34c1c68943 | ||
|
|
f48b72c54e | ||
|
|
413f69eb6a | ||
|
|
69fe6dc7b1 | ||
|
|
80214bf42d | ||
|
|
62b6bbdcbc
|
||
|
|
e2a327c730
|
||
|
|
521ea18782
|
||
|
|
6afcee8df5
|
||
|
|
1ce468145c
|
||
|
|
2f4aac8cf7
|
||
|
|
94a5f6a9f7
|
||
|
|
3070d03893 | ||
|
|
a53e6a420f
|
||
|
|
57deda4b94
|
||
|
|
3632a0fcc9
|
||
|
|
ff6b8b75a6
|
||
|
|
eee1594d30
|
@@ -0,0 +1,155 @@
|
||||
name: AUR Nightly (kst4contest-git)
|
||||
|
||||
on:
|
||||
workflow_run:
|
||||
workflows: ["Nightly Runtime Artifacts"]
|
||||
types: [completed]
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
env:
|
||||
AUR_SSH_DIR: /tmp/aur-ssh
|
||||
|
||||
jobs:
|
||||
update-aur-git:
|
||||
if: >-
|
||||
github.event_name == 'workflow_dispatch' ||
|
||||
github.event.workflow_run.conclusion == 'success'
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
container:
|
||||
image: archlinux:latest
|
||||
|
||||
steps:
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
pacman -Sy --noconfirm git openssh base-devel
|
||||
useradd -m builder
|
||||
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v7
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Mark workspace as safe Git directory
|
||||
run: |
|
||||
git config --global --add safe.directory "$GITHUB_WORKSPACE"
|
||||
|
||||
- name: Compute pkgver
|
||||
id: ver
|
||||
run: |
|
||||
BASE=$(grep -m1 '<version>' pom.xml \
|
||||
| sed 's/.*<version>\(.*\)<\/version>.*/\1/' \
|
||||
| sed 's/[-.]nightly//')
|
||||
|
||||
GIT_PKGVER="${BASE}.r$(git rev-list --count HEAD).g$(git rev-parse --short HEAD)"
|
||||
|
||||
echo "Computed pkgver: ${GIT_PKGVER}"
|
||||
echo "pkgver=${GIT_PKGVER}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Update PKGBUILD
|
||||
env:
|
||||
PKGVER: ${{ steps.ver.outputs.pkgver }}
|
||||
run: |
|
||||
sed -i "s/^pkgver=.*/pkgver=${PKGVER}/" \
|
||||
packaging/aur/kst4contest-git/PKGBUILD
|
||||
|
||||
- name: Generate .SRCINFO
|
||||
run: |
|
||||
cp -r packaging/aur/kst4contest-git /tmp/kst4contest-git
|
||||
chown -R builder:builder /tmp/kst4contest-git
|
||||
|
||||
su builder -c \
|
||||
"cd /tmp/kst4contest-git && makepkg --printsrcinfo > .SRCINFO"
|
||||
|
||||
cp /tmp/kst4contest-git/.SRCINFO \
|
||||
packaging/aur/kst4contest-git/.SRCINFO
|
||||
|
||||
echo "Generated .SRCINFO:"
|
||||
cat packaging/aur/kst4contest-git/.SRCINFO
|
||||
|
||||
- name: Set up and verify AUR SSH
|
||||
env:
|
||||
AUR_SSH_PRIVATE_KEY: ${{ secrets.AUR_SSH_PRIVATE_KEY }}
|
||||
run: |
|
||||
mkdir -p "${AUR_SSH_DIR}"
|
||||
|
||||
printf '%s\n' "${AUR_SSH_PRIVATE_KEY}" \
|
||||
> "${AUR_SSH_DIR}/aur_ed25519"
|
||||
|
||||
sed -i 's/\r$//' "${AUR_SSH_DIR}/aur_ed25519"
|
||||
chmod 600 "${AUR_SSH_DIR}/aur_ed25519"
|
||||
|
||||
ssh-keygen -y \
|
||||
-f "${AUR_SSH_DIR}/aur_ed25519" \
|
||||
> /dev/null
|
||||
|
||||
ssh-keyscan \
|
||||
-T 10 \
|
||||
-t ed25519 \
|
||||
aur.archlinux.org \
|
||||
> "${AUR_SSH_DIR}/known_hosts"
|
||||
|
||||
if [ ! -s "${AUR_SSH_DIR}/known_hosts" ]; then
|
||||
echo "::error::No SSH host key was received from aur.archlinux.org."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Received AUR host-key fingerprint:"
|
||||
ssh-keygen -lf "${AUR_SSH_DIR}/known_hosts"
|
||||
|
||||
if ! ssh-keygen -lf "${AUR_SSH_DIR}/known_hosts" \
|
||||
| grep -Fq "SHA256:RFzBCUItH9LZS0cKB5UE6ceAYhBD5C8GeOBip8Z11+4"; then
|
||||
echo "::error::The AUR SSH host-key fingerprint does not match the official fingerprint."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
printf '%s\n' \
|
||||
"Host aur.archlinux.org" \
|
||||
" HostName aur.archlinux.org" \
|
||||
" User aur" \
|
||||
" IdentityFile ${AUR_SSH_DIR}/aur_ed25519" \
|
||||
" IdentitiesOnly yes" \
|
||||
" StrictHostKeyChecking yes" \
|
||||
" UserKnownHostsFile ${AUR_SSH_DIR}/known_hosts" \
|
||||
> "${AUR_SSH_DIR}/config"
|
||||
|
||||
chmod 600 "${AUR_SSH_DIR}/config"
|
||||
chmod 600 "${AUR_SSH_DIR}/known_hosts"
|
||||
|
||||
- name: Push package metadata to AUR
|
||||
env:
|
||||
GIT_SSH_COMMAND: ssh -F /tmp/aur-ssh/config
|
||||
run: |
|
||||
git config --global user.email "philipp@wagnersnetz.de"
|
||||
git config --global user.name "Philipp Wagner"
|
||||
|
||||
AUR_DIR="/tmp/aur/kst4contest-git"
|
||||
|
||||
mkdir -p "$(dirname "${AUR_DIR}")"
|
||||
|
||||
git -c init.defaultBranch=master clone \
|
||||
"ssh://aur@aur.archlinux.org/kst4contest-git.git" \
|
||||
"${AUR_DIR}"
|
||||
|
||||
cp packaging/aur/kst4contest-git/PKGBUILD \
|
||||
"${AUR_DIR}/PKGBUILD"
|
||||
|
||||
cp packaging/aur/kst4contest-git/.SRCINFO \
|
||||
"${AUR_DIR}/.SRCINFO"
|
||||
|
||||
git -C "${AUR_DIR}" add PKGBUILD .SRCINFO
|
||||
|
||||
if git -C "${AUR_DIR}" diff --cached --quiet; then
|
||||
echo "kst4contest-git: package metadata is already current."
|
||||
else
|
||||
git -C "${AUR_DIR}" commit \
|
||||
-m "Update pkgver to ${{ steps.ver.outputs.pkgver }}"
|
||||
|
||||
git -C "${AUR_DIR}" push origin HEAD:master
|
||||
|
||||
echo "kst4contest-git: successfully pushed to AUR."
|
||||
fi
|
||||
@@ -0,0 +1,177 @@
|
||||
name: Publish AUR Packages
|
||||
|
||||
on:
|
||||
workflow_run:
|
||||
workflows: ["Tagged Release Build"]
|
||||
types: [completed]
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
version:
|
||||
description: "Release tag (e.g. v1.41.1) — defaults to latest stable release"
|
||||
required: false
|
||||
default: ""
|
||||
dry_run:
|
||||
description: "Dry run — skip AUR push and repo commit (for testing)"
|
||||
type: choice
|
||||
options: ["false", "true"]
|
||||
default: "false"
|
||||
|
||||
env:
|
||||
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
|
||||
|
||||
jobs:
|
||||
publish-aur:
|
||||
if: >-
|
||||
github.event_name == 'workflow_dispatch' ||
|
||||
github.event.workflow_run.conclusion == 'success'
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: archlinux:latest
|
||||
|
||||
steps:
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
pacman -Sy --noconfirm git openssh curl base-devel nodejs
|
||||
useradd -m builder
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4.1.7
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Resolve release version
|
||||
id: ver
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
INPUT_VERSION: ${{ inputs.version }}
|
||||
run: |
|
||||
if [[ -n "${INPUT_VERSION}" ]]; then
|
||||
TAG="${INPUT_VERSION}"
|
||||
else
|
||||
TAG=$(curl -sf \
|
||||
-H "Authorization: Bearer ${GH_TOKEN}" \
|
||||
-H "Accept: application/vnd.github+json" \
|
||||
"https://api.github.com/repos/${{ github.repository }}/releases/latest" \
|
||||
| grep '"tag_name"' | sed 's/.*"tag_name": *"\([^"]*\)".*/\1/')
|
||||
fi
|
||||
PKGVER="${TAG#v}"
|
||||
echo "tag=${TAG} → pkgver=${PKGVER}"
|
||||
echo "tag=${TAG}" >> "$GITHUB_OUTPUT"
|
||||
echo "pkgver=${PKGVER}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Compute SHA256 checksums
|
||||
id: sha
|
||||
run: |
|
||||
TAG="${{ steps.ver.outputs.tag }}"
|
||||
REPO="${{ github.repository }}"
|
||||
|
||||
echo "Hashing pre-built Arch package..."
|
||||
SHA_BIN=$(curl -sfL \
|
||||
"https://github.com/${REPO}/releases/download/${TAG}/KST4Contest-${TAG}-archlinux-x86_64.pkg.tar.zst" \
|
||||
| sha256sum | awk '{print $1}')
|
||||
echo "bin: ${SHA_BIN}"
|
||||
|
||||
echo "Hashing source tarball..."
|
||||
SHA_SRC=$(curl -sfL \
|
||||
"https://github.com/${REPO}/archive/refs/tags/${TAG}.tar.gz" \
|
||||
| sha256sum | awk '{print $1}')
|
||||
echo "src: ${SHA_SRC}"
|
||||
|
||||
echo "bin=${SHA_BIN}" >> "$GITHUB_OUTPUT"
|
||||
echo "src=${SHA_SRC}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Update PKGBUILDs
|
||||
env:
|
||||
PKGVER: ${{ steps.ver.outputs.pkgver }}
|
||||
SHA_BIN: ${{ steps.sha.outputs.bin }}
|
||||
SHA_SRC: ${{ steps.sha.outputs.src }}
|
||||
run: |
|
||||
# kst4contest-bin
|
||||
sed -i "s/^pkgver=.*/pkgver=${PKGVER}/" packaging/aur/kst4contest-bin/PKGBUILD
|
||||
sed -i "s/^sha256sums=.*/sha256sums=('${SHA_BIN}')/" packaging/aur/kst4contest-bin/PKGBUILD
|
||||
|
||||
# kst4contest (source build)
|
||||
sed -i "s/^pkgver=.*/pkgver=${PKGVER}/" packaging/aur/kst4contest/PKGBUILD
|
||||
sed -i "s/^sha256sums=.*/sha256sums=('${SHA_SRC}')/" packaging/aur/kst4contest/PKGBUILD
|
||||
|
||||
# kst4contest-git: Basis aus pom.xml, Suffix aus git
|
||||
BASE_VER=$(grep -m1 '<version>' pom.xml \
|
||||
| sed 's/.*<version>\(.*\)<\/version>.*/\1/' | sed 's/[-.]nightly//')
|
||||
GIT_PKGVER="${BASE_VER}.r$(git rev-list --count HEAD).g$(git rev-parse --short HEAD)"
|
||||
sed -i "s/^pkgver=.*/pkgver=${GIT_PKGVER}/" packaging/aur/kst4contest-git/PKGBUILD
|
||||
|
||||
echo "=== Updated PKGBUILD versions ==="
|
||||
grep -H '^pkgver=\|^sha256sums=' packaging/aur/*/PKGBUILD
|
||||
|
||||
- name: Generate .SRCINFO files
|
||||
run: |
|
||||
for pkg in kst4contest-bin kst4contest kst4contest-git; do
|
||||
cp -r "packaging/aur/${pkg}" "/tmp/${pkg}"
|
||||
chown -R builder:builder "/tmp/${pkg}"
|
||||
su builder -c "cd /tmp/${pkg} && makepkg --printsrcinfo > .SRCINFO"
|
||||
cp "/tmp/${pkg}/.SRCINFO" "packaging/aur/${pkg}/.SRCINFO"
|
||||
echo "=== ${pkg}/.SRCINFO ==="
|
||||
cat "packaging/aur/${pkg}/.SRCINFO"
|
||||
done
|
||||
|
||||
- name: Commit updated PKGBUILDs to repo
|
||||
if: inputs.dry_run != 'true'
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
||||
git config user.name "github-actions[bot]"
|
||||
git remote set-url origin "https://x-access-token:${GITHUB_TOKEN}@github.com/${{ github.repository }}.git"
|
||||
git add packaging/aur/
|
||||
git diff --cached --quiet && echo "No PKGBUILD changes to commit." && exit 0
|
||||
git commit -m "chore: update AUR packages to ${{ steps.ver.outputs.tag }} [skip ci]"
|
||||
git push
|
||||
|
||||
- name: Set up AUR SSH
|
||||
if: inputs.dry_run != 'true'
|
||||
env:
|
||||
AUR_SSH_PRIVATE_KEY: ${{ secrets.AUR_SSH_PRIVATE_KEY }}
|
||||
run: |
|
||||
mkdir -p ~/.ssh
|
||||
printf '%s\n' "${AUR_SSH_PRIVATE_KEY}" > ~/.ssh/aur_ed25519
|
||||
chmod 600 ~/.ssh/aur_ed25519
|
||||
ssh-keyscan -t ed25519 aur.archlinux.org >> ~/.ssh/known_hosts
|
||||
cat >> ~/.ssh/config << 'EOF'
|
||||
Host aur.archlinux.org
|
||||
IdentityFile ~/.ssh/aur_ed25519
|
||||
User aur
|
||||
EOF
|
||||
|
||||
- name: Push to AUR
|
||||
if: inputs.dry_run != 'true'
|
||||
env:
|
||||
TAG: ${{ steps.ver.outputs.tag }}
|
||||
run: |
|
||||
git config --global user.email "philipp@wagnersnetz.de"
|
||||
git config --global user.name "Philipp Wagner"
|
||||
|
||||
push_to_aur() {
|
||||
local pkg="$1"
|
||||
local msg="$2"
|
||||
local aur_dir="/tmp/aur/${pkg}"
|
||||
|
||||
git clone "ssh://aur@aur.archlinux.org/${pkg}.git" "${aur_dir}" 2>/dev/null || {
|
||||
mkdir -p "${aur_dir}"
|
||||
git -C "${aur_dir}" init
|
||||
git -C "${aur_dir}" remote add origin "ssh://aur@aur.archlinux.org/${pkg}.git"
|
||||
}
|
||||
|
||||
cp "packaging/aur/${pkg}/PKGBUILD" "${aur_dir}/"
|
||||
cp "packaging/aur/${pkg}/.SRCINFO" "${aur_dir}/"
|
||||
git -C "${aur_dir}" add PKGBUILD .SRCINFO
|
||||
git -C "${aur_dir}" diff --cached --quiet \
|
||||
&& echo "${pkg}: no changes, skipping push" && return 0
|
||||
git -C "${aur_dir}" commit -m "${msg}"
|
||||
git -C "${aur_dir}" push origin HEAD:master
|
||||
echo "${pkg}: pushed to AUR"
|
||||
}
|
||||
|
||||
push_to_aur kst4contest-bin "Update to ${TAG}"
|
||||
push_to_aur kst4contest "Update to ${TAG}"
|
||||
push_to_aur kst4contest-git \
|
||||
"Update pkgver to $(grep '^pkgver=' packaging/aur/kst4contest-git/PKGBUILD | cut -d= -f2)"
|
||||
@@ -4,8 +4,12 @@ on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
schedule:
|
||||
- cron: "20 2 * * *"
|
||||
paths:
|
||||
- "src/**"
|
||||
- "pom.xml"
|
||||
- "mvnw"
|
||||
- "mvnw.cmd"
|
||||
- ".github/workflows/nightly-artifacts.yml"
|
||||
workflow_dispatch:
|
||||
|
||||
env:
|
||||
@@ -34,11 +38,11 @@ jobs:
|
||||
Add-Content -Path $env:GITHUB_ENV -Value "SHORT_SHA=$shortSha"
|
||||
Add-Content -Path $env:GITHUB_ENV -Value "ASSET_BASENAME=praktiKST-$version-$shortSha"
|
||||
|
||||
- name: Set up Java 17
|
||||
- name: Set up Java 21
|
||||
uses: actions/setup-java@v4.1.0
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: "17"
|
||||
java-version: "21"
|
||||
|
||||
- name: Install WiX Toolset
|
||||
shell: pwsh
|
||||
@@ -89,11 +93,11 @@ jobs:
|
||||
SHORT_SHA="${GITHUB_SHA::7}"
|
||||
echo "ASSET_BASENAME=KST4Contest-${VERSION}-${SHORT_SHA}" >> "$GITHUB_ENV"
|
||||
|
||||
- name: Set up Java 17
|
||||
- name: Set up Java 21
|
||||
uses: actions/setup-java@v4.1.0
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: "17"
|
||||
java-version: "21"
|
||||
|
||||
- name: Ensure mvnw is executable
|
||||
run: chmod +x mvnw
|
||||
@@ -169,11 +173,11 @@ jobs:
|
||||
SHORT_SHA="${GITHUB_SHA::7}"
|
||||
echo "ASSET_BASENAME=KST4Contest-${VERSION}-${SHORT_SHA}" >> "$GITHUB_ENV"
|
||||
|
||||
- name: Set up Java 17
|
||||
- name: Set up Java 21
|
||||
uses: actions/setup-java@v4.1.0
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: "17"
|
||||
java-version: "21"
|
||||
|
||||
- name: Install packaging dependencies
|
||||
run: |
|
||||
@@ -199,6 +203,7 @@ jobs:
|
||||
--main-class kst4contest.view.Kst4ContestApplication \
|
||||
--module-path target/dist-libs \
|
||||
--add-modules javafx.controls,javafx.graphics,javafx.fxml,javafx.web,javafx.media,java.sql,java.net.http,jdk.crypto.ec \
|
||||
--linux-package-deps "libgstreamer1.0-0,libgstreamer-plugins-base1.0-0,gstreamer1.0-plugins-good" \
|
||||
--dest dist
|
||||
DEB="$(ls dist/*.deb | head -n 1)"
|
||||
if [ -z "$DEB" ]; then
|
||||
@@ -227,11 +232,11 @@ jobs:
|
||||
SHORT_SHA="${GITHUB_SHA::7}"
|
||||
echo "ASSET_BASENAME=KST4Contest-${VERSION}-${SHORT_SHA}" >> "$GITHUB_ENV"
|
||||
|
||||
- name: Set up Java 17
|
||||
- name: Set up Java 21
|
||||
uses: actions/setup-java@v4.1.0
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: "17"
|
||||
java-version: "21"
|
||||
|
||||
- name: Install packaging dependencies
|
||||
run: |
|
||||
@@ -257,6 +262,7 @@ jobs:
|
||||
--main-class kst4contest.view.Kst4ContestApplication \
|
||||
--module-path target/dist-libs \
|
||||
--add-modules javafx.controls,javafx.graphics,javafx.fxml,javafx.web,javafx.media,java.sql,java.net.http,jdk.crypto.ec \
|
||||
--linux-package-deps "gstreamer1,gstreamer1-plugins-base,gstreamer1-plugins-good" \
|
||||
--dest dist
|
||||
RPM="$(ls dist/*.rpm | head -n 1)"
|
||||
if [ -z "$RPM" ]; then
|
||||
@@ -287,11 +293,11 @@ jobs:
|
||||
echo "SHORT_SHA=$SHORT_SHA" >> "$GITHUB_ENV"
|
||||
echo "ASSET_BASENAME=KST4Contest-${VERSION}-${SHORT_SHA}" >> "$GITHUB_ENV"
|
||||
|
||||
- name: Set up Java 17
|
||||
- name: Set up Java 21
|
||||
uses: actions/setup-java@v4.1.0
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: "17"
|
||||
java-version: "21"
|
||||
|
||||
- name: Install packaging dependencies
|
||||
run: |
|
||||
@@ -346,21 +352,42 @@ jobs:
|
||||
if [ -f "$PKGROOT/usr/lib/KST4Contest/lib/KST4Contest.png" ]; then
|
||||
cp "$PKGROOT/usr/lib/KST4Contest/lib/KST4Contest.png" "$PKGROOT/usr/share/icons/hicolor/256x256/apps/KST4Contest.png"
|
||||
fi
|
||||
INSTALLED_SIZE=$(du -sk "$PKGROOT" | cut -f1)
|
||||
cat > "$PKGROOT/.PKGINFO" << EOF
|
||||
pkgname = kst4contest
|
||||
pkgbase = kst4contest
|
||||
pkgver = ${PKGVER}-1
|
||||
pkgdesc = KST4Contest amateur radio contest logger
|
||||
url = https://github.com/${{ github.repository }}
|
||||
builddate = $(date +%s)
|
||||
packager = GitHub Actions
|
||||
size = ${INSTALLED_SIZE}
|
||||
arch = ${ARCH}
|
||||
license = custom
|
||||
depend = java-runtime
|
||||
EOF
|
||||
tar --zstd -cf "dist/${ASSET_BASENAME}-archlinux-${ARCH}.pkg.tar.zst" -C "$PKGROOT" .
|
||||
INSTALLED_SIZE=$(du -sb "$PKGROOT" | cut -f1)
|
||||
BUILDDATE=$(date +%s)
|
||||
WORKFLOW_SHA256=$(sha256sum "$GITHUB_WORKSPACE/.github/workflows/nightly-artifacts.yml" | awk '{print $1}')
|
||||
{
|
||||
echo "pkgname = kst4contest"
|
||||
echo "pkgbase = kst4contest"
|
||||
echo "xdata = pkgtype=pkg"
|
||||
echo "pkgver = ${PKGVER}-1"
|
||||
echo "pkgdesc = KST4Contest amateur radio contest logger"
|
||||
echo "url = https://github.com/${{ github.repository }}"
|
||||
echo "builddate = ${BUILDDATE}"
|
||||
echo "packager = GitHub Actions"
|
||||
echo "size = ${INSTALLED_SIZE}"
|
||||
echo "arch = ${ARCH}"
|
||||
echo "license = custom"
|
||||
echo "depend = java-runtime"
|
||||
echo "depend = gst-plugins-base"
|
||||
echo "depend = gst-plugins-good"
|
||||
} > "$PKGROOT/.PKGINFO"
|
||||
{
|
||||
echo "format = 2"
|
||||
echo "pkgname = kst4contest"
|
||||
echo "pkgbase = kst4contest"
|
||||
echo "pkgver = ${PKGVER}-1"
|
||||
echo "pkgarch = ${ARCH}"
|
||||
echo "pkgbuild_sha256sum = ${WORKFLOW_SHA256}"
|
||||
echo "packager = GitHub Actions"
|
||||
echo "builddate = ${BUILDDATE}"
|
||||
echo "builddir = /build"
|
||||
echo "startdir = /build"
|
||||
echo "buildtool = makepkg"
|
||||
echo "buildtoolver = 7.0.0-1-x86_64"
|
||||
echo "buildenv = !distcc !color !ccache check !sign"
|
||||
echo "options = !strip docs libtool staticlibs emptydirs zipman purge !debug !lto"
|
||||
} > "$PKGROOT/.BUILDINFO"
|
||||
tar --zstd --transform 's|^\./||' -cf "dist/${ASSET_BASENAME}-archlinux-${ARCH}.pkg.tar.zst" -C "$PKGROOT" .
|
||||
|
||||
- name: Upload Arch Linux artifact
|
||||
uses: actions/upload-artifact@v4.3.4
|
||||
@@ -385,11 +412,11 @@ jobs:
|
||||
echo "SHORT_SHA=$SHORT_SHA" >> "$GITHUB_ENV"
|
||||
echo "ASSET_BASENAME=KST4Contest-${VERSION}-${SHORT_SHA}" >> "$GITHUB_ENV"
|
||||
|
||||
- name: Set up Java 17
|
||||
- name: Set up Java 21
|
||||
uses: actions/setup-java@v4.1.0
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: "17"
|
||||
java-version: "21"
|
||||
|
||||
- name: Ensure mvnw is executable
|
||||
run: chmod +x mvnw
|
||||
@@ -428,14 +455,18 @@ jobs:
|
||||
finish-args:
|
||||
- --socket=wayland
|
||||
- --socket=x11
|
||||
- --socket=pulseaudio
|
||||
- --share=network
|
||||
- --share=ipc
|
||||
- --device=dri
|
||||
- --filesystem=~/.praktiKST
|
||||
- --env=ALSA_CONFIG_PATH=/app/share/alsa/asound.conf
|
||||
modules:
|
||||
- name: kst4contest
|
||||
buildsystem: simple
|
||||
build-commands:
|
||||
- install -d /app/lib/KST4Contest /app/bin /app/share/applications
|
||||
- install -d /app/lib/KST4Contest /app/bin /app/share/applications /app/share/alsa
|
||||
- printf '@include /usr/share/alsa/alsa.conf\npcm.!default { type pulse }\nctl.!default { type pulse }\n' > /app/share/alsa/asound.conf
|
||||
- cp -a . /app/lib/KST4Contest/
|
||||
- printf '#!/bin/sh\nexec /app/lib/KST4Contest/bin/KST4Contest "$@"\n' > /app/bin/KST4Contest
|
||||
- chmod 755 /app/bin/KST4Contest
|
||||
@@ -572,11 +603,11 @@ jobs:
|
||||
echo "ASSET_BASENAME=KST4Contest-${VERSION}-${SHORT_SHA}" >> "$GITHUB_ENV"
|
||||
echo "ARCH=$ARCH" >> "$GITHUB_ENV"
|
||||
|
||||
- name: Set up Java 17
|
||||
- name: Set up Java 21
|
||||
uses: actions/setup-java@v4.1.0
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: "17"
|
||||
java-version: "21"
|
||||
|
||||
- name: Ensure mvnw is executable
|
||||
run: chmod +x mvnw
|
||||
|
||||
@@ -11,18 +11,18 @@ env:
|
||||
|
||||
jobs:
|
||||
compile:
|
||||
name: Compile (Java 17)
|
||||
name: Compile (Java 21)
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4.1.7
|
||||
|
||||
- name: Set up Java 17
|
||||
- name: Set up Java 21
|
||||
uses: actions/setup-java@v4.1.0
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: "17"
|
||||
java-version: "21"
|
||||
|
||||
- name: Ensure mvnw is executable
|
||||
run: chmod +x mvnw
|
||||
|
||||
@@ -22,11 +22,11 @@ jobs:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4.1.7
|
||||
|
||||
- name: Set up Java 17
|
||||
- name: Set up Java 21
|
||||
uses: actions/setup-java@v4.1.0
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: "17"
|
||||
java-version: "21"
|
||||
|
||||
- name: Install WiX Toolset
|
||||
shell: pwsh
|
||||
@@ -70,11 +70,11 @@ jobs:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4.1.7
|
||||
|
||||
- name: Set up Java 17
|
||||
- name: Set up Java 21
|
||||
uses: actions/setup-java@v4.1.0
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: "17"
|
||||
java-version: "21"
|
||||
|
||||
- name: Ensure mvnw is executable
|
||||
run: chmod +x mvnw
|
||||
@@ -143,11 +143,11 @@ jobs:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4.1.7
|
||||
|
||||
- name: Set up Java 17
|
||||
- name: Set up Java 21
|
||||
uses: actions/setup-java@v4.1.0
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: "17"
|
||||
java-version: "21"
|
||||
|
||||
- name: Install packaging dependencies
|
||||
run: |
|
||||
@@ -173,6 +173,7 @@ jobs:
|
||||
--main-class kst4contest.view.Kst4ContestApplication \
|
||||
--module-path target/dist-libs \
|
||||
--add-modules javafx.controls,javafx.graphics,javafx.fxml,javafx.web,javafx.media,java.sql,java.net.http,jdk.crypto.ec \
|
||||
--linux-package-deps "libgstreamer1.0-0,libgstreamer-plugins-base1.0-0,gstreamer1.0-plugins-good" \
|
||||
--dest dist
|
||||
DEB="$(ls dist/*.deb | head -n 1)"
|
||||
if [ -z "$DEB" ]; then
|
||||
@@ -194,11 +195,11 @@ jobs:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4.1.7
|
||||
|
||||
- name: Set up Java 17
|
||||
- name: Set up Java 21
|
||||
uses: actions/setup-java@v4.1.0
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: "17"
|
||||
java-version: "21"
|
||||
|
||||
- name: Install packaging dependencies
|
||||
run: |
|
||||
@@ -224,6 +225,7 @@ jobs:
|
||||
--main-class kst4contest.view.Kst4ContestApplication \
|
||||
--module-path target/dist-libs \
|
||||
--add-modules javafx.controls,javafx.graphics,javafx.fxml,javafx.web,javafx.media,java.sql,java.net.http,jdk.crypto.ec \
|
||||
--linux-package-deps "gstreamer1,gstreamer1-plugins-base,gstreamer1-plugins-good" \
|
||||
--dest dist
|
||||
RPM="$(ls dist/*.rpm | head -n 1)"
|
||||
if [ -z "$RPM" ]; then
|
||||
@@ -245,11 +247,11 @@ jobs:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4.1.7
|
||||
|
||||
- name: Set up Java 17
|
||||
- name: Set up Java 21
|
||||
uses: actions/setup-java@v4.1.0
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: "17"
|
||||
java-version: "21"
|
||||
|
||||
- name: Install packaging dependencies
|
||||
run: |
|
||||
@@ -304,21 +306,42 @@ jobs:
|
||||
if [ -f "$PKGROOT/usr/lib/KST4Contest/lib/KST4Contest.png" ]; then
|
||||
cp "$PKGROOT/usr/lib/KST4Contest/lib/KST4Contest.png" "$PKGROOT/usr/share/icons/hicolor/256x256/apps/KST4Contest.png"
|
||||
fi
|
||||
INSTALLED_SIZE=$(du -sk "$PKGROOT" | cut -f1)
|
||||
cat > "$PKGROOT/.PKGINFO" << EOF
|
||||
pkgname = kst4contest
|
||||
pkgbase = kst4contest
|
||||
pkgver = ${PKGVER}-1
|
||||
pkgdesc = KST4Contest amateur radio contest logger
|
||||
url = https://github.com/${{ github.repository }}
|
||||
builddate = $(date +%s)
|
||||
packager = GitHub Actions
|
||||
size = ${INSTALLED_SIZE}
|
||||
arch = ${ARCH}
|
||||
license = custom
|
||||
depend = java-runtime
|
||||
EOF
|
||||
tar --zstd -cf "dist/KST4Contest-${{ github.ref_name }}-archlinux-${ARCH}.pkg.tar.zst" -C "$PKGROOT" .
|
||||
INSTALLED_SIZE=$(du -sb "$PKGROOT" | cut -f1)
|
||||
BUILDDATE=$(date +%s)
|
||||
WORKFLOW_SHA256=$(sha256sum "$GITHUB_WORKSPACE/.github/workflows/tagged-release.yml" | awk '{print $1}')
|
||||
{
|
||||
echo "pkgname = kst4contest"
|
||||
echo "pkgbase = kst4contest"
|
||||
echo "xdata = pkgtype=pkg"
|
||||
echo "pkgver = ${PKGVER}-1"
|
||||
echo "pkgdesc = KST4Contest amateur radio contest logger"
|
||||
echo "url = https://github.com/${{ github.repository }}"
|
||||
echo "builddate = ${BUILDDATE}"
|
||||
echo "packager = GitHub Actions"
|
||||
echo "size = ${INSTALLED_SIZE}"
|
||||
echo "arch = ${ARCH}"
|
||||
echo "license = custom"
|
||||
echo "depend = java-runtime"
|
||||
echo "depend = gst-plugins-base"
|
||||
echo "depend = gst-plugins-good"
|
||||
} > "$PKGROOT/.PKGINFO"
|
||||
{
|
||||
echo "format = 2"
|
||||
echo "pkgname = kst4contest"
|
||||
echo "pkgbase = kst4contest"
|
||||
echo "pkgver = ${PKGVER}-1"
|
||||
echo "pkgarch = ${ARCH}"
|
||||
echo "pkgbuild_sha256sum = ${WORKFLOW_SHA256}"
|
||||
echo "packager = GitHub Actions"
|
||||
echo "builddate = ${BUILDDATE}"
|
||||
echo "builddir = /build"
|
||||
echo "startdir = /build"
|
||||
echo "buildtool = makepkg"
|
||||
echo "buildtoolver = 7.0.0-1-x86_64"
|
||||
echo "buildenv = !distcc !color !ccache check !sign"
|
||||
echo "options = !strip docs libtool staticlibs emptydirs zipman purge !debug !lto"
|
||||
} > "$PKGROOT/.BUILDINFO"
|
||||
tar --zstd --transform 's|^\./||' -cf "dist/KST4Contest-${{ github.ref_name }}-archlinux-${ARCH}.pkg.tar.zst" -C "$PKGROOT" .
|
||||
|
||||
- name: Upload Arch Linux artifact
|
||||
uses: actions/upload-artifact@v4.3.4
|
||||
@@ -334,11 +357,11 @@ jobs:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4.1.7
|
||||
|
||||
- name: Set up Java 17
|
||||
- name: Set up Java 21
|
||||
uses: actions/setup-java@v4.1.0
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: "17"
|
||||
java-version: "21"
|
||||
|
||||
- name: Ensure mvnw is executable
|
||||
run: chmod +x mvnw
|
||||
@@ -377,14 +400,18 @@ jobs:
|
||||
finish-args:
|
||||
- --socket=wayland
|
||||
- --socket=x11
|
||||
- --socket=pulseaudio
|
||||
- --share=network
|
||||
- --share=ipc
|
||||
- --device=dri
|
||||
- --filesystem=~/.praktiKST
|
||||
- --env=ALSA_CONFIG_PATH=/app/share/alsa/asound.conf
|
||||
modules:
|
||||
- name: kst4contest
|
||||
buildsystem: simple
|
||||
build-commands:
|
||||
- install -d /app/lib/KST4Contest /app/bin /app/share/applications
|
||||
- install -d /app/lib/KST4Contest /app/bin /app/share/applications /app/share/alsa
|
||||
- printf '@include /usr/share/alsa/alsa.conf\npcm.!default { type pulse }\nctl.!default { type pulse }\n' > /app/share/alsa/asound.conf
|
||||
- cp -a . /app/lib/KST4Contest/
|
||||
- printf '#!/bin/sh\nexec /app/lib/KST4Contest/bin/KST4Contest "$@"\n' > /app/bin/KST4Contest
|
||||
- chmod 755 /app/bin/KST4Contest
|
||||
@@ -408,12 +435,14 @@ jobs:
|
||||
FLATPAK_GPG_KEY_ID=$(gpg --list-secret-keys --with-colons | awk -F: '/^fpr/{print $10; exit}')
|
||||
echo "FLATPAK_GPG_KEY_ID=$FLATPAK_GPG_KEY_ID" >> "$GITHUB_ENV"
|
||||
|
||||
- name: Determine OSTree branch
|
||||
- name: Determine OSTree branch and flatpakref filename
|
||||
run: |
|
||||
if [[ "${{ github.ref_name }}" == beta-* ]]; then
|
||||
echo "OSTREE_BRANCH=beta" >> "$GITHUB_ENV"
|
||||
echo "FLATPAKREF_NAME=de.x08.KST4Contest.beta.flatpakref" >> "$GITHUB_ENV"
|
||||
else
|
||||
echo "OSTREE_BRANCH=stable" >> "$GITHUB_ENV"
|
||||
echo "FLATPAKREF_NAME=de.x08.KST4Contest.flatpakref" >> "$GITHUB_ENV"
|
||||
fi
|
||||
|
||||
- name: Build Flatpak repo
|
||||
@@ -427,7 +456,7 @@ jobs:
|
||||
REPO_NAME="${GITHUB_REPOSITORY#*/}"
|
||||
PAGES_URL="https://${GITHUB_REPOSITORY_OWNER}.github.io/${REPO_NAME}/"
|
||||
GPG_KEY_B64=$(gpg --export "$FLATPAK_GPG_KEY_ID" | base64 -w 0)
|
||||
cat > "dist/de.x08.KST4Contest.${OSTREE_BRANCH}.flatpakref" << EOF
|
||||
cat > "dist/${FLATPAKREF_NAME}" << EOF
|
||||
[Flatpak Ref]
|
||||
Name=de.x08.KST4Contest
|
||||
Branch=${OSTREE_BRANCH}
|
||||
@@ -442,7 +471,7 @@ jobs:
|
||||
uses: actions/upload-artifact@v4.3.4
|
||||
with:
|
||||
name: flatpakref
|
||||
path: dist/de.x08.KST4Contest.*.flatpakref
|
||||
path: dist/*.flatpakref
|
||||
|
||||
- name: Upload Flatpak OSTree repo
|
||||
uses: actions/upload-artifact@v4.3.4
|
||||
@@ -461,11 +490,11 @@ jobs:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4.1.7
|
||||
|
||||
- name: Set up Java 17
|
||||
- name: Set up Java 21
|
||||
uses: actions/setup-java@v4.1.0
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: "17"
|
||||
java-version: "21"
|
||||
|
||||
- name: Ensure mvnw is executable
|
||||
run: chmod +x mvnw
|
||||
@@ -720,7 +749,7 @@ jobs:
|
||||
release-assets/debian/KST4Contest-${{ github.ref_name }}-debian-amd64.deb,
|
||||
release-assets/fedora/KST4Contest-${{ github.ref_name }}-fedora-x86_64.rpm,
|
||||
release-assets/archlinux/KST4Contest-${{ github.ref_name }}-archlinux-*.pkg.tar.zst,
|
||||
release-assets/flatpakref/de.x08.KST4Contest.flatpakref,
|
||||
release-assets/flatpakref/*.flatpakref,
|
||||
release-assets/macos/KST4Contest-${{ github.ref_name }}-macos-*.dmg,
|
||||
release-assets/docs/KST4Contest-${{ github.ref_name }}-manual-en.pdf,
|
||||
release-assets/docs/KST4Contest-${{ github.ref_name }}-manual-de.pdf
|
||||
|
||||
+10
-1
@@ -34,4 +34,13 @@ build/
|
||||
dist/
|
||||
|
||||
#zip files for local backups
|
||||
*.zip
|
||||
*.zip
|
||||
|
||||
# node Modules in website
|
||||
website/node_modules/
|
||||
|
||||
# built website output (rebuilt on the server)
|
||||
website/_site/
|
||||
|
||||
# Local secrets for act testing
|
||||
.secrets
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
# KST4Contest
|
||||
|
||||
KST4Contest (also known as pratiKST) is a Java-based chat client for ON4KST, focused on VHF/UHF/SHF contest operation.
|
||||
KST4Contest (also known as pratiKST) is a Java-based chat client for [ON4KST](http://www.on4kst.com/chat), focused on VHF/UHF/SHF contest operation.
|
||||
|
||||
## Website
|
||||
|
||||
The offical Website of KST4Contest is now instead of [do5amf.funkerportal.de](https://do5amf.funkerportal.de) the new website [here](https://kst4contest.hamradioonline.de) [https://kst4contest.hamradioonline.de](https://kst4contest.hamradioonline.de)
|
||||
|
||||
## Documentation
|
||||
|
||||
@@ -35,4 +39,4 @@ Wiki Publishing:
|
||||
|
||||
Builds:
|
||||
|
||||
[](https://github.com/praktimarc/kst4contest/actions/workflows/nightly-artifacts.yml)
|
||||
[](https://github.com/praktimarc/kst4contest/actions/workflows/nightly-artifacts.yml)
|
||||
|
||||
+14
-42
@@ -1,53 +1,25 @@
|
||||
# KST4Contest – Wiki
|
||||
# KST4Contest
|
||||
|
||||
**KST4Contest** (auch bekannt als *PraktiKST*) ist ein Java-basierter Chat-Client für den [ON4KST-Chat](http://www.on4kst.info/chat/), speziell entwickelt für den Contest-Betrieb auf den VHF/UHF/SHF-Bändern.
|
||||
KST4Contest is a desktop client for the [ON4KST Chat](https://www.on4kst.org/chat/login.php), developed for VHF, UHF and SHF contest operation. It combines chat, candidate selection, sked planning, aircraft scatter information and connections to logging and station software.
|
||||
|
||||
Entwickelt von **DO5AMF (Marc Fröhlich)**, Operator bei DM5M.
|
||||
KST4Contest ist ein Desktop-Client für den [ON4KST-Chat](https://www.on4kst.org/chat/login.php), der für den Contest-Betrieb auf den VHF-, UHF- und SHF-Bändern entwickelt wurde. Er verbindet Chat, Stationsauswahl, Sked-Planung, Aircraft-Scatter-Daten sowie die Anbindung an Log- und Stationssoftware.
|
||||
|
||||
Developed by / Entwickelt von **DO5AMF (Marc Fröhlich)**, operator at / Operator bei DM5M.
|
||||
|
||||
---
|
||||
|
||||
## 🌐 Sprache / Language
|
||||
## Language / Sprache
|
||||
|
||||
| 🇩🇪 Deutsch | 🇬🇧 English |
|
||||
| Deutsch | English |
|
||||
|---|---|
|
||||
| [Startseite (Deutsch)](de-Home) | [Home (English)](en-Home) |
|
||||
| [Deutsches Handbuch](de-Home) | [English manual](en-Home) |
|
||||
|
||||
---
|
||||
|
||||
## 🇩🇪 Inhalt (Deutsch)
|
||||
## Project links / Projektlinks
|
||||
|
||||
| Seite | Inhalt |
|
||||
|---|---|
|
||||
| [Installation](de-Installation) | Download, Java-Voraussetzungen, Update |
|
||||
| [Konfiguration](de-Konfiguration) | Alle Einstellungen im Detail |
|
||||
| [Log-Synchronisation](de-Log-Synchronisation) | UCXLog, N1MM+, QARTest, DXLog.net, WinTest |
|
||||
| [AirScout-Integration](de-AirScout-Integration) | Flugzeug-Scatter-Erkennung |
|
||||
| [DX-Cluster-Server](de-DX-Cluster-Server) | Integrierter DX-Cluster für das Log-Programm |
|
||||
| [Funktionen](de-Funktionen) | Alle Features im Überblick |
|
||||
| [Makros und Variablen](de-Makros-und-Variablen) | Text-Snippets, Shortcuts, Variablen |
|
||||
| [Benutzeroberfläche](de-Benutzeroberflaeche) | UI-Erklärung und Bedienung |
|
||||
| [Changelog](de-Changelog) | Versionsgeschichte |
|
||||
|
||||
---
|
||||
|
||||
## 🇬🇧 Contents (English)
|
||||
|
||||
| Page | Contents |
|
||||
|---|---|
|
||||
| [Installation](en-Installation) | Download, Java requirements, updates |
|
||||
| [Configuration](en-Configuration) | All settings in detail |
|
||||
| [Log Synchronisation](en-Log-Sync) | UCXLog, N1MM+, QARTest, DXLog.net, WinTest |
|
||||
| [AirScout Integration](en-AirScout-Integration) | Aircraft scatter detection |
|
||||
| [DX Cluster Server](en-DX-Cluster-Server) | Built-in DX cluster for your logging software |
|
||||
| [Features](en-Features) | All features at a glance |
|
||||
| [Macros and Variables](en-Macros-and-Variables) | Text snippets, shortcuts, variables |
|
||||
| [User Interface](en-User-Interface) | UI explained and how to operate it |
|
||||
| [Changelog](en-Changelog) | Version history |
|
||||
|
||||
---
|
||||
|
||||
## Schnellinfo / Quick Info
|
||||
|
||||
- **Download**: https://github.com/praktimarc/kst4contest/releases
|
||||
- **GitHub**: https://github.com/praktimarc/kst4contest
|
||||
- **Kontakt / Contact**: praktimarc+kst4contest@gmail.com
|
||||
- [Project website / Projektwebseite](https://kst4contest.hamradioonline.de/)
|
||||
- [Download the current stable release / Aktuelle stabile Version herunterladen](https://github.com/praktimarc/kst4contest/releases/latest)
|
||||
- [Source code / Quellcode](https://github.com/praktimarc/kst4contest)
|
||||
- [Bug reports and feature requests / Fehler und Funktionswünsche](https://github.com/praktimarc/kst4contest/issues)
|
||||
- [Online manual / Online-Handbuch](https://kst4contest.hamradioonline.de/manual/)
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 82 KiB |
@@ -2,75 +2,202 @@
|
||||
|
||||
> 🇬🇧 [English version](en-DX-Cluster-Server) | 🇩🇪 Du liest gerade die deutsche Version
|
||||
|
||||
Ab **Version 1.23** enthält KST4Contest einen integrierten DX-Cluster-Server. Dieser sendet Spots direkt an das Logprogramm, wenn eine Richtungs-Warnung ausgelöst wird.
|
||||
Seit Version 1.23 enthält KST4Contest einen lokalen DX-Cluster-Server. Er übergibt erkannte Richtungsgelegenheiten und die dazugehörige Frequenz an den DX-Cluster-Client eines Logprogramms.
|
||||
|
||||
*(Idee von OM0AAO, Viliam Petrik – danke!)*
|
||||
Die Idee dazu stammt von OM0AAO, Viliam Petrik. Vielen Dank!
|
||||
|
||||
---
|
||||
|
||||
## Wozu dient der integrierte DX-Cluster-Server?
|
||||
## Warum überhaupt ein eigener DX-Cluster-Server?
|
||||
|
||||
Wenn KST4Contest erkennt, dass eine Station aus der eigenen Richtung ein Sked anfragt und gleichzeitig eine QRG bekannt ist, wird **automatisch ein DX-Cluster-Spot generiert** und an den Cluster-Client des Logprogramms gesendet.
|
||||
Eine interessante Frequenz im Chat zu erkennen ist nur der erste Schritt. Im Contest muss diese Information dort ankommen, wo sie unmittelbar verwendet werden kann: im Logprogramm und dessen Bandmap.
|
||||
|
||||
Das Logprogramm zeigt den Spot in der Bandkarte an. Ein Klick auf den Spot stellt Frequenz und Mode des Transceivers direkt ein – ohne manuelles Eintippen.
|
||||
KST4Contest verbindet deshalb zwei bereits vorhandene Informationen:
|
||||
|
||||
1. Aus einer gerichteten Chat-Nachricht lässt sich abschätzen, in welche Richtung die sendende Station ihre Antenne wahrscheinlich ausgerichtet hat.
|
||||
2. Aus derselben oder einer vorherigen Nachricht kann eine Frequenz der Station bekannt sein.
|
||||
|
||||
Treffen beide Informationen zusammen, erzeugt KST4Contest einen lokalen DX-Cluster-Spot. Der Logger kann diesen Spot in seiner Bandmap anzeigen und – abhängig von seiner eigenen Konfiguration – den Transceiver nach einem Klick auf die entsprechende Frequenz einstellen.
|
||||
|
||||
Im Klartext: Die Information muss nicht erst im Chat gefunden, gelesen, gemerkt und anschließend erneut in den Logger eingetragen werden. Genau diese kleinen Unterbrechungen kosten im Contest überraschend viel Aufmerksamkeit.
|
||||
|
||||
---
|
||||
|
||||
## Einrichtung
|
||||
## Wie wird eine Richtungsgelegenheit hergeleitet?
|
||||
|
||||
### In KST4Contest
|
||||
Angenommen, Station A schreibt eine gerichtete Nachricht an Station B. KST4Contest geht in diesem Fall davon aus, dass Station A ihre Antenne zumindest ungefähr in Richtung von Station B ausgerichtet hat.
|
||||
|
||||
In den Preferences → **DX-Cluster-Server-Einstellungen**:
|
||||
Anschließend werden zwei Richtungen verglichen:
|
||||
|
||||
1. **Port** des internen Servers eintragen (z. B. 7300 oder 8000 – muss mit dem Logprogramm übereinstimmen).
|
||||
2. **Spotter-Rufzeichen** eintragen – **unbedingt ein anderes Rufzeichen als das Contest-Rufzeichen verwenden!**
|
||||
- Grund: Logprogramme filtern Spots, die vom eigenen Rufzeichen stammen, als „gearbeitet" heraus. Wenn der Spotter dasselbe Rufzeichen hat, werden die Spots nicht angezeigt.
|
||||
3. **Angenommene MHz** eintragen: Bei Frequenzangaben wie „.205" im Chat muss KST4Contest entscheiden, ob 144.205, 432.205 oder 1296.205 gemeint ist. Bei Einband-Contests einfach die entsprechende Bandmitte eintragen. Vollständige Frequenzangaben wie „144.205" oder „1296.338" im Chat werden immer korrekt erkannt.
|
||||
- die Richtung von Station A zu Station B,
|
||||
- die Richtung von Station A zur eigenen Station.
|
||||
|
||||
### In UCXLog
|
||||
Liegt die eigene Station innerhalb des konfigurierten Antennen-Öffnungswinkels und befindet sich Station A innerhalb des eingestellten maximalen QRB, wird die Situation als Richtungsgelegenheit behandelt.
|
||||
|
||||
- Verbindung zu einem DX-Cluster-Server konfigurieren:
|
||||
- Host: `127.0.0.1` (oder IP des KST4Contest-Computers)
|
||||
- Port: Wie in KST4Contest konfiguriert
|
||||
- Passwort: kann leer bleiben
|
||||
- Über die Schaltfläche **„Send a test message to your log"** kann die Verbindung getestet werden.
|
||||
Ein DX-Cluster-Spot wird nur erzeugt, wenn alle folgenden Bedingungen erfüllt sind:
|
||||
|
||||
### In N1MM+
|
||||
1. Die Nachricht ist an eine konkrete andere Station gerichtet.
|
||||
2. Für Absender und Empfänger sind gültige Locator bekannt.
|
||||
3. Der Absender liegt innerhalb des konfigurierten maximalen QRB.
|
||||
4. Die eigene Station liegt aus Sicht des Absenders innerhalb des konfigurierten Antennen-Öffnungswinkels.
|
||||
5. Für den Absender ist eine verwertbare Frequenz bekannt.
|
||||
6. Der lokale DX-Cluster-Server ist aktiviert.
|
||||
|
||||
Ähnliche Einstellungen:
|
||||
- Host: `127.0.0.1` (oder IP des KST4Contest-Computers)
|
||||
- Port: Wie in KST4Contest konfiguriert
|
||||
Das Verfahren ist bewusst eine geometrische Herleitung. Es beweist weder, dass die Gegenstation tatsächlich mit genau dieser Antennenrichtung arbeitet, noch ersetzt es eine Ausbreitungsberechnung. Es erkennt eine plausible Gelegenheit. Mehr sollte man aus einer Chat-Nachricht auch nicht herauslesen.
|
||||
|
||||
---
|
||||
|
||||
## Funktionsweise
|
||||
## Welche Frequenz wird verwendet?
|
||||
|
||||
Ein Spot wird generiert, wenn **beide** Bedingungen erfüllt sind:
|
||||
Vollständige Frequenzangaben können direkt verarbeitet werden, beispielsweise:
|
||||
|
||||
1. Eine **Richtungs-Warnung** wurde ausgelöst (Station macht ein Sked in die eigene Richtung).
|
||||
2. **QRG der Station ist bekannt** (aus dem Chat ausgelesen oder manuell eingetragen).
|
||||
```text
|
||||
144.205
|
||||
432.088
|
||||
1296.338
|
||||
```
|
||||
|
||||
Der generierte Spot enthält:
|
||||
- Rufzeichen der Station
|
||||
- Frequenz
|
||||
- Spotterzeit
|
||||
Im Chat werden jedoch häufig nur relative Angaben geschrieben:
|
||||
|
||||
Das Logprogramm kann den Spot dann in der Bandkarte anzeigen und den TRX per Mausklick auf die Frequenz abstimmen.
|
||||
```text
|
||||
205
|
||||
.205
|
||||
338
|
||||
```
|
||||
|
||||
In diesem Fall fehlt das Band. KST4Contest ergänzt deshalb das unter **Fallback band in MHz** konfigurierte Bandpräfix.
|
||||
|
||||
Beispiel:
|
||||
|
||||
```text
|
||||
Fallback-Band: 144
|
||||
Chat-Frequenz: .205
|
||||
DX-Cluster-Frequenz: 144205.0 kHz
|
||||
```
|
||||
|
||||
Bei einem Einband-Contest ist diese Annahme normalerweise eindeutig. Bei gleichzeitigem Betrieb mehrerer Bänder kann sie falsch sein. Das Fallback-Band sollte deshalb zu dem Band passen, das in der betreffenden Chat-Kategorie überwiegend verwendet wird.
|
||||
|
||||
Die weitergehende Frage, ob sich das Band künftig aus der konkreten Arbeitsfrequenz oder dem Stationskontext ableiten lässt, wird nach Abschluss des Manuals gesondert geprüft.
|
||||
|
||||
---
|
||||
|
||||
## Multi-Computer-Setup
|
||||
## Einrichtung in KST4Contest
|
||||
|
||||
Wenn KST4Contest auf einem separaten Computer läuft (nicht auf dem Log-Computer):
|
||||
Öffne den Reiter **Notification** in den Preferences.
|
||||
|
||||
- Host im Logprogramm: IP des KST4Contest-Computers (nicht `127.0.0.1`)
|
||||
- Entspricht der Konfiguration der QSO-UDP-Broadcast-Pakete (siehe [Log-Synchronisation](de-Log-Synchronisation))
|
||||

|
||||
|
||||
Konfiguriere anschließend:
|
||||
|
||||
1. **Enable the local DX Cluster server …** aktivieren.
|
||||
2. Einen freien **TCP port** eintragen. Standard ist `8000`.
|
||||
3. Das passende **Fallback band in MHz** eintragen.
|
||||
4. Ein **Spotter callsign** festlegen.
|
||||
|
||||
Für das Spotter-Rufzeichen sollte nach Möglichkeit ein anderes Rufzeichen als das Contest-Rufzeichen verwendet werden. Einige Logger filtern Spots, die scheinbar von der eigenen Station stammen. Das Ergebnis wäre technisch korrekt erzeugt, aber in der Bandmap trotzdem unsichtbar – eine besonders unproduktive Art von Erfolg.
|
||||
|
||||
Änderungen am Aktivierungsstatus und am TCP-Port werden während einer laufenden Chat-Verbindung sofort angewendet. Bei einem Portwechsel werden vorhandene DX-Cluster-Verbindungen getrennt und müssen vom Logger neu aufgebaut werden.
|
||||
|
||||
Die Einstellungen werden erst mit **Save Settings** dauerhaft in der `preferences.xml` gespeichert.
|
||||
|
||||
---
|
||||
|
||||
## Einrichtung im Logprogramm
|
||||
|
||||
Das Logprogramm wird als DX-Cluster-Client mit KST4Contest verbunden.
|
||||
|
||||
| Einstellung | KST4Contest und Logger auf demselben Computer | Logger auf einem anderen Computer |
|
||||
|---|---|---|
|
||||
| Host | `127.0.0.1` | IP-Adresse des KST4Contest-Computers |
|
||||
| Port | In KST4Contest konfigurierter TCP-Port | In KST4Contest konfigurierter TCP-Port |
|
||||
| Login | Beliebiges Rufzeichen, falls der Logger eines verlangt | Beliebiges Rufzeichen, falls der Logger eines verlangt |
|
||||
| Passwort | Nicht erforderlich | Nicht erforderlich |
|
||||
|
||||
KST4Contest wertet den vom Logger gesendeten Login nicht zur Authentifizierung aus. Die Verbindung ist für ein lokales oder vertrauenswürdiges Stationsnetz vorgesehen.
|
||||
|
||||
Wenn der Logger auf einem anderen Computer läuft, muss dessen Verbindung durch die lokale Firewall des KST4Contest-Computers zugelassen werden. Der Port sollte nicht ohne weiteren Schutz aus dem Internet erreichbar sein.
|
||||
|
||||
Mehrere DX-Cluster-Clients können gleichzeitig verbunden werden. Ein erzeugter Spot wird an alle aktuell verbundenen Clients gesendet.
|
||||
|
||||
---
|
||||
|
||||
## Verbindung testen
|
||||
|
||||
Die Schaltfläche **Send test spot** erzeugt einen neutralen Testeintrag:
|
||||
|
||||
```text
|
||||
Spotted callsign: DO5AMF
|
||||
Comment: KST4CONTEST TEST
|
||||
Frequency: .300 des konfigurierten Fallback-Bandes
|
||||
```
|
||||
|
||||
Bei einem Fallback-Band von `144` erscheint der Spot daher auf ungefähr `144.300 MHz`.
|
||||
|
||||
Vor dem Test müssen drei Bedingungen erfüllt sein:
|
||||
|
||||
1. KST4Contest ist mit dem ON4KST-Chat verbunden.
|
||||
2. Der lokale DX-Cluster-Server ist aktiviert.
|
||||
3. Der DX-Cluster-Client des Logprogramms ist mit KST4Contest verbunden.
|
||||
|
||||
Fehlt die Client-Verbindung, zeigt KST4Contest eine entsprechende Meldung an. Ein erfolgreich ausgeführter Test bedeutet damit tatsächlich, dass mindestens ein Client den Spot erhalten hat.
|
||||
|
||||
---
|
||||
|
||||
## Inhalt eines erzeugten Spots
|
||||
|
||||
Ein Spot enthält:
|
||||
|
||||
- das konfigurierte Spotter-Rufzeichen,
|
||||
- die normalisierte Frequenz,
|
||||
- das Rufzeichen der erkannten Station,
|
||||
- den Locator,
|
||||
- Flugzeug-Scatter-Informationen, falls vorhanden,
|
||||
- die aktuelle UTC-Zeit.
|
||||
|
||||
Wenn für die Station aktuelle Aircraft-Scatter-Informationen vorliegen, kann KST4Contest diese als zusätzliche AP-Information in den Kommentar des Spots aufnehmen.
|
||||
|
||||
---
|
||||
|
||||
## Wenn kein Spot erscheint
|
||||
|
||||
### Der Testspot kommt nicht im Logger an
|
||||
|
||||
Prüfe:
|
||||
|
||||
- Ist KST4Contest mit dem Chat verbunden?
|
||||
- Ist der lokale DX-Cluster-Server aktiviert?
|
||||
- Verwendet der Logger denselben TCP-Port?
|
||||
- Verwendet der Logger bei lokalem Betrieb `127.0.0.1`?
|
||||
- Blockiert eine Firewall die Verbindung?
|
||||
- Ist im Logger das DX-Cluster-Fenster beziehungsweise die Bandmap aktiviert?
|
||||
|
||||
### Testspot funktioniert, aber reale Spots fehlen
|
||||
|
||||
Dann funktioniert die Verbindung grundsätzlich. Für die betreffende Chat-Situation war wahrscheinlich mindestens eine fachliche Bedingung nicht erfüllt:
|
||||
|
||||
- kein gerichteter Nachrichtenaustausch,
|
||||
- fehlender Locator,
|
||||
- Station außerhalb des maximalen QRB,
|
||||
- Richtung außerhalb des konfigurierten Öffnungswinkels,
|
||||
- keine erkannte Frequenz.
|
||||
|
||||
KST4Contest sendet absichtlich nicht jede gefundene Frequenz an den Logger. Andernfalls würde aus einer Arbeitserleichterung sehr schnell eine lokale Spot-Schleuder.
|
||||
|
||||
### Der Spot erscheint auf dem falschen Band
|
||||
|
||||
Prüfe zuerst das konfigurierte **Fallback band in MHz**. Es wird nur benötigt, wenn im Chat keine vollständige Frequenz angegeben wurde.
|
||||
|
||||
### Der Spot wird vom Logger ausgeblendet
|
||||
|
||||
Verwende ein Spotter-Rufzeichen, das nicht mit dem eigenen Contest-Rufzeichen identisch ist. Abhängig vom Logger können eigene Spots gefiltert oder besonders behandelt werden.
|
||||
|
||||
---
|
||||
|
||||
## Getestete Logprogramme
|
||||
|
||||
- **UCXLog** ✓
|
||||
- **N1MM+** ✓
|
||||
Die Schnittstelle wurde mit folgenden Logprogrammen verwendet:
|
||||
|
||||
Weitere Testergebnisse sind willkommen – bitte per E-Mail an DO5AMF melden.
|
||||
- UCXLog
|
||||
- N1MM+
|
||||
|
||||
Weitere Logger können funktionieren, wenn sie eine normale TCP-Verbindung zu einem DX-Cluster-Server unterstützen.
|
||||
|
||||
@@ -34,7 +34,13 @@ Die Berechnung berücksichtigt keine topografischen Wegberechnungen – das ist
|
||||
|
||||
## Sked-Richtungs-Spots (Integrierter DX-Cluster)
|
||||
|
||||
Ab **v1.23**: Richtungs-Warnungen werden als DX-Cluster-Spots an das Logprogramm weitergeleitet, wenn eine QRG bekannt ist. Details: [DX-Cluster-Server](de-DX-Cluster-Server).
|
||||
Seit Version 1.23 kann KST4Contest erkannte Richtungsgelegenheiten als DX-Cluster-Spots an ein verbundenes Logprogramm weitergeben.
|
||||
|
||||
Aus einer gerichteten Chat-Nachricht wird zunächst die wahrscheinliche Antennenrichtung des Absenders hergeleitet. Liegt die eigene Station innerhalb des konfigurierten Öffnungswinkels und ist eine Frequenz bekannt, erscheint die Station als Spot in der Bandmap des Logprogramms.
|
||||
|
||||
KST4Contest überträgt damit nicht jede gefundene Frequenz, sondern nur Situationen, die für die eigene Station geometrisch plausibel sind.
|
||||
|
||||
Details: [Integrierter DX-Cluster-Server](de-DX-Cluster-Server).
|
||||
|
||||
---
|
||||
|
||||
@@ -149,9 +155,11 @@ Aktivierung: FurtherInfo-Panel der entsprechenden Station.
|
||||
|
||||
---
|
||||
|
||||
## QSO-Sniffer (ab v1.31)
|
||||
## QSO-Monitoring (ab v1.31)
|
||||
|
||||
Der QSO-Sniffer überwacht den Chat auf Nachrichten von einer konfigurierbaren Rufzeichen-Liste und leitet diese automatisch in das **PM-Fenster** weiter. So gehen keine relevanten Nachrichten im allgemeinen Chat-Rauschen unter.
|
||||
Für ausgewählte Rufzeichen kann KST4Contest gerichtete Nachrichten zusätzlich in der PM-Tabelle anzeigen. Dabei werden sowohl Nachrichten berücksichtigt, die das überwachte Rufzeichen sendet, als auch Nachrichten, die an dieses Rufzeichen gerichtet sind.
|
||||
|
||||
Die Nachricht bleibt gleichzeitig in ihrer ursprünglichen Tabelle erhalten und wird im PM-Fenster mit Absender und Empfänger als überwachte Kommunikation gekennzeichnet.
|
||||
|
||||
Konfiguration: [Konfiguration – Sniffer-Einstellungen](de-Konfiguration#sniffer-einstellungen-ab-v131)
|
||||
|
||||
|
||||
+89
-34
@@ -1,10 +1,64 @@
|
||||
# KST4Contest – Wiki
|
||||
# KST4Contest – Handbuch
|
||||
|
||||
> 🇬🇧 [English version](en-Home) | 🇩🇪 Du liest gerade die deutsche Version
|
||||
> [English version](en-Home) | Du liest gerade die deutsche Version
|
||||
|
||||
**KST4Contest** (auch bekannt als *PraktiKST*) ist ein Java-basierter Chat-Client für den [ON4KST-Chat](http://www.on4kst.info/chat/), der speziell für den Contest-Betrieb auf den VHF/UHF/SHF-Bändern (144 MHz und aufwärts) entwickelt wurde.
|
||||
KST4Contest ist ein Desktop-Client für den [ON4KST-Chat](https://www.on4kst.org/chat/login.php), der für den Contest-Betrieb auf den VHF-, UHF- und SHF-Bändern entwickelt wurde. Er verbindet Chat, Stationsauswahl, Sked-Planung, Aircraft-Scatter-Daten und die Anbindung an weitere Programme in einer gemeinsamen Arbeitsoberfläche.
|
||||
|
||||
Entwickelt von **DO5AMF (Marc Fröhlich)**, Operator bei DM5M.
|
||||
Entwickelt wird KST4Contest von **DO5AMF (Marc Fröhlich)**, Operator bei DM5M und (seit Mai 2025) **DN9APW (Philipp Wagner)**. Der Quellcode ist öffentlich auf [GitHub](https://github.com/praktimarc/kst4contest) verfügbar.
|
||||
|
||||
---
|
||||
|
||||
## Wozu braucht man einen eigenen ON4KST-Client?
|
||||
|
||||
Der ON4KST-Chat liefert während eines Contests eine große Menge an Informationen: aktive Stationen, Locator, Frequenzen, Sked-Anfragen und Hinweise auf aktuelle Aktivität. Das eigentliche Problem besteht nicht darin, diese Daten zu sehen. Man muss daraus rechtzeitig die nächste sinnvolle Verbindung ableiten.
|
||||
|
||||
KST4Contest wertet die verfügbaren Informationen aus, ordnet sie und stellt sie in einem contestgerechten Arbeitsablauf dar. Dabei werden unter anderem Antennenrichtung, Entfernung, bekannte Bänder und Frequenzen, bereits gearbeitete Stationen, Chat-Aktivität und Aircraft-Scatter-Zeiten berücksichtigt.
|
||||
|
||||
Das Programm entscheidet jedoch nicht, welches QSO tatsächlich möglich ist. Der Prioritätsscore, die AP-Timeline und die verschiedenen Hervorhebungen sind Entscheidungshilfen. Die endgültige Beurteilung bleibt beim Operator – schon deshalb, weil auch ein recht überzeugender Computerbildschirm noch keine Funkverbindung herstellt.
|
||||
|
||||
---
|
||||
|
||||
## Was KST4Contest im Contest unterstützt
|
||||
|
||||
- **Chat beobachten und einordnen:** KST4Contest kann zwei ON4KST-Chat-Kategorien gleichzeitig darstellen. Nachrichten, Frequenzangaben und bekannte Bandaktivitäten werden den jeweiligen Stationen zugeordnet.
|
||||
|
||||
- **Relevante Stationen herausfiltern:** Richtungs-, Entfernungs-, Worked- und NOT-QRV-Filter reduzieren die Benutzerliste auf die Stationen, die für den aktuellen Betriebszustand tatsächlich interessant sind.
|
||||
|
||||
- **Kandidaten priorisieren:** Das Score-System bewertet aktive Chatmember anhand mehrerer bekannter Kriterien. Die derzeit relevantesten Kandidaten erscheinen zusätzlich in einer eigenen Prioritätsliste.
|
||||
|
||||
- **Skeds vorbereiten und im Blick behalten:** Sked-Erinnerungen, automatische Vorwarnungen und die AP-Timeline helfen dabei, vereinbarte Verbindungen nicht zwischen Chat, Log und laufendem CQ-Betrieb zu verlieren.
|
||||
|
||||
- **Aircraft Scatter einbeziehen:** Über die AirScout-Schnittstelle werden geeignete Flugzeuge und erwartete Reflexionszeiten in die Stationsbewertung und Zeitplanung übernommen.
|
||||
|
||||
- **Log und Funkstation anbinden:** KST4Contest synchronisiert gearbeitete Stationen und Frequenzinformationen mit unterstützten Logprogrammen. Zusätzlich stehen Schnittstellen zu Win-Test, PSTRotator und ein integrierter DX-Cluster-Server zur Verfügung.
|
||||
|
||||
- **Stationen und Funkwege darstellen:** Die Stationskarte zeigt aktive Chatmember, Locator-Felder, Antennenrichtungen und den Weg zur ausgewählten Station. Für ausgewählte Verbindungen kann außerdem ein Geländeprofil berechnet werden.
|
||||
|
||||
Die Funktionen greifen ineinander. Eine erkannte Frequenz kann beispielsweise einem aktiven Band zugeordnet werden, der Worked-Status stammt aus dem Log, Aircraft-Scatter-Daten ergänzen die zeitliche Bewertung und der daraus berechnete Score beeinflusst die Prioritätsliste. Fehlende oder veraltete Eingangsdaten können deshalb auch das Ergebnis beeinflussen.
|
||||
|
||||
---
|
||||
|
||||
## Voraussetzungen
|
||||
|
||||
Für die Anmeldung ist ein registrierter ON4KST-Account erforderlich. Registrierung und Login sind über die [offizielle ON4KST-Anmeldeseite](https://www.on4kst.org/chat/login.php) erreichbar.
|
||||
|
||||
Die offizielle Sprache im ON4KST-Chat ist Englisch. Das gilt auch für Nachrichten an Stationen aus dem eigenen Land. Übliche Amateurfunk-Abkürzungen wie `pse`, `agn`, `qrg`, `dir`, `rrr`, `tnx` oder `73` sind dabei normal und meistens erheblich schneller als ausformulierte Prosa.
|
||||
|
||||
Download, unterstützte Betriebssysteme und Installationswege sind im Kapitel [Installation](de-Installation) beschrieben.
|
||||
|
||||
---
|
||||
|
||||
## Versionsstand dieses Handbuchs
|
||||
|
||||
Dieses Handbuch beschreibt die stabile Version **v1.41.1**.
|
||||
|
||||
Funktionen oder Änderungen, die nur im aktuellen Entwicklungsstand enthalten sind, werden ausdrücklich als **Nightly / v1.42** gekennzeichnet. Fehlt eine solche Kennzeichnung, bezieht sich die Beschreibung auf die stabile Version.
|
||||
|
||||
- [Aktuelle stabile Version herunterladen](https://github.com/praktimarc/kst4contest/releases/latest)
|
||||
- [Nightly-Builds und automatisierte Builds](https://github.com/praktimarc/kst4contest/actions)
|
||||
- [Versionsgeschichte](de-Changelog)
|
||||
|
||||
Für einen Contest ist grundsätzlich die stabile Version zu empfehlen. Nightly-Builds enthalten neuere Korrekturen und Funktionen, können sich aber zwischen zwei Builds verändern. Sie sind sinnvoll, wenn eine bestimmte Änderung getestet werden soll – weniger sinnvoll ist der erste Versuch zehn Minuten vor Contestbeginn.
|
||||
|
||||
---
|
||||
|
||||
@@ -12,51 +66,52 @@ Entwickelt von **DO5AMF (Marc Fröhlich)**, Operator bei DM5M.
|
||||
|
||||
| Seite | Inhalt |
|
||||
|---|---|
|
||||
| [Installation](de-Installation) | Download, Java-Voraussetzungen, Update |
|
||||
| [Konfiguration](de-Konfiguration) | Alle Einstellungen im Detail |
|
||||
| [Log-Synchronisation](de-Log-Synchronisation) | UCXLog, N1MM+, QARTest, DXLog.net, WinTest |
|
||||
| [AirScout-Integration](de-AirScout-Integration) | Flugzeug-Scatter-Erkennung |
|
||||
| [DX-Cluster-Server](de-DX-Cluster-Server) | Integrierter DX-Cluster für das Log-Programm |
|
||||
| [Funktionen](de-Funktionen) | Alle Features im Überblick |
|
||||
| [Makros und Variablen](de-Makros-und-Variablen) | Text-Snippets, Shortcuts, Variablen |
|
||||
| [Benutzeroberfläche](de-Benutzeroberflaeche) | UI-Erklärung und Bedienung |
|
||||
| [Changelog](de-Changelog) | Versionsgeschichte |
|
||||
| [Installation](de-Installation) | ON4KST-Account, Download, Installation und Updates |
|
||||
| [Konfiguration](de-Konfiguration) | Login, Station, Bänder, Benutzeroberfläche und externe Schnittstellen |
|
||||
| [Log-Synchronisation](de-Log-Synchronisation) | Simplelogfile, UCXLog, N1MM+, QARTest, DXLog.net und Win-Test |
|
||||
| [AirScout-Integration](de-AirScout-Integration) | Verbindung zu AirScout und Auswertung von Aircraft-Scatter-Zeiten |
|
||||
| [DX-Cluster-Server](de-DX-Cluster-Server) | Übergabe erkannter Möglichkeiten an das Logprogramm |
|
||||
| [Funktionen](de-Funktionen) | Arbeitsweise, Herleitung und Grenzen der einzelnen Funktionen |
|
||||
| [Makros und Variablen](de-Makros-und-Variablen) | Wiederverwendbare Texte, Shortcuts und automatisch ersetzte Werte |
|
||||
| [Benutzeroberfläche](de-Benutzeroberflaeche) | Aufbau der Oberfläche und Bedienung im Contest |
|
||||
| [Changelog](de-Changelog) | Releases, Nightly-Änderungen und behobene Fehler |
|
||||
|
||||
---
|
||||
|
||||
## Was ist KST4Contest?
|
||||
## Kontakt und Support
|
||||
|
||||
Der ON4KST-Chat ist der De-facto-Standard für Skeds auf den 144-MHz-und-höher-Bändern. KST4Contest erweitert die Chat-Nutzung um contest-spezifische Funktionen:
|
||||
- **Download:** [Aktuelle stabile Version](https://github.com/praktimarc/kst4contest/releases/latest)
|
||||
- **Quellcode:** [praktimarc/kst4contest](https://github.com/praktimarc/kst4contest)
|
||||
- **Fehler und Funktionswünsche:** [GitHub Issues](https://github.com/praktimarc/kst4contest/issues)
|
||||
- **E-Mail:** praktimarc+kst4contest@gmail.com
|
||||
Bitte nur für Themen verwenden, die KST4Contest betreffen.
|
||||
|
||||
- **Worked-Markierung**: Bereits gearbeitete Stationen werden farblich markiert, direkt aus dem Logprogramm via UDP synchronisiert.
|
||||
- **Sked-Richtungs-Erkennung**: Wenn eine Station eine andere aus deiner Richtung anruft, wird sie grün und fett hervorgehoben.
|
||||
- **QRG-Erkennung**: KST4Contest liest Frequenzen automatisch aus dem Chat-Verkehr und zeigt sie in der Benutzerliste an.
|
||||
- **AirScout-Interface**: Anzeige reflektierbarer Flugzeuge direkt in der Benutzerliste.
|
||||
- **Integrierter DX-Cluster-Server**: Spots werden direkt an das Logprogramm gesendet.
|
||||
- **Dark Mode** (ab v1.26): Schont die Augen in der Nacht.
|
||||
- **Multi-Channel-Login** (ab v1.26): Gleichzeitig in zwei Chat-Kategorien einloggen.
|
||||
### Einen Fehler melden
|
||||
|
||||
---
|
||||
Ein Fehler lässt sich wesentlich schneller nachvollziehen, wenn die Meldung mindestens folgende Angaben enthält:
|
||||
|
||||
## Kontakt & Support
|
||||
1. verwendete KST4Contest-Version,
|
||||
2. Betriebssystem und Installationsart,
|
||||
3. genaue Schritte bis zum Fehler,
|
||||
4. erwartetes und tatsächlich beobachtetes Verhalten,
|
||||
5. gegebenenfalls einen Screenshot,
|
||||
6. die Fehler-Logdatei.
|
||||
|
||||
- **E-Mail**: praktimarc+kst4contest@gmail.com *(nur für kst4contest-Themen)*
|
||||
- **GitHub**: https://github.com/praktimarc/kst4contest
|
||||
- **Download**: https://github.com/praktimarc/kst4contest/releases/latest
|
||||
Die Fehler-Logdatei wird hier gespeichert:
|
||||
|
||||
### Fehler melden (Issue erstellen)
|
||||
|
||||
Beim Melden eines Fehlers bitte **immer die Logdatei anhängen**. KST4Contest schreibt automatisch eine Fehler-Logdatei (nur Fehlermeldungen, keine persönlichen Daten):
|
||||
|
||||
| Betriebssystem | Pfad zur Logdatei |
|
||||
| Betriebssystem | Pfad |
|
||||
|---|---|
|
||||
| Linux / macOS | `~/.praktiKST/kst4contest-errors.log` |
|
||||
| Windows | `C:\Users\<Benutzername>\.praktiKST\kst4contest-errors.log` |
|
||||
|
||||
Beim Erstellen eines Issues auf GitHub steht eine Vorlage bereit, die alle wichtigen Felder abfragt.
|
||||
Vor dem Hochladen sollte die Datei kurz geprüft werden. Ein Fehlerprotokoll enthält überwiegend technische Informationen, kann abhängig vom Fehler aber beispielsweise lokale Dateipfade oder weitere Kontextdaten enthalten.
|
||||
|
||||
In Datei- und Verzeichnisnamen wird teilweise noch der technische Name `praktiKST` verwendet. Gemeint ist dasselbe Programm.
|
||||
|
||||
---
|
||||
|
||||
## Danksagungen
|
||||
|
||||
Besonderer Dank gilt: Gianluca Costantino (IU3OAR), Alessandro Murador (IZ3VTH), Reczetár István (HA1FV), OM0AAO (Viliam Petrik, DX-Cluster-Idee), DC9DJ (Konrad Neitzel, Projektstruktur), DO5ALF (Andreas, Webmaster funkerportal.de), PE0WGA (Franz van Velzen, Tester) sowie allen weiteren Testern und Ideengebern.
|
||||
KST4Contest wurde durch Rückmeldungen aus dem praktischen Contest-Betrieb wesentlich verbessert.
|
||||
|
||||
Besonderer Dank gilt Gianluca Costantino (IU3OAR), Alessandro Murador (IZ3VTH), Reczetár István (HA1FV), Viliam Petrik (OM0AAO, Idee zum DX-Cluster), Konrad Neitzel (DC9DJ, Projektstruktur), Andreas (DO5ALF, Webmaster von funkerportal.de), Franz van Velzen (PE0WGA, Tester), DN9APW als neuem Entwickler im Team und Master über die CI/CD pipelines sowie allen weiteren Testern und Ideengebern.
|
||||
+330
-127
@@ -2,219 +2,422 @@
|
||||
|
||||
> 🇬🇧 [English version](en-Installation) | 🇩🇪 Du liest gerade die deutsche Version
|
||||
|
||||
## Voraussetzungen
|
||||
KST4Contest wird für Windows, Linux und macOS als fertiges Programmpaket bereitgestellt. Für die offiziellen Release-Pakete muss Java nicht separat installiert werden: Die benötigte Java-Laufzeitumgebung ist bereits enthalten.
|
||||
|
||||
Es wird eine Mindestauflösung von 1200px mal 720px empfohlen
|
||||
Benötigt werden damit im Wesentlichen ein unterstütztes Betriebssystem, eine Internetverbindung und ein ON4KST-Account. Das klingt zunächst überschaubar – und ist es normalerweise auch.
|
||||
|
||||
## Voraussetzungen
|
||||
|
||||
### ON4KST-Account
|
||||
|
||||
Um den Chat zu nutzen, ist ein registrierter Account beim ON4KST-Chat-Dienst erforderlich:
|
||||
KST4Contest ist ein eigenständiger Client für den ON4KST-Chat, ersetzt aber nicht den dazugehörigen Benutzeraccount.
|
||||
|
||||
- Registrierung unter: http://www.on4kst.info/chat/register.php
|
||||
Falls noch kein Account vorhanden ist, kann er über die offizielle ON4KST-Seite angelegt werden:
|
||||
|
||||
### Verhaltensregeln im Chat
|
||||
- [ON4KST-Anmeldung und Registrierung](https://www.on4kst.org/chat/login.php)
|
||||
|
||||
Die offizielle Sprache im ON4KST-Chat ist **Englisch**. Auch bei Kommunikation mit Stationen aus dem eigenen Land bitte Englisch verwenden. Übliche HAM-Abkürzungen (agn, dir, pse, rrr, tnx, 73 …) sind gang und gäbe.
|
||||
ON4KST gibt Englisch als gemeinsame Sprache im Chat vor. Das gilt auch dann, wenn beide beteiligten Stationen dieselbe Muttersprache sprechen. Im Contestbetrieb werden häufig die üblichen Amateurfunk-Abkürzungen wie `pse`, `agn`, `qrg`, `dir`, `rrr`, `tnx` oder `73` verwendet.
|
||||
|
||||
### Persönliche Nachrichten
|
||||
Die eigentliche Bedienung des Chats und das Senden persönlicher Nachrichten werden im Kapitel zur Benutzeroberfläche beschrieben. Für die Installation ist zunächst nur wichtig, dass der Account funktioniert.
|
||||
|
||||
Um eine Privatnachricht an eine andere Station zu senden, immer folgendes Format verwenden:
|
||||
### Bildschirmgröße
|
||||
|
||||
```
|
||||
/CQ RUFZEICHEN Nachrichtentext
|
||||
```
|
||||
Eine nutzbare Bildschirmfläche von ungefähr **1200 × 720 Pixeln** oder mehr wird empfohlen.
|
||||
|
||||
Beispiel: `/CQ DL5ASG pse sked 144.205?`
|
||||
KST4Contest passt das Hauptfenster automatisch an die verfügbare Fläche des primären Bildschirms an. Das Programm kann deshalb auch auf kleineren Bildschirmen gestartet werden. Weniger Platz bleibt allerdings weniger Platz: In diesem Fall können nicht alle Tabellen, Filter und Zusatzinformationen gleichzeitig in sinnvoller Größe dargestellt werden.
|
||||
|
||||
Bei starkem Chat-Verkehr (5–6 Nachrichten pro Sekunde im Contest) gehen öffentliche Nachrichten, die an ein bestimmtes Rufzeichen gerichtet sind, leicht unter. KST4Contest fängt solche Nachrichten aber auch dann ab, wenn sie fälschlicherweise öffentlich gepostet werden (siehe [Funktionen – PM-Abfang](Funktionen#catching-personal-messages)).
|
||||
### Java
|
||||
|
||||
Für die fertigen Pakete aus den [GitHub Releases](https://github.com/praktimarc/kst4contest/releases/latest) ist keine separate Java-Installation erforderlich. Die benötigte Laufzeitumgebung wird zusammen mit KST4Contest ausgeliefert.
|
||||
|
||||
Eine Java-Entwicklungsumgebung wird nur benötigt, wenn KST4Contest selbst aus dem Quellcode gebaut werden soll. Bei den AUR-Paketen `kst4contest` und `kst4contest-git` werden Java 21 und Maven als Build-Abhängigkeiten durch den Paketmanager berücksichtigt.
|
||||
|
||||
---
|
||||
|
||||
## Stable, Beta oder Nightly?
|
||||
|
||||
KST4Contest wird in drei Entwicklungsständen bereitgestellt:
|
||||
|
||||
| Kanal | Geeignet für | Einordnung |
|
||||
|---|---|---|
|
||||
| **Stable** | Normaler Contestbetrieb | Veröffentlichte und für den regulären Einsatz vorgesehene Version |
|
||||
| **Beta** | Gezielte Tests vor einem Stable-Release | Vorabversion mit weitgehend festgelegtem Funktionsumfang |
|
||||
| **Nightly** | Frühe Tests neuer Funktionen | Aktueller Entwicklungsstand aus dem `main`-Branch |
|
||||
|
||||
Für den normalen Einsatz wird die **Stable-Version** empfohlen.
|
||||
|
||||
Beta- und Nightly-Versionen können Funktionen enthalten, die im Stable-Release noch nicht verfügbar sind. Sie können aber ebenso unfertige Bedienabläufe, geänderte Einstellungen oder neue Fehler enthalten. Das ist kein ungewöhnlicher Defekt im Veröffentlichungsverfahren, sondern der Zweck eines Entwicklungskanals.
|
||||
|
||||
Wenn in diesem Manual eine Funktion ausdrücklich mit **Nightly** gekennzeichnet ist, gehört sie noch nicht zwingend zum aktuellen Stable-Release.
|
||||
|
||||
---
|
||||
|
||||
## Download
|
||||
|
||||
### Windows
|
||||
Die aktuelle Stable-Version ist hier verfügbar:
|
||||
|
||||
Die aktuelle Version kann als ZIP-Datei heruntergeladen werden:
|
||||
- [Aktuelles KST4Contest-Release](https://github.com/praktimarc/kst4contest/releases/latest)
|
||||
- [Alle veröffentlichten Versionen](https://github.com/praktimarc/kst4contest/releases)
|
||||
|
||||
**https://github.com/praktimarc/kst4contest/releases/latest**
|
||||
Die Release-Seite enthält die Programmpakete sowie die deutschen und englischen PDF-Handbücher.
|
||||
|
||||
Der Dateiname hat das Format `praktiKST-v<Versionsnummer>-windows-x64.zip `.
|
||||
### Welches Paket wird benötigt?
|
||||
|
||||
### Linux
|
||||
|
||||
Mehrere Paketformate stehen auf der Releases-Seite zur Verfügung:
|
||||
|
||||
**https://github.com/praktimarc/kst4contest/releases/latest**
|
||||
|
||||
| Format | Dateiname | Geeignet für |
|
||||
| Betriebssystem | Paket | Typischer Dateiname |
|
||||
|---|---|---|
|
||||
| AppImage | `KST4Contest-v<Version>-linux-x86_64.AppImage` | Alle Distributionen |
|
||||
| Debian-Paket | `KST4Contest-v<Version>-debian-amd64.deb` | Debian, Ubuntu, Linux Mint, … |
|
||||
| RPM-Paket | `KST4Contest-v<Version>-fedora-x86_64.rpm` | Fedora, RHEL, openSUSE, … |
|
||||
| Arch-Paket | `KST4Contest-v<Version>-archlinux-x86_64.pkg.tar.zst` | Arch Linux, Manjaro, … |
|
||||
| Flatpak | `de.x08.KST4Contest.flatpakref` | Alle Distributionen mit Flatpak |
|
||||
|
||||
> **Empfehlung für Linux:** Die Flatpak-Installation ist der einfachste Weg, immer aktuell zu bleiben – `flatpak update` erledigt alle zukünftigen Updates automatisch. Das Repository ist GPG-signiert.
|
||||
|
||||
### macOS
|
||||
|
||||
> ⚠️ **Best-Effort-Support:** macOS-Builds werden als zusätzliche Option bereitgestellt, sind aber **nicht umfassend getestet**. Wir bauen und veröffentlichen macOS-Binaries mit jedem Release, können allerdings nicht alle Szenarien unter macOS testen. Bei Problemen freuen wir uns über eine Rückmeldung – wir versuchen unser Bestes, können aber nicht den gleichen Support-Umfang wie für Windows und Linux garantieren.
|
||||
|
||||
Die aktuelle Version kann als DMG-Disk-Image heruntergeladen werden (für Apple-Silicon- und Intel-Macs verfügbar):
|
||||
|
||||
**https://github.com/praktimarc/kst4contest/releases/latest**
|
||||
|
||||
Der Dateiname hat das Format `KST4Contest-v<Versionsnummer>-macos-<Architektur>.dmg`, wobei `<Architektur>` entweder `arm64` (Apple Silicon) oder `x86_64` (Intel) ist.
|
||||
| Windows x64 | ZIP-Paket | `praktiKST-v<Version>-windows-x64.zip` |
|
||||
| Linux x86_64 | AppImage | `KST4Contest-v<Version>-linux-x86_64.AppImage` |
|
||||
| Debian/Ubuntu amd64 | DEB-Paket | `KST4Contest-v<Version>-debian-amd64.deb` |
|
||||
| Fedora/RPM x86_64 | RPM-Paket | `KST4Contest-v<Version>-fedora-x86_64.rpm` |
|
||||
| Arch Linux x86_64 | Arch-Paket | `KST4Contest-v<Version>-archlinux-x86_64.pkg.tar.zst` |
|
||||
| Linux mit Flatpak | Flatpak-Referenz | `de.x08.KST4Contest.flatpakref` |
|
||||
| macOS Apple Silicon | DMG für ARM64 | `KST4Contest-v<Version>-macos-arm64.dmg` |
|
||||
| macOS Intel | DMG für x86_64 | `KST4Contest-v<Version>-macos-x86_64.dmg` |
|
||||
|
||||
Lade Programmpakete nur aus den offiziellen GitHub Releases, dem KST4Contest-Flatpak-Repository oder den verlinkten AUR-Paketen herunter. Dateien aus anderen Quellen können anders gebaut, veraltet oder verändert worden sein.
|
||||
|
||||
---
|
||||
|
||||
## Installation
|
||||
## Installation unter Windows
|
||||
|
||||
### Windows
|
||||
KST4Contest wird unter Windows als ZIP-Paket bereitgestellt. Ein klassischer Installer ist nicht erforderlich.
|
||||
|
||||
1. ZIP-Datei herunterladen.
|
||||
2. ZIP-Datei in einen gewünschten Ordner entpacken.
|
||||
3. `praktiKST.exe` ausführen.
|
||||
1. Lade `praktiKST-v<Version>-windows-x64.zip` aus dem aktuellen Release herunter.
|
||||
2. Entpacke die ZIP-Datei vollständig in einen eigenen Ordner.
|
||||
3. Öffne den entpackten Ordner.
|
||||
4. Starte `praktiKST.exe`.
|
||||
|
||||
Die Einstellungen werden unter `%USERPROFILE%\.praktikst\preferences.xml` gespeichert.
|
||||
Starte das Programm nicht direkt aus der noch komprimierten ZIP-Datei. KST4Contest besteht aus mehreren Dateien und einer mitgelieferten Laufzeitumgebung. Windows kann diese Struktur nur zuverlässig verwenden, wenn das Archiv vorher vollständig entpackt wurde.
|
||||
|
||||
### Linux
|
||||
Die Programmeinstellungen befinden sich nicht im entpackten Programmordner, sondern im Benutzerverzeichnis:
|
||||
|
||||
Die Einstellungen werden immer unter `~/.praktikst/preferences.xml` gespeichert.
|
||||
```text
|
||||
%USERPROFILE%\.praktiKST\preferences.xml
|
||||
```
|
||||
|
||||
#### AppImage
|
||||
Dadurch können neue Programmversionen in einen anderen Ordner entpackt werden, ohne dass die vorhandenen Einstellungen verloren gehen.
|
||||
|
||||
1. AppImage herunterladen.
|
||||
2. Ausführbar machen: `chmod +x KST4Contest-v<Version>-linux-x86_64.AppImage`
|
||||
3. Starten.
|
||||
---
|
||||
|
||||
#### Debian / Ubuntu
|
||||
## Installation unter Linux
|
||||
|
||||
Unter Linux stehen mehrere Paketformate zur Verfügung. Welches davon sinnvoll ist, hängt weniger von KST4Contest als von der verwendeten Distribution und der gewünschten Update-Methode ab.
|
||||
|
||||
| Installationsart | Sinnvoll, wenn … |
|
||||
|---|---|
|
||||
| **Flatpak** | Updates zentral verwaltet werden sollen und Flatpak bereits verwendet wird |
|
||||
| **AppImage** | KST4Contest ohne Installation als einzelne portable Datei gestartet werden soll |
|
||||
| **DEB/RPM** | die Paketverwaltung der Distribution verwendet werden soll |
|
||||
| **Arch-Paket/AUR** | Arch Linux, Manjaro oder EndeavourOS eingesetzt wird |
|
||||
|
||||
### Flatpak
|
||||
|
||||
Flatpak ist für die meisten Linux-Anwender der einfachste Weg, KST4Contest installiert und aktualisierbar zu halten. Das KST4Contest-Repository ist GPG-signiert und enthält die Kanäle Stable, Beta und Nightly.
|
||||
|
||||
#### Stable über die Release-Datei installieren
|
||||
|
||||
Lade `de.x08.KST4Contest.flatpakref` aus dem aktuellen Release herunter und öffne die Datei mit der Softwareverwaltung der verwendeten Desktop-Umgebung.
|
||||
|
||||
Alternativ kann sie im Terminal installiert werden:
|
||||
|
||||
```bash
|
||||
flatpak install ./de.x08.KST4Contest.flatpakref
|
||||
```
|
||||
|
||||
#### KST4Contest-Repository hinzufügen
|
||||
|
||||
Das Repository muss nur einmal hinzugefügt werden:
|
||||
|
||||
```bash
|
||||
flatpak remote-add --if-not-exists kst4contest \
|
||||
https://praktimarc.github.io/kst4contest/kst4contest.flatpakrepo
|
||||
```
|
||||
|
||||
Anschließend kann die Stable-Version installiert werden:
|
||||
|
||||
```bash
|
||||
flatpak install kst4contest de.x08.KST4Contest//stable
|
||||
```
|
||||
|
||||
#### Beta installieren
|
||||
|
||||
```bash
|
||||
flatpak install kst4contest de.x08.KST4Contest//beta
|
||||
```
|
||||
|
||||
#### Nightly installieren
|
||||
|
||||
```bash
|
||||
flatpak install kst4contest de.x08.KST4Contest//nightly
|
||||
```
|
||||
|
||||
KST4Contest verwendet für alle drei Kanäle dieselbe App-ID. Eine parallele Installation mehrerer KST4Contest-Kanäle ist deshalb nicht vorgesehen.
|
||||
|
||||
Zum Wechsel von Stable auf Nightly beispielsweise:
|
||||
|
||||
```bash
|
||||
flatpak uninstall de.x08.KST4Contest//stable
|
||||
flatpak install kst4contest de.x08.KST4Contest//nightly
|
||||
```
|
||||
|
||||
Die persönlichen Einstellungen im Verzeichnis `~/.praktiKST` werden dabei nicht automatisch gelöscht.
|
||||
|
||||
Installierte Flatpak-Anwendungen können mit folgendem Befehl aktualisiert werden:
|
||||
|
||||
```bash
|
||||
flatpak update de.x08.KST4Contest
|
||||
```
|
||||
|
||||
Je nach Desktop-Umgebung kann die grafische Softwareverwaltung verfügbare Flatpak-Updates ebenfalls anzeigen oder automatisch installieren. Der Befehl `flatpak update` selbst führt das Update aus; er verspricht nicht, irgendwann von allein vorbeizukommen.
|
||||
|
||||
### AppImage
|
||||
|
||||
Das AppImage benötigt keine klassische Installation.
|
||||
|
||||
1. Lade `KST4Contest-v<Version>-linux-x86_64.AppImage` herunter.
|
||||
2. Öffne ein Terminal im Download-Verzeichnis.
|
||||
3. Mache die Datei ausführbar:
|
||||
|
||||
```bash
|
||||
chmod +x KST4Contest-v<Version>-linux-x86_64.AppImage
|
||||
```
|
||||
|
||||
4. Starte KST4Contest:
|
||||
|
||||
```bash
|
||||
./KST4Contest-v<Version>-linux-x86_64.AppImage
|
||||
```
|
||||
|
||||
Das AppImage kann anschließend an einen anderen Ort verschoben werden, beispielsweise nach `~/Applications`.
|
||||
|
||||
### Debian und Ubuntu
|
||||
|
||||
Installiere das DEB-Paket mit:
|
||||
|
||||
```bash
|
||||
sudo apt install ./KST4Contest-v<Version>-debian-amd64.deb
|
||||
```
|
||||
|
||||
Oder die `.deb`-Datei im Dateimanager doppelklicken.
|
||||
Alternativ kann die Datei in einer grafischen Paketverwaltung geöffnet werden.
|
||||
|
||||
#### Fedora / RHEL
|
||||
### Fedora und kompatible RPM-Systeme
|
||||
|
||||
Installiere das RPM-Paket mit:
|
||||
|
||||
```bash
|
||||
sudo dnf install ./KST4Contest-v<Version>-fedora-x86_64.rpm
|
||||
```
|
||||
|
||||
#### Arch Linux
|
||||
### Arch Linux: fertiges Release-Paket
|
||||
|
||||
Das aus dem GitHub Release heruntergeladene Arch-Paket kann direkt installiert werden:
|
||||
|
||||
```bash
|
||||
sudo pacman -U KST4Contest-v<Version>-archlinux-x86_64.pkg.tar.zst
|
||||
```
|
||||
|
||||
#### Flatpak
|
||||
### Arch Linux: Installation über den AUR
|
||||
|
||||
Im AUR stehen drei Varianten bereit:
|
||||
|
||||
| Paket | Inhalt |
|
||||
|---|---|
|
||||
| [`kst4contest-bin`](https://aur.archlinux.org/packages/kst4contest-bin) | Vorgefertigtes Paket des aktuellen Stable-Releases |
|
||||
| [`kst4contest`](https://aur.archlinux.org/packages/kst4contest) | Stable-Release, das lokal aus dem Quellcode gebaut wird |
|
||||
| [`kst4contest-git`](https://aur.archlinux.org/packages/kst4contest-git) | Aktueller Entwicklungsstand aus dem `main`-Branch |
|
||||
|
||||
Für die meisten Anwender ist `kst4contest-bin` die naheliegende Variante:
|
||||
|
||||
Die Datei `de.x08.KST4Contest.flatpakref` aus dem [aktuellen Release](https://github.com/praktimarc/kst4contest/releases/latest) herunterladen und öffnen, oder:
|
||||
```bash
|
||||
flatpak install de.x08.KST4Contest.flatpakref
|
||||
yay -S kst4contest-bin
|
||||
```
|
||||
|
||||
Oder den Remote manuell hinzufügen (empfohlen für Nightly/Beta-Zugriff):
|
||||
Stable aus dem Quellcode bauen:
|
||||
|
||||
```bash
|
||||
flatpak remote-add --if-not-exists kst4contest https://praktimarc.github.io/kst4contest/kst4contest.flatpakrepo
|
||||
flatpak install kst4contest de.x08.KST4Contest
|
||||
yay -S kst4contest
|
||||
```
|
||||
|
||||
#### Flatpak-Kanäle (nightly / beta / stable)
|
||||
Aktuellen Entwicklungsstand bauen:
|
||||
|
||||
Das Flatpak-Repository enthält drei Kanäle, die alle im selben Remote liegen:
|
||||
```bash
|
||||
yay -S kst4contest-git
|
||||
```
|
||||
|
||||
| Kanal | Inhalt | Wird gebaut bei |
|
||||
Die drei Pakete stellen dieselbe Anwendung bereit und sind deshalb als gegenseitige Konflikte definiert. Installiere nur eine Variante gleichzeitig.
|
||||
|
||||
AUR-Updates werden berücksichtigt, wenn der verwendete AUR-Helper nach Paketaktualisierungen sucht, beispielsweise mit:
|
||||
|
||||
```bash
|
||||
yay -Syu
|
||||
```
|
||||
|
||||
Sie erfolgen nicht allein deshalb automatisch, weil das Paket aus dem AUR stammt.
|
||||
|
||||
---
|
||||
|
||||
## Installation unter macOS
|
||||
|
||||
> **Best-Effort-Support:** Die macOS-Pakete werden zusammen mit den übrigen Releases gebaut, aber nicht in demselben Umfang getestet wie die Windows- und Linux-Versionen. Rückmeldungen sind willkommen; eine vollständig geprüfte Unterstützung aller macOS-Versionen und Hardwarevarianten kann derzeit jedoch nicht zugesagt werden.
|
||||
|
||||
Für Apple-Silicon-Macs wird das Paket mit `arm64` benötigt. Für Intel-Macs ist das Paket mit `x86_64` vorgesehen.
|
||||
|
||||
1. Lade die passende DMG-Datei herunter.
|
||||
2. Öffne die DMG-Datei.
|
||||
3. Ziehe `KST4Contest.app` in den Ordner **Programme**.
|
||||
4. Starte KST4Contest aus dem Programme-Ordner oder über das Launchpad.
|
||||
|
||||
Die Anwendung ist derzeit nicht von Apple notarisiert. macOS kann den ersten Start deshalb blockieren.
|
||||
|
||||
Falls die Anwendung aus dem offiziellen GitHub Release stammt:
|
||||
|
||||
1. Öffne den Programme-Ordner im Finder.
|
||||
2. Klicke mit der rechten Maustaste oder mit gedrückter Ctrl-Taste auf `KST4Contest.app`.
|
||||
3. Wähle **Öffnen**.
|
||||
4. Bestätige den Start im angezeigten Dialog.
|
||||
|
||||
Alternativ kann macOS unter **Systemeinstellungen → Datenschutz & Sicherheit** die Schaltfläche **Trotzdem öffnen** anbieten.
|
||||
|
||||
---
|
||||
|
||||
## Wo werden die Einstellungen gespeichert?
|
||||
|
||||
KST4Contest speichert seine Einstellungen und weitere lokale Arbeitsdateien im Benutzerverzeichnis. Der Programmordner und das Datenverzeichnis sind voneinander getrennt.
|
||||
|
||||
| Betriebssystem | Datenverzeichnis | Einstellungsdatei |
|
||||
|---|---|---|
|
||||
| `stable` | Aktuelle stabile Version | Jedem normalen Release-Tag |
|
||||
| `beta` | Vorab-Version | Tags mit `beta-` Präfix |
|
||||
| `nightly` | Entwicklungsstand (main-Branch) | Täglich / jedem Push auf main |
|
||||
| Windows | `%USERPROFILE%\.praktiKST\` | `%USERPROFILE%\.praktiKST\preferences.xml` |
|
||||
| Linux | `~/.praktiKST/` | `~/.praktiKST/preferences.xml` |
|
||||
| macOS | `~/.praktiKST/` | `~/.praktiKST/preferences.xml` |
|
||||
|
||||
Remote einmalig hinzufügen, dann den gewünschten Kanal installieren:
|
||||
Beachte die Schreibweise `.praktiKST` mit großem `KST`. Unter Linux und macOS wird zwischen Groß- und Kleinschreibung unterschieden. `.praktikst` wäre dort schlicht ein anderes Verzeichnis.
|
||||
|
||||
Ein Programmupdate entfernt dieses Verzeichnis nicht. Trotzdem ist es sinnvoll, vor größeren Versionswechseln oder umfangreichen Änderungen an der Konfiguration eine Sicherung davon anzulegen.
|
||||
|
||||
---
|
||||
|
||||
## Updates
|
||||
|
||||
KST4Contest prüft beim Start, ob ein neueres Stable-Release verfügbar ist. Wenn eine neuere Version gefunden wird, erscheint ein Informationsfenster mit:
|
||||
|
||||
- der installierten Version,
|
||||
- der aktuellen Stable-Version,
|
||||
- einer kurzen Übersicht wesentlicher Änderungen,
|
||||
- dem Changelog,
|
||||
- bekannten Problemen,
|
||||
- einem Link zur passenden GitHub-Release-Seite.
|
||||
|
||||

|
||||
|
||||
Der Update-Checker installiert nichts selbst. Er informiert über die neue Version und öffnet die plattformneutrale Release-Seite. Dort muss das passende Paket für Windows, Linux oder macOS ausgewählt werden.
|
||||
|
||||
### Windows aktualisieren
|
||||
|
||||
1. Beende KST4Contest.
|
||||
2. Lade das neue Windows-ZIP herunter.
|
||||
3. Entpacke es in einen neuen oder leeren Ordner.
|
||||
4. Starte die neue Version.
|
||||
5. Prüfe, ob die bisherigen Einstellungen geladen wurden.
|
||||
6. Entferne den alten Programmordner erst danach.
|
||||
|
||||
Die Einstellungen bleiben erhalten, weil sie unter `%USERPROFILE%\.praktiKST` und nicht im Programmordner gespeichert werden.
|
||||
|
||||
### AppImage aktualisieren
|
||||
|
||||
1. Lade das neue AppImage herunter.
|
||||
2. Mache es ausführbar.
|
||||
3. Starte die neue Datei.
|
||||
4. Entferne das bisherige AppImage erst, wenn die neue Version funktioniert.
|
||||
|
||||
### Debian und Ubuntu aktualisieren
|
||||
|
||||
```bash
|
||||
flatpak remote-add --if-not-exists kst4contest https://praktimarc.github.io/kst4contest/kst4contest.flatpakrepo
|
||||
|
||||
# Stable (Standard)
|
||||
flatpak install kst4contest de.x08.KST4Contest
|
||||
|
||||
# Beta
|
||||
flatpak install kst4contest de.x08.KST4Contest//beta
|
||||
|
||||
# Nightly
|
||||
flatpak install kst4contest de.x08.KST4Contest//nightly
|
||||
sudo apt install ./KST4Contest-v<Version>-debian-amd64.deb
|
||||
```
|
||||
|
||||
> **Hinweis:** Flatpak erlaubt nur eine installierte Version pro App-ID gleichzeitig. Um den Kanal zu wechseln, zuerst die aktuelle Version deinstallieren:
|
||||
> ```bash
|
||||
> flatpak uninstall de.x08.KST4Contest
|
||||
> flatpak install kst4contest de.x08.KST4Contest//nightly
|
||||
> ```
|
||||
### Fedora aktualisieren
|
||||
|
||||
Updates erfolgen wie gewohnt mit `flatpak update`.
|
||||
```bash
|
||||
sudo dnf upgrade ./KST4Contest-v<Version>-fedora-x86_64.rpm
|
||||
```
|
||||
|
||||
Die `flatpakref`-Dateien für Beta und Stable liegen jeweils im entsprechenden [GitHub-Release](https://github.com/praktimarc/kst4contest/releases). Das Nightly-`flatpakref` (`de.x08.KST4Contest.nightly.flatpakref`) ist als Artifact im [GitHub Actions](https://github.com/praktimarc/kst4contest/actions)-Tab verfügbar (wird 14 Tage aufbewahrt) – der Remote-Weg oben ist für Nightly daher meist einfacher.
|
||||
### Arch-Paket aktualisieren
|
||||
|
||||
### macOS
|
||||
1. DMG-Datei für die eigene Architektur herunterladen (Apple Silicon oder Intel).
|
||||
2. DMG-Datei öffnen.
|
||||
3. `KST4Contest.app` in den **Programme**-Ordner ziehen.
|
||||
4. Beim ersten Start zeigt macOS ggf. eine Warnung, da die App nicht notarisiert ist. Zum Öffnen:
|
||||
- Rechtsklick (oder Ctrl-Klick) auf `KST4Contest.app` im Finder → **Öffnen** wählen.
|
||||
- Alternativ: **Systemeinstellungen → Datenschutz & Sicherheit** → **Trotzdem öffnen** klicken.
|
||||
5. KST4Contest aus dem Programme-Ordner oder dem Launchpad starten.
|
||||
```bash
|
||||
sudo pacman -U KST4Contest-v<Version>-archlinux-x86_64.pkg.tar.zst
|
||||
```
|
||||
|
||||
Die Einstellungen werden unter `~/.praktikst/preferences.xml` gespeichert.
|
||||
### AUR-Paket aktualisieren
|
||||
|
||||
Aktualisiere das installierte Paket über den verwendeten AUR-Helper, beispielsweise:
|
||||
|
||||
```bash
|
||||
yay -Syu
|
||||
```
|
||||
|
||||
### Flatpak aktualisieren
|
||||
|
||||
```bash
|
||||
flatpak update de.x08.KST4Contest
|
||||
```
|
||||
|
||||
### macOS aktualisieren
|
||||
|
||||
1. Lade die neue DMG-Datei für die vorhandene Architektur herunter.
|
||||
2. Beende KST4Contest.
|
||||
3. Öffne die DMG-Datei.
|
||||
4. Ersetze `KST4Contest.app` im Programme-Ordner.
|
||||
5. Starte die neue Version und prüfe die vorhandenen Einstellungen.
|
||||
|
||||
Die Konfiguration unter `~/.praktiKST` bleibt davon unberührt.
|
||||
|
||||
---
|
||||
|
||||
## Update
|
||||
## Probleme beim ersten Start
|
||||
|
||||
KST4Contest enthält einen **automatischen Update-Hinweis-Dienst**: Sobald eine neue Version verfügbar ist, erscheint beim Start ein Fenster mit:
|
||||
- der Information, dass eine neue Version vorliegt,
|
||||
- einem Changelog,
|
||||
- dem Download-Link zur neuen Version.
|
||||
### Windows meldet eine unbekannte Anwendung
|
||||
|
||||

|
||||
KST4Contest ist derzeit nicht mit einem kommerziellen Windows-Code-Signing-Zertifikat signiert. Windows oder ein zusätzlich installiertes Sicherheitsprodukt kann deshalb vor einer unbekannten oder selten heruntergeladenen Anwendung warnen.
|
||||
|
||||
### Update-Prozess
|
||||
Prüfe in diesem Fall zuerst:
|
||||
|
||||
#### Windows
|
||||
- Stammt die Datei aus dem [offiziellen KST4Contest-Release](https://github.com/praktimarc/kst4contest/releases/latest)?
|
||||
- Passt der Dateiname zum veröffentlichten Release?
|
||||
- Wurde die Datei vollständig heruntergeladen?
|
||||
- Nennt das Sicherheitsprogramm einen konkreten Erkennungsnamen oder nur eine allgemeine Reputationswarnung?
|
||||
|
||||
Derzeit gibt es nur einen Weg zum Aktualisieren:
|
||||
Ein Warnhinweis ist allein weder ein sicherer Beweis für Schadsoftware noch automatisch ein Fehlalarm. Wenn die Herkunft der Datei unklar ist, sollte sie nicht gestartet oder aus der Quarantäne wiederhergestellt werden.
|
||||
|
||||
1. Den alten Ordner löschen.
|
||||
2. Das neue ZIP entpacken.
|
||||
Einige Anwender haben insbesondere Quarantäne-Meldungen von Norton 360 gemeldet. Wenn sich eine Meldung reproduzieren lässt, erstelle bitte einen [GitHub-Issue](https://github.com/praktimarc/kst4contest/issues) und nenne:
|
||||
|
||||
Die Einstellungsdatei (`preferences.xml`) bleibt erhalten, da sie im Benutzerordner gespeichert ist – nicht im Programmordner.
|
||||
- die KST4Contest-Version,
|
||||
- den vollständigen Dateinamen,
|
||||
- das verwendete Sicherheitsprodukt und dessen Version,
|
||||
- den angezeigten Erkennungsnamen,
|
||||
- möglichst einen Screenshot der Meldung.
|
||||
|
||||
#### Linux
|
||||
### Das AppImage startet nicht
|
||||
|
||||
- **AppImage**: Neues AppImage herunterladen, ausführbar machen (`chmod +x`), altes optional löschen.
|
||||
- **Debian/Ubuntu**: `sudo apt install ./KST4Contest-v<Version>-debian-amd64.deb`
|
||||
- **Fedora/RHEL**: `sudo dnf upgrade ./KST4Contest-v<Version>-fedora-x86_64.rpm`
|
||||
- **Arch Linux**: `sudo pacman -U KST4Contest-v<Version>-archlinux-x86_64.pkg.tar.zst`
|
||||
- **Flatpak (Repository)**: `flatpak update` – aktualisiert alle Flatpak-Apps einschließlich KST4Contest.
|
||||
Prüfe zuerst das Ausführungsrecht:
|
||||
|
||||
#### macOS
|
||||
```bash
|
||||
chmod +x KST4Contest-v<Version>-linux-x86_64.AppImage
|
||||
```
|
||||
|
||||
1. Neue DMG-Datei herunterladen.
|
||||
2. DMG öffnen.
|
||||
3. Die neue `KST4Contest.app` in den **Programme**-Ordner ziehen und die alte Version ersetzen.
|
||||
Starte die Datei anschließend aus einem Terminal. Fehlermeldungen sind dort meist aussagekräftiger als ein Doppelklick, der lediglich nichts Sichtbares tut.
|
||||
|
||||
### macOS blockiert die Anwendung
|
||||
|
||||
---
|
||||
Verwende die unter [Installation unter macOS](#installation-unter-macos) beschriebene Funktion **Öffnen** im Kontextmenü. Prüfe vorher, ob die DMG-Datei aus dem offiziellen GitHub Release stammt.
|
||||
|
||||
## Bekannte Probleme beim Start
|
||||
### Das Problem bleibt bestehen
|
||||
|
||||
### Norton 360
|
||||
Prüfe zunächst, ob das Problem bereits unter [GitHub Issues](https://github.com/praktimarc/kst4contest/issues) beschrieben wurde. Falls nicht, erstelle einen neuen Issue mit:
|
||||
|
||||
Norton 360 stuft `praktiKST.exe` als gefährlich ein (Fehlalarm). Es muss eine Ausnahme für die Datei eingerichtet werden:
|
||||
- Betriebssystem und Version,
|
||||
- verwendeter KST4Contest-Version,
|
||||
- Installationsart,
|
||||
- genauer Fehlermeldung,
|
||||
- den Schritten, mit denen sich das Problem reproduzieren lässt.
|
||||
|
||||
1. Norton 360 öffnen.
|
||||
2. Sicherheit → Verlauf → Das entsprechende Ereignis suchen.
|
||||
3. „Wiederherstellen & Ausnahme hinzufügen" wählen.
|
||||
|
||||
*(Gemeldet von PE0WGA, Franz van Velzen – danke!)*
|
||||
„Geht nicht“ beschreibt den Zustand meistens korrekt, hilft bei der Fehlersuche aber nur begrenzt.
|
||||
@@ -14,7 +14,7 @@ Nach dem ersten Start öffnet sich das **Einstellungsfenster** – dieses ist de
|
||||
|
||||
### Login und Chat-Kategorien
|
||||
|
||||
Hier werden die Zugangsdaten für den ON4KST-Chat eingetragen (Rufzeichen und Passwort).
|
||||
Hier werden die Zugangsdaten für den ON4KST-Chat eingetragen (Rufzeichen und Passwort).
|
||||
Zudem wird die **primäre Chat-Kategorie** (z. B. IARU Region 1 VHF/Microwave) ausgewählt.
|
||||
|
||||
Mit der Option für einen **zweiten Chat** (Multi-Channel-Login) kann man sich gleichzeitig in eine weitere Kategorie (z. B. UHF/SHF) einloggen. Beide Chats werden dann parallel überwacht. Hier kann optional auch ein abweichender Login-Name für den zweiten Chat vergeben werden (nützlich für Opposite Station Multi-Callsign Logging).
|
||||
@@ -87,11 +87,77 @@ Konfiguration der Schnittstelle zu AirScout für die Flugzeug-Scatter-Erkennung.
|
||||
|
||||
## Notification Settings (Benachrichtigungen)
|
||||
|
||||
Drei Benachrichtigungstypen stehen zur Wahl:
|
||||

|
||||
|
||||
1. **Einfache Sounds**: TADA-Sound für eingehende Nachrichten, Tick für Sked-Richtungserkennung usw.
|
||||
2. **CW-Ansage**: Das Rufzeichen einer Station, die eine Privatnachricht sendet, wird als CW-Signal ausgegeben.
|
||||
3. **Phonetische Ansage**: Das Rufzeichen wird phonetisch ausgesprochen.
|
||||
Im Reiter **Notification** werden nicht nur akustische Hinweise konfiguriert. Hier befinden sich auch die Einstellungen für den lokalen DX-Cluster-Server, den Band-Upgrade-Hinweis und das QSO-Monitoring.
|
||||
|
||||
### Akustische Hinweise
|
||||
|
||||
Die drei Audiofunktionen arbeiten unabhängig voneinander:
|
||||
|
||||
- **Play notification sounds …** aktiviert kurze Hinweistöne für neue Privatnachrichten, erkannte Richtungsgelegenheiten, Sked-Erinnerungen und Band-Upgrade-Hinweise.
|
||||
- **Spell the sender's callsign in CW …** gibt das Rufzeichen des Absenders einer neuen Privatnachricht als CW-Signal aus.
|
||||
- **Speak the sender's callsign phonetically …** spricht das Rufzeichen des Absenders phonetisch aus.
|
||||
|
||||
CW- und Sprachausgabe können gleichzeitig aktiviert werden. Das ist technisch möglich, im Contest aber nicht zwingend hilfreich. In der Praxis sollte nur die Ausgabe eingeschaltet werden, die im eigenen Stationsbetrieb tatsächlich wahrgenommen werden kann, ohne den Operator dauerhaft zu beschäftigen.
|
||||
|
||||
### Local DX Cluster output
|
||||
|
||||
KST4Contest kann erkannte Richtungsgelegenheiten als DX-Cluster-Spots an ein Logprogramm weitergeben. Der praktische Nutzen liegt auf der Hand: Eine im Chat erkannte Frequenz erscheint direkt in der Bandmap des Logprogramms und muss nicht erst von Hand übertragen werden.
|
||||
|
||||
Die Checkbox **Enable the local DX Cluster server …** startet beziehungsweise beendet den lokalen TCP-Server. Bei einer laufenden Chat-Verbindung wird die Änderung sofort wirksam.
|
||||
|
||||
Folgende Einstellungen sind erforderlich:
|
||||
|
||||
- **TCP port**: Port, auf dem KST4Contest Verbindungen von DX-Cluster-Clients annimmt. Der Standardwert ist `8000`. Wird der Port während einer laufenden Verbindung geändert, startet KST4Contest den Server auf dem neuen Port neu. Der Logger muss sich anschließend ebenfalls mit dem neuen Port verbinden.
|
||||
- **Fallback band in MHz**: Bandpräfix für relative Frequenzangaben. Aus `205` oder `.205` wird bei einem Fallback-Band von `144` die Frequenz `144.205 MHz`. Vollständige Angaben wie `432.205` oder `1296.338` benötigen diesen Fallback nicht.
|
||||
- **Spotter callsign**: Rufzeichen, das im erzeugten DX-Cluster-Spot als Spotter erscheint. Hier sollte ein anderes Rufzeichen als das im Contest verwendete Stationsrufzeichen eingetragen werden. Einige Logprogramme filtern Spots des eigenen Rufzeichens oder behandeln sie anders als fremde Spots.
|
||||
- **Send test spot**: Sendet einen Testspot für `DL0TEST` auf `.300` des eingestellten Fallback-Bandes. Der Test funktioniert nur, wenn KST4Contest mit dem Chat verbunden, der lokale DX-Cluster-Server aktiviert und mindestens ein DX-Cluster-Client verbunden ist.
|
||||
|
||||
KST4Contest erzeugt nicht bei jeder im Chat gefundenen Frequenz automatisch einen Spot. Ein Spot entsteht nur dann, wenn eine gerichtete Nachricht zwischen zwei Stationen auf eine für die eigene Station interessante Antennenrichtung schließen lässt und für den Absender eine nutzbare Frequenz bekannt ist.
|
||||
|
||||
Die vollständige Herleitung und die Einrichtung des Logprogramms sind im Kapitel [Integrierter DX-Cluster-Server](de-DX-Cluster-Server) beschrieben.
|
||||
|
||||
### Band-Upgrade-Hinweis nach einem Logeintrag
|
||||
|
||||
Nach einem über UCXLog oder Win-Test empfangenen Logeintrag kann KST4Contest prüfen, ob die gerade gearbeitete Station auf einem weiteren gemeinsamen, aber noch nicht gearbeiteten Band aktiv ist.
|
||||
|
||||
Dafür werden drei Informationen miteinander verglichen:
|
||||
|
||||
1. die in den Stationseinstellungen aktivierten eigenen Bänder,
|
||||
2. die innerhalb der letzten 30 Minuten erkannten Bänder der Gegenstation,
|
||||
3. die bereits pro Band gespeicherten Worked-Markierungen.
|
||||
|
||||
Bleibt danach mindestens ein gemeinsames, noch nicht gearbeitetes Band übrig, erscheint im Hauptfenster ein blinkender **BAND+**-Hinweis. Ist die allgemeine Soundausgabe aktiviert, wird zusätzlich ein Hinweiston abgespielt.
|
||||
|
||||
Die beiden Optionen haben unterschiedliche Aufgaben:
|
||||
|
||||
- **Blink + sound …** aktiviert den eigentlichen Band-Upgrade-Hinweis.
|
||||
- **Priority boost …** erhöht zusätzlich die Priorität entsprechender Stationen, damit sie in den Kandidatenlisten besser sichtbar bleiben. Der Boost garantiert keinen bestimmten Listenplatz; er ist nur ein zusätzlicher Faktor innerhalb der gesamten Prioritätsberechnung.
|
||||
|
||||
Der Hinweis setzt eine Log-Synchronisation mit Bandinformation voraus. Der einfache dateibasierte Callsign-Interpreter kann nur Rufzeichen erkennen und liefert deshalb keine ausreichende Grundlage für diese Prüfung.
|
||||
|
||||
### Sniffer-Einstellungen (ab v1.31)
|
||||
|
||||
Das QSO-Monitoring ist für Stationen gedacht, deren Kommunikation man gezielt verfolgen möchte. Das kann beispielsweise eine seltene Station, eine DXpedition oder eine andere Station des eigenen Contest-Teams sein.
|
||||
|
||||
Für jedes eingetragene Rufzeichen zeigt KST4Contest Nachrichten zusätzlich in der PM-Tabelle an, wenn das Rufzeichen entweder Absender oder Empfänger der Nachricht ist. Die ursprüngliche Nachricht wird dabei nicht aus ihrer normalen Tabelle entfernt.
|
||||
|
||||
Überwachte Nachrichten werden in der PM-Tabelle eindeutig gekennzeichnet:
|
||||
|
||||
```text
|
||||
Sniffed: (SENDER > RECEIVER) Nachrichtentext
|
||||
```
|
||||
|
||||
So wird sichtbar, dass die Nachricht nicht an die eigene Station gerichtet war.
|
||||
|
||||
Rufzeichen werden folgendermaßen verwaltet:
|
||||
|
||||
1. Mit **Add monitored callsign** ein neues Rufzeichen hinzufügen.
|
||||
2. Ein vorhandenes Rufzeichen per Doppelklick bearbeiten und die Änderung mit `Enter` übernehmen.
|
||||
3. Zum Entfernen den Inhalt einer Tabellenzelle löschen und mit `Enter` bestätigen.
|
||||
|
||||
Doppelte oder syntaktisch ungültige Rufzeichen werden nicht übernommen. Die Liste wird mit **Save Settings** in der `preferences.xml` gespeichert und beim nächsten Programmstart wiederhergestellt.
|
||||
|
||||
---
|
||||
|
||||
@@ -163,17 +229,6 @@ Einstellungen:
|
||||
|
||||
---
|
||||
|
||||
## Sniffer-Einstellungen (ab v1.31)
|
||||
|
||||
Der QSO-Sniffer filtert Chat-Nachrichten von konfigurierbaren Rufzeichen und leitet sie ins PM-Fenster weiter.
|
||||
|
||||
Einstellungen:
|
||||
- **Rufzeichen-Liste**: Kommagetrennte Liste von Rufzeichen, deren Nachrichten immer in das PM-Fenster weitergeleitet werden sollen.
|
||||
|
||||
Anwendungsfall: Wichtige Stationen (z. B. DX-Peditionen oder feste Verbündete im Contest) im Auge behalten, ohne den Haupt-Chat ständig zu beobachten.
|
||||
|
||||
---
|
||||
|
||||
## Worked Station Database Settings (Gearbeitete-Stationen-Datenbank)
|
||||
|
||||
Die interne Worked-Datenbank enthält:
|
||||
|
||||
+90
-35
@@ -1,62 +1,117 @@
|
||||
# KST4Contest – Wiki
|
||||
# KST4Contest – User Manual
|
||||
|
||||
> 🇬🇧 You are reading the English version | 🇩🇪 [Deutsche Version](de-Home)
|
||||
> You are reading the English version | [Deutsche Version](de-Home)
|
||||
|
||||
**KST4Contest** (also known as *PraktiKST*) is a Java-based chat client for the [ON4KST Chat](http://www.on4kst.info/chat/), specifically designed for contest operation on the VHF/UHF/SHF bands (144 MHz and above).
|
||||
KST4Contest is a desktop client for the [ON4KST Chat](https://www.on4kst.org/chat/login.php), developed for VHF, UHF and SHF contest operation. It brings chat, candidate selection, sked planning, aircraft scatter data and external station software together in a single operating interface.
|
||||
|
||||
Developed by **DO5AMF (Marc Fröhlich)**, operator at DM5M.
|
||||
KST4Contest is developed by **DO5AMF (Marc Fröhlich)**, operator at DM5M and (since May 2025) **DN9APW (Philipp Wagner)**. The source code is publicly available on [GitHub](https://github.com/praktimarc/kst4contest).
|
||||
|
||||
---
|
||||
|
||||
## Quick Navigation
|
||||
## Why use a dedicated ON4KST client?
|
||||
|
||||
During a contest, the ON4KST Chat provides a considerable amount of information: active stations, locators, frequencies, sked requests and indications of current activity. The actual problem is not seeing this data. It is turning it into the next useful contact in time.
|
||||
|
||||
KST4Contest evaluates the available information, puts it into context and presents it as part of a contest-oriented workflow. This includes antenna direction, distance, known bands and frequencies, worked status, chat activity and aircraft scatter timing.
|
||||
|
||||
The program does not decide which QSO is actually possible. Priority scores, the AP timeline and visual highlights are decision aids. The final judgement remains with the operator – not least because even a very convincing computer screen cannot complete a radio contact.
|
||||
|
||||
---
|
||||
|
||||
## How KST4Contest supports contest operation
|
||||
|
||||
- **Observe and organise chat activity:** KST4Contest can display two ON4KST chat categories at the same time. Messages, frequency information and known band activity are assigned to the corresponding stations.
|
||||
|
||||
- **Reduce the station list:** Direction, distance, worked and NOT-QRV filters help limit the user list to stations that are relevant to the current operating situation.
|
||||
|
||||
- **Prioritise candidates:** The score system evaluates active chat members using several known criteria. The currently most relevant candidates are also shown in a separate priority list.
|
||||
|
||||
- **Prepare skeds and keep them visible:** Sked reminders, automatic advance messages and the AP timeline help prevent scheduled contacts from disappearing somewhere between chat, logging and ongoing CQ operation.
|
||||
|
||||
- **Include aircraft scatter information:** The AirScout interface brings suitable aircraft and expected reflection times into candidate evaluation and sked planning.
|
||||
|
||||
- **Connect the logger and station equipment:** KST4Contest synchronises worked stations and frequency information with supported logging software. It also provides interfaces for Win-Test, PSTRotator and a built-in DX Cluster server.
|
||||
|
||||
- **Display stations and radio paths:** The station map shows active chat members, locator squares, antenna directions and the path to the selected station. A terrain profile can also be calculated for selected paths.
|
||||
|
||||
These functions share information. A detected frequency can indicate an active band, worked status comes from the logger, aircraft scatter data adds timing information and the resulting score affects the priority list. Missing or outdated input data can therefore affect the result as well.
|
||||
|
||||
---
|
||||
|
||||
## Requirements
|
||||
|
||||
A registered ON4KST account is required. Registration and login are available from the [official ON4KST login page](https://www.on4kst.org/chat/login.php).
|
||||
|
||||
English is the official language of the ON4KST Chat. This also applies when communicating with stations from your own country. Common amateur radio abbreviations such as `pse`, `agn`, `qrg`, `dir`, `rrr`, `tnx` and `73` are normal and usually considerably faster than carefully written prose.
|
||||
|
||||
Downloads, supported operating systems and installation methods are described in [Installation](en-Installation).
|
||||
|
||||
---
|
||||
|
||||
## Manual version
|
||||
|
||||
This manual describes the stable release **v1.41.1**.
|
||||
|
||||
Features or changes that are only available in the current development version are explicitly marked as **Nightly / v1.42**. If no such label is present, the description refers to the stable release.
|
||||
|
||||
- [Download the current stable release](https://github.com/praktimarc/kst4contest/releases/latest)
|
||||
- [Nightly and automated builds](https://github.com/praktimarc/kst4contest/actions)
|
||||
- [Version history](en-Changelog)
|
||||
|
||||
For contest operation, the stable release is generally the appropriate choice. Nightly builds contain newer fixes and features, but may change between builds. They are useful when a particular change needs testing. Trying one for the first time ten minutes before a contest is less useful.
|
||||
|
||||
---
|
||||
|
||||
## Quick navigation
|
||||
|
||||
| Page | Contents |
|
||||
|---|---|
|
||||
| [Installation](en-Installation) | Download, Java requirements, update |
|
||||
| [Configuration](en-Configuration) | All settings in detail |
|
||||
| [Log Synchronisation](en-Log-Sync) | UCXLog, N1MM+, QARTest, DXLog.net, WinTest |
|
||||
| [AirScout Integration](en-AirScout-Integration) | Aircraft scatter detection |
|
||||
| [DX Cluster Server](en-DX-Cluster-Server) | Built-in DX cluster for your logging software |
|
||||
| [Features](en-Features) | All features at a glance |
|
||||
| [Macros and Variables](en-Macros-and-Variables) | Text snippets, shortcuts, variables |
|
||||
| [User Interface](en-User-Interface) | UI explanation and operation |
|
||||
| [Changelog](en-Changelog) | Version history |
|
||||
| [Installation](en-Installation) | ON4KST account, downloads, installation and updates |
|
||||
| [Configuration](en-Configuration) | Login, station, bands, user interface and external connections |
|
||||
| [Log Synchronisation](en-Log-Sync) | Simplelogfile, UCXLog, N1MM+, QARTest, DXLog.net and Win-Test |
|
||||
| [AirScout Integration](en-AirScout-Integration) | Connecting AirScout and evaluating aircraft scatter timing |
|
||||
| [DX Cluster Server](en-DX-Cluster-Server) | Passing detected opportunities to logging software |
|
||||
| [Features](en-Features) | Operation, reasoning and limitations of individual functions |
|
||||
| [Macros and Variables](en-Macros-and-Variables) | Reusable messages, shortcuts and automatically substituted values |
|
||||
| [User Interface](en-User-Interface) | Interface layout and operation during a contest |
|
||||
| [Changelog](en-Changelog) | Releases, Nightly changes and resolved issues |
|
||||
|
||||
---
|
||||
|
||||
## What is KST4Contest?
|
||||
## Contact and support
|
||||
|
||||
The ON4KST Chat is the de-facto standard for skeds on the 144 MHz and higher bands. KST4Contest enhances the chat experience with contest-specific features:
|
||||
- **Download:** [Current stable release](https://github.com/praktimarc/kst4contest/releases/latest)
|
||||
- **Source code:** [praktimarc/kst4contest](https://github.com/praktimarc/kst4contest)
|
||||
- **Bug reports and feature requests:** [GitHub Issues](https://github.com/praktimarc/kst4contest/issues)
|
||||
- **Email:** praktimarc+kst4contest@gmail.com
|
||||
Please use this address only for KST4Contest-related topics.
|
||||
|
||||
- **Worked marking**: Stations already worked are highlighted visually, synchronised directly from your logging software via UDP.
|
||||
- **Sked direction detection**: When a station calls another one from your direction, it is highlighted green and bold.
|
||||
- **QRG detection**: KST4Contest automatically reads frequencies from the chat traffic and shows them in the user list.
|
||||
- **AirScout interface**: Reflectable aircraft are shown directly in the user list.
|
||||
- **Built-in DX cluster server**: Spots are sent directly to your logging software.
|
||||
- **Dark mode** (from v1.26): Easy on the eyes during night-time operation.
|
||||
- **Multi-channel login** (from v1.26): Simultaneously logged into two chat categories.
|
||||
### Reporting a bug
|
||||
|
||||
---
|
||||
A problem is considerably easier to reproduce when the report contains at least:
|
||||
|
||||
## Contact & Support
|
||||
1. the KST4Contest version,
|
||||
2. the operating system and installation method,
|
||||
3. the exact steps leading to the problem,
|
||||
4. the expected and observed behaviour,
|
||||
5. a screenshot where appropriate,
|
||||
6. the error log file.
|
||||
|
||||
- **Email**: praktimarc+kst4contest@gmail.com *(for kst4contest topics only)*
|
||||
- **GitHub**: https://github.com/praktimarc/kst4contest
|
||||
- **Download**: https://github.com/praktimarc/kst4contest/releases/latest
|
||||
The error log is stored at:
|
||||
|
||||
### Reporting a bug (creating an issue)
|
||||
|
||||
When reporting a bug, please **always attach the log file**. KST4Contest automatically writes an error log (error messages only, no personal data):
|
||||
|
||||
| Operating system | Log file location |
|
||||
| Operating system | Path |
|
||||
|---|---|
|
||||
| Linux / macOS | `~/.praktiKST/kst4contest-errors.log` |
|
||||
| Windows | `C:\Users\<YourName>\.praktiKST\kst4contest-errors.log` |
|
||||
|
||||
When opening an issue on GitHub, a template is provided that guides you through all relevant fields.
|
||||
Please inspect the file briefly before uploading it. An error log mainly contains technical information, but depending on the problem it may also include local file paths or other contextual data.
|
||||
|
||||
Some file and directory names still use the technical name `praktiKST`. They refer to the same program.
|
||||
|
||||
---
|
||||
|
||||
## Acknowledgements
|
||||
|
||||
Special thanks to: Gianluca Costantino (IU3OAR), Alessandro Murador (IZ3VTH), Reczetár István (HA1FV), OM0AAO (Viliam Petrik, DX cluster idea), DC9DJ (Konrad Neitzel, project structure), DO5ALF (Andreas, webmaster funkerportal.de), PE0WGA (Franz van Velzen, tester) and all other testers and contributors.
|
||||
KST4Contest has benefited considerably from feedback gathered during actual contest operation.
|
||||
|
||||
Special thanks go to Gianluca Costantino (IU3OAR), Alessandro Murador (IZ3VTH), Reczetár István (HA1FV), Viliam Petrik (OM0AAO, DX Cluster idea), Konrad Neitzel (DC9DJ, project structure), Andreas (DO5ALF, webmaster of funkerportal.de), Franz van Velzen (PE0WGA, testing), and all other testers and contributors.
|
||||
+344
-141
@@ -2,219 +2,422 @@
|
||||
|
||||
> 🇬🇧 You are reading the English version | 🇩🇪 [Deutsche Version](de-Installation)
|
||||
|
||||
## Prerequisites
|
||||
KST4Contest is distributed as a ready-to-run application package for Windows, Linux and macOS. The official release packages do not require a separate Java installation: the required Java runtime is already included.
|
||||
|
||||
An resolution of 1200px by 720px is recommended
|
||||
In practical terms, you need a supported operating system, an internet connection and an ON4KST account. That sounds manageable—and usually it is.
|
||||
|
||||
### ON4KST Account
|
||||
## Requirements
|
||||
|
||||
To use the chat, a registered account with the ON4KST chat service is required:
|
||||
### ON4KST account
|
||||
|
||||
- Register at: http://www.on4kst.info/chat/register.php
|
||||
KST4Contest is an independent client for the ON4KST chat, but it does not replace the corresponding user account.
|
||||
|
||||
### Chat Etiquette
|
||||
If you do not have an account yet, you can create one on the official ON4KST website:
|
||||
|
||||
The official language in the ON4KST Chat is **English**. Please use English even when communicating with stations from your own country. Common HAM abbreviations (agn, dir, pse, rrr, tnx, 73 …) are widely used and understood.
|
||||
- [ON4KST login and registration](https://www.on4kst.org/chat/login.php)
|
||||
|
||||
### Personal Messages
|
||||
ON4KST specifies English as the common language for the chat. This also applies when both stations happen to share another language. During contests, the usual amateur radio abbreviations such as `pse`, `agn`, `qrg`, `dir`, `rrr`, `tnx` and `73` are widely used.
|
||||
|
||||
To send a private message to another station, always use the following format:
|
||||
Using the chat and sending personal messages are covered in the user interface chapter. For the installation, the only thing that matters at this point is that the account works.
|
||||
|
||||
```
|
||||
/CQ CALLSIGN message text
|
||||
```
|
||||
### Screen size
|
||||
|
||||
Example: `/CQ DL5ASG pse sked 144.205?`
|
||||
A usable screen area of approximately **1200 × 720 pixels** or more is recommended.
|
||||
|
||||
During heavy chat traffic (5–6 messages per second in a contest), public messages directed at a specific callsign are easily missed. However, KST4Contest also catches such messages if they are accidentally posted publicly (see [Features – PM Catching](Features#catching-personal-messages)).
|
||||
KST4Contest automatically adapts the main window to the available area of the primary screen. The application can therefore also be started on smaller displays. Less space is still less space, however: not all tables, filters and additional information can then be displayed at a useful size at the same time.
|
||||
|
||||
### Java
|
||||
|
||||
The ready-made packages from the [GitHub Releases](https://github.com/praktimarc/kst4contest/releases/latest) do not require a separate Java installation. The required runtime is distributed with KST4Contest.
|
||||
|
||||
A Java development environment is only required if you want to build KST4Contest from source. For the AUR packages `kst4contest` and `kst4contest-git`, Java 21 and Maven are handled as build dependencies by the package manager.
|
||||
|
||||
---
|
||||
|
||||
## Stable, Beta or Nightly?
|
||||
|
||||
KST4Contest is distributed through three development channels:
|
||||
|
||||
| Channel | Intended use | Classification |
|
||||
|---|---|---|
|
||||
| **Stable** | Normal contest operation | Published version intended for regular use |
|
||||
| **Beta** | Focused testing before a Stable release | Pre-release version with a largely defined feature set |
|
||||
| **Nightly** | Early testing of new features | Current development state from the `main` branch |
|
||||
|
||||
The **Stable version** is recommended for normal operation.
|
||||
|
||||
Beta and Nightly builds may contain features that are not yet available in the Stable release. They may also contain unfinished workflows, changed settings or new defects. That is not an unusual failure of the release process; it is the purpose of a development channel.
|
||||
|
||||
If a function in this manual is explicitly marked as **Nightly**, it is not necessarily part of the current Stable release yet.
|
||||
|
||||
---
|
||||
|
||||
## Download
|
||||
|
||||
### Windows
|
||||
The current Stable version is available here:
|
||||
|
||||
The latest version can be downloaded as a ZIP file:
|
||||
- [Latest KST4Contest release](https://github.com/praktimarc/kst4contest/releases/latest)
|
||||
- [All published releases](https://github.com/praktimarc/kst4contest/releases)
|
||||
|
||||
**https://github.com/praktimarc/kst4contest/releases/latest**
|
||||
The release page contains the application packages as well as the English and German PDF manuals.
|
||||
|
||||
The filename has the format `praktiKST-v<version_number>-windows-x64.zip`.
|
||||
### Which package do I need?
|
||||
|
||||
### Linux
|
||||
|
||||
Multiple package formats are available from the releases page:
|
||||
|
||||
**https://github.com/praktimarc/kst4contest/releases/latest**
|
||||
|
||||
| Format | Filename | Suitable for |
|
||||
| Operating system | Package | Typical filename |
|
||||
|---|---|---|
|
||||
| AppImage | `KST4Contest-v<version>-linux-x86_64.AppImage` | All distributions |
|
||||
| Debian package | `KST4Contest-v<version>-debian-amd64.deb` | Debian, Ubuntu, Linux Mint, … |
|
||||
| RPM package | `KST4Contest-v<version>-fedora-x86_64.rpm` | Fedora, RHEL, openSUSE, … |
|
||||
| Arch package | `KST4Contest-v<version>-archlinux-x86_64.pkg.tar.zst` | Arch Linux, Manjaro, … |
|
||||
| Flatpak | `de.x08.KST4Contest.flatpakref` | All distributions with Flatpak |
|
||||
|
||||
> **Recommended for Linux:** The Flatpak installation is the easiest way to stay up to date — `flatpak update` handles all future updates automatically. The repository is GPG-signed for security.
|
||||
|
||||
### macOS
|
||||
|
||||
> ⚠️ **Best-Effort Support:** macOS builds are provided as a convenience but are **not fully tested**. We build and release macOS binaries with every release, but we cannot test every scenario on macOS. If you encounter issues, please report them – we will do our best to address them, but cannot guarantee the same level of support as for Windows and Linux.
|
||||
|
||||
The latest version can be downloaded as a DMG disk image (available for both Apple Silicon and Intel Macs):
|
||||
|
||||
**https://github.com/praktimarc/kst4contest/releases/latest**
|
||||
|
||||
The filename has the format `KST4Contest-v<version_number>-macos-<arch>.dmg`, where `<arch>` is `arm64` (Apple Silicon) or `x86_64` (Intel).
|
||||
| Windows x64 | ZIP package | `praktiKST-v<version>-windows-x64.zip` |
|
||||
| Linux x86_64 | AppImage | `KST4Contest-v<version>-linux-x86_64.AppImage` |
|
||||
| Debian/Ubuntu amd64 | DEB package | `KST4Contest-v<version>-debian-amd64.deb` |
|
||||
| Fedora/RPM x86_64 | RPM package | `KST4Contest-v<version>-fedora-x86_64.rpm` |
|
||||
| Arch Linux x86_64 | Arch package | `KST4Contest-v<version>-archlinux-x86_64.pkg.tar.zst` |
|
||||
| Linux with Flatpak | Flatpak reference | `de.x08.KST4Contest.flatpakref` |
|
||||
| macOS Apple Silicon | DMG for ARM64 | `KST4Contest-v<version>-macos-arm64.dmg` |
|
||||
| macOS Intel | DMG for x86_64 | `KST4Contest-v<version>-macos-x86_64.dmg` |
|
||||
|
||||
Only download application packages from the official GitHub Releases, the KST4Contest Flatpak repository or the linked AUR packages. Files from other sources may have been built differently, may be outdated or may have been modified.
|
||||
|
||||
---
|
||||
|
||||
## Installation
|
||||
## Installing on Windows
|
||||
|
||||
### Windows
|
||||
KST4Contest is distributed as a ZIP package for Windows. A conventional installer is not required.
|
||||
|
||||
1. Download the ZIP file.
|
||||
2. Unzip the ZIP file into a folder of your choice.
|
||||
3. Run `praktiKST.exe`.
|
||||
1. Download `praktiKST-v<version>-windows-x64.zip` from the latest release.
|
||||
2. Extract the ZIP file completely into a dedicated folder.
|
||||
3. Open the extracted folder.
|
||||
4. Start `praktiKST.exe`.
|
||||
|
||||
Settings are stored at `%USERPROFILE%\.praktikst\preferences.xml`.
|
||||
Do not start the application directly from the compressed ZIP file. KST4Contest consists of several files and a bundled runtime. Windows can only use this structure reliably after the archive has been extracted completely.
|
||||
|
||||
### Linux
|
||||
The application settings are not stored in the extracted program directory. They are stored in your user profile:
|
||||
|
||||
Settings are always stored at `~/.praktikst/preferences.xml`.
|
||||
```text
|
||||
%USERPROFILE%\.praktiKST\preferences.xml
|
||||
```
|
||||
|
||||
#### AppImage
|
||||
This allows a new application version to be extracted into a different directory without losing the existing settings.
|
||||
|
||||
1. Download the AppImage.
|
||||
2. Make it executable: `chmod +x KST4Contest-v<version>-linux-x86_64.AppImage`
|
||||
3. Run it.
|
||||
---
|
||||
|
||||
#### Debian / Ubuntu
|
||||
## Installing on Linux
|
||||
|
||||
Several Linux package formats are available. The appropriate choice depends less on KST4Contest itself than on your distribution and the way you prefer to manage updates.
|
||||
|
||||
| Installation method | Useful when … |
|
||||
|---|---|
|
||||
| **Flatpak** | updates should be managed centrally and Flatpak is already in use |
|
||||
| **AppImage** | KST4Contest should run as a portable file without package installation |
|
||||
| **DEB/RPM** | the distribution’s native package manager should be used |
|
||||
| **Arch package/AUR** | Arch Linux, Manjaro or EndeavourOS is being used |
|
||||
|
||||
### Flatpak
|
||||
|
||||
For most Linux users, Flatpak is the simplest way to keep KST4Contest installed and manageable through the system’s update tools. The KST4Contest repository is GPG-signed and contains the Stable, Beta and Nightly channels.
|
||||
|
||||
#### Installing Stable from the release file
|
||||
|
||||
Download `de.x08.KST4Contest.flatpakref` from the latest release and open it with the software manager provided by your desktop environment.
|
||||
|
||||
Alternatively, install it from a terminal:
|
||||
|
||||
```bash
|
||||
flatpak install ./de.x08.KST4Contest.flatpakref
|
||||
```
|
||||
|
||||
#### Adding the KST4Contest repository
|
||||
|
||||
The repository only needs to be added once:
|
||||
|
||||
```bash
|
||||
flatpak remote-add --if-not-exists kst4contest \
|
||||
https://praktimarc.github.io/kst4contest/kst4contest.flatpakrepo
|
||||
```
|
||||
|
||||
You can then install the Stable version:
|
||||
|
||||
```bash
|
||||
flatpak install kst4contest de.x08.KST4Contest//stable
|
||||
```
|
||||
|
||||
#### Installing Beta
|
||||
|
||||
```bash
|
||||
flatpak install kst4contest de.x08.KST4Contest//beta
|
||||
```
|
||||
|
||||
#### Installing Nightly
|
||||
|
||||
```bash
|
||||
flatpak install kst4contest de.x08.KST4Contest//nightly
|
||||
```
|
||||
|
||||
All three KST4Contest channels use the same application ID. Installing multiple KST4Contest channels in parallel is therefore not supported.
|
||||
|
||||
For example, to switch from Stable to Nightly:
|
||||
|
||||
```bash
|
||||
flatpak uninstall de.x08.KST4Contest//stable
|
||||
flatpak install kst4contest de.x08.KST4Contest//nightly
|
||||
```
|
||||
|
||||
This does not automatically remove the personal settings stored in `~/.praktiKST`.
|
||||
|
||||
Installed Flatpak applications can be updated with:
|
||||
|
||||
```bash
|
||||
flatpak update de.x08.KST4Contest
|
||||
```
|
||||
|
||||
Depending on the desktop environment, the graphical software manager may also display or automatically install available Flatpak updates. The `flatpak update` command itself performs an update; it does not promise to turn up and run by itself one day.
|
||||
|
||||
### AppImage
|
||||
|
||||
The AppImage does not require a conventional installation.
|
||||
|
||||
1. Download `KST4Contest-v<version>-linux-x86_64.AppImage`.
|
||||
2. Open a terminal in the download directory.
|
||||
3. Make the file executable:
|
||||
|
||||
```bash
|
||||
chmod +x KST4Contest-v<version>-linux-x86_64.AppImage
|
||||
```
|
||||
|
||||
4. Start KST4Contest:
|
||||
|
||||
```bash
|
||||
./KST4Contest-v<version>-linux-x86_64.AppImage
|
||||
```
|
||||
|
||||
The AppImage can then be moved to another location, such as `~/Applications`.
|
||||
|
||||
### Debian and Ubuntu
|
||||
|
||||
Install the DEB package with:
|
||||
|
||||
```bash
|
||||
sudo apt install ./KST4Contest-v<version>-debian-amd64.deb
|
||||
```
|
||||
|
||||
Or double-click the `.deb` file in your file manager.
|
||||
Alternatively, open the file with a graphical package manager.
|
||||
|
||||
#### Fedora / RHEL
|
||||
### Fedora and compatible RPM systems
|
||||
|
||||
Install the RPM package with:
|
||||
|
||||
```bash
|
||||
sudo dnf install ./KST4Contest-v<version>-fedora-x86_64.rpm
|
||||
```
|
||||
|
||||
#### Arch Linux
|
||||
### Arch Linux: installing the release package
|
||||
|
||||
The Arch package downloaded from the GitHub Release can be installed directly:
|
||||
|
||||
```bash
|
||||
sudo pacman -U KST4Contest-v<version>-archlinux-x86_64.pkg.tar.zst
|
||||
```
|
||||
|
||||
#### Flatpak
|
||||
### Arch Linux: installing from the AUR
|
||||
|
||||
Download `de.x08.KST4Contest.flatpakref` from the [latest release](https://github.com/praktimarc/kst4contest/releases/latest) and open it, or run:
|
||||
```bash
|
||||
flatpak install de.x08.KST4Contest.flatpakref
|
||||
```
|
||||
Three variants are available in the AUR:
|
||||
|
||||
Or add the remote manually (recommended for nightly/beta access):
|
||||
```bash
|
||||
flatpak remote-add --if-not-exists kst4contest https://praktimarc.github.io/kst4contest/kst4contest.flatpakrepo
|
||||
flatpak install kst4contest de.x08.KST4Contest
|
||||
```
|
||||
| Package | Content |
|
||||
|---|---|
|
||||
| [`kst4contest-bin`](https://aur.archlinux.org/packages/kst4contest-bin) | Pre-built package from the current Stable release |
|
||||
| [`kst4contest`](https://aur.archlinux.org/packages/kst4contest) | Stable release built locally from source |
|
||||
| [`kst4contest-git`](https://aur.archlinux.org/packages/kst4contest-git) | Current development state from the `main` branch |
|
||||
|
||||
#### Flatpak Channels (nightly / beta / stable)
|
||||
|
||||
The Flatpak repository provides three channels, all served from the same remote:
|
||||
|
||||
| Channel | Content | Built on |
|
||||
|---|---|---|
|
||||
| `stable` | Current stable version | Every normal release tag |
|
||||
| `beta` | Pre-release version | Tags prefixed with `beta-` |
|
||||
| `nightly` | Development build (main branch) | Every push to main / daily |
|
||||
|
||||
Add the remote once, then install the channel you want:
|
||||
For most users, `kst4contest-bin` is the straightforward option:
|
||||
|
||||
```bash
|
||||
flatpak remote-add --if-not-exists kst4contest https://praktimarc.github.io/kst4contest/kst4contest.flatpakrepo
|
||||
|
||||
# Stable (default)
|
||||
flatpak install kst4contest de.x08.KST4Contest
|
||||
|
||||
# Beta
|
||||
flatpak install kst4contest de.x08.KST4Contest//beta
|
||||
|
||||
# Nightly
|
||||
flatpak install kst4contest de.x08.KST4Contest//nightly
|
||||
yay -S kst4contest-bin
|
||||
```
|
||||
|
||||
> **Note:** Flatpak only allows one installed version per App-ID at a time. To switch channels, uninstall the current version first:
|
||||
> ```bash
|
||||
> flatpak uninstall de.x08.KST4Contest
|
||||
> flatpak install kst4contest de.x08.KST4Contest//nightly
|
||||
> ```
|
||||
To build the Stable release from source:
|
||||
|
||||
Updates work as usual with `flatpak update`.
|
||||
```bash
|
||||
yay -S kst4contest
|
||||
```
|
||||
|
||||
The `flatpakref` files for beta and stable are attached to their respective [GitHub Releases](https://github.com/praktimarc/kst4contest/releases). The nightly `flatpakref` (`de.x08.KST4Contest.nightly.flatpakref`) is available as an artifact on the [GitHub Actions](https://github.com/praktimarc/kst4contest/actions) tab (kept for 14 days) — for nightly builds, the manual remote approach above is usually more convenient.
|
||||
To build the current development state:
|
||||
|
||||
### macOS
|
||||
1. Download the DMG file for your architecture (Apple Silicon or Intel).
|
||||
```bash
|
||||
yay -S kst4contest-git
|
||||
```
|
||||
|
||||
All three packages provide the same application and are therefore defined as conflicting with one another. Install only one variant at a time.
|
||||
|
||||
AUR updates are included when the selected AUR helper checks for package updates, for example:
|
||||
|
||||
```bash
|
||||
yay -Syu
|
||||
```
|
||||
|
||||
They do not happen automatically merely because the package came from the AUR.
|
||||
|
||||
---
|
||||
|
||||
## Installing on macOS
|
||||
|
||||
> **Best-effort support:** The macOS packages are built together with the other releases, but they are not tested to the same extent as the Windows and Linux versions. Feedback is welcome; fully tested support for every macOS version and hardware variant cannot currently be guaranteed.
|
||||
|
||||
Apple Silicon Macs require the package marked `arm64`. Intel Macs require the package marked `x86_64`.
|
||||
|
||||
1. Download the appropriate DMG file.
|
||||
2. Open the DMG file.
|
||||
3. Drag `KST4Contest.app` into your **Applications** folder.
|
||||
4. On first launch, macOS may show a warning because the app is not notarised. To open it:
|
||||
- Right-click (or Control-click) on `KST4Contest.app` in Finder and choose **Open**.
|
||||
- Alternatively, go to **System Settings → Privacy & Security** and click **Open Anyway**.
|
||||
5. Run KST4Contest from your Applications folder or Launchpad.
|
||||
3. Drag `KST4Contest.app` into the **Applications** folder.
|
||||
4. Start KST4Contest from the Applications folder or Launchpad.
|
||||
|
||||
Settings are stored at `~/.praktikst/preferences.xml`.
|
||||
The application is not currently notarized by Apple. macOS may therefore block the first launch.
|
||||
|
||||
If the application came from the official GitHub Release:
|
||||
|
||||
1. Open the Applications folder in Finder.
|
||||
2. Right-click or Control-click `KST4Contest.app`.
|
||||
3. Select **Open**.
|
||||
4. Confirm the launch in the dialog that appears.
|
||||
|
||||
Alternatively, macOS may provide an **Open Anyway** button under **System Settings → Privacy & Security**.
|
||||
|
||||
---
|
||||
|
||||
## Updating
|
||||
## Where are the settings stored?
|
||||
|
||||
KST4Contest includes an **automatic update notification service**: as soon as a new version is available, a window will appear at startup with:
|
||||
- information that a new version is available,
|
||||
- a changelog,
|
||||
- the download link for the new version.
|
||||
KST4Contest stores its settings and other local working files in the user’s home directory. The application directory and the data directory are separate.
|
||||
|
||||

|
||||
| Operating system | Data directory | Settings file |
|
||||
|---|---|---|
|
||||
| Windows | `%USERPROFILE%\.praktiKST\` | `%USERPROFILE%\.praktiKST\preferences.xml` |
|
||||
| Linux | `~/.praktiKST/` | `~/.praktiKST/preferences.xml` |
|
||||
| macOS | `~/.praktiKST/` | `~/.praktiKST/preferences.xml` |
|
||||
|
||||
### Update Process
|
||||
|
||||
#### Windows
|
||||
|
||||
Currently, there is only one way to update:
|
||||
|
||||
1. Delete the old folder.
|
||||
2. Unzip the new ZIP file.
|
||||
|
||||
The settings file (`preferences.xml`) is preserved because it is stored in the user folder, not the program folder.
|
||||
|
||||
#### Linux
|
||||
|
||||
- **AppImage**: Download the new AppImage, make it executable (`chmod +x`), optionally delete the old one.
|
||||
- **Debian/Ubuntu**: `sudo apt install ./KST4Contest-v<version>-debian-amd64.deb`
|
||||
- **Fedora/RHEL**: `sudo dnf upgrade ./KST4Contest-v<version>-fedora-x86_64.rpm`
|
||||
- **Arch Linux**: `sudo pacman -U KST4Contest-v<version>-archlinux-x86_64.pkg.tar.zst`
|
||||
- **Flatpak (repository)**: `flatpak update` – updates all Flatpak apps including KST4Contest.
|
||||
|
||||
#### macOS
|
||||
|
||||
1. Download the new DMG file.
|
||||
2. Open the DMG.
|
||||
3. Drag the new `KST4Contest.app` into your **Applications** folder, replacing the old version.
|
||||
Note the spelling `.praktiKST` with an uppercase `KST`. Linux and macOS distinguish between uppercase and lowercase letters. `.praktikst` would simply be a different directory.
|
||||
|
||||
An application update does not remove this directory. Even so, creating a backup before a major version change or extensive configuration work is sensible.
|
||||
|
||||
---
|
||||
|
||||
## Known Issues at Startup
|
||||
## Updates
|
||||
|
||||
### Norton 360
|
||||
When KST4Contest starts, it checks whether a newer Stable release is available. If it finds one, it displays an information window containing:
|
||||
|
||||
Norton 360 classifies `praktiKST.exe` as dangerous (false positive). An exception must be created for the file:
|
||||
- the installed version,
|
||||
- the latest Stable version,
|
||||
- a short overview of the main changes,
|
||||
- the changelog,
|
||||
- known issues,
|
||||
- a link to the corresponding GitHub Release page.
|
||||
|
||||
1. Open Norton 360.
|
||||
2. Security → History → Find the corresponding event.
|
||||
3. Select "Restore & Add Exception".
|
||||

|
||||
|
||||
*(Reported by PE0WGA, Franz van Velzen – thank you!)*
|
||||
The update checker does not install anything. It reports the new version and opens the platform-neutral release page. From there, you must select the appropriate package for Windows, Linux or macOS.
|
||||
|
||||
### Updating Windows
|
||||
|
||||
1. Close KST4Contest.
|
||||
2. Download the new Windows ZIP package.
|
||||
3. Extract it into a new or empty directory.
|
||||
4. Start the new version.
|
||||
5. Check that the existing settings have been loaded.
|
||||
6. Remove the old application directory only after that.
|
||||
|
||||
The settings are preserved because they are stored under `%USERPROFILE%\.praktiKST`, not in the application directory.
|
||||
|
||||
### Updating an AppImage
|
||||
|
||||
1. Download the new AppImage.
|
||||
2. Make it executable.
|
||||
3. Start the new file.
|
||||
4. Remove the previous AppImage only after the new version works.
|
||||
|
||||
### Updating Debian and Ubuntu
|
||||
|
||||
```bash
|
||||
sudo apt install ./KST4Contest-v<version>-debian-amd64.deb
|
||||
```
|
||||
|
||||
### Updating Fedora
|
||||
|
||||
```bash
|
||||
sudo dnf upgrade ./KST4Contest-v<version>-fedora-x86_64.rpm
|
||||
```
|
||||
|
||||
### Updating the Arch package
|
||||
|
||||
```bash
|
||||
sudo pacman -U KST4Contest-v<version>-archlinux-x86_64.pkg.tar.zst
|
||||
```
|
||||
|
||||
### Updating an AUR package
|
||||
|
||||
Update the installed package through the selected AUR helper, for example:
|
||||
|
||||
```bash
|
||||
yay -Syu
|
||||
```
|
||||
|
||||
### Updating Flatpak
|
||||
|
||||
```bash
|
||||
flatpak update de.x08.KST4Contest
|
||||
```
|
||||
|
||||
### Updating macOS
|
||||
|
||||
1. Download the new DMG file for the appropriate architecture.
|
||||
2. Close KST4Contest.
|
||||
3. Open the DMG file.
|
||||
4. Replace `KST4Contest.app` in the Applications folder.
|
||||
5. Start the new version and check the existing settings.
|
||||
|
||||
The configuration under `~/.praktiKST` is not affected.
|
||||
|
||||
---
|
||||
|
||||
## Problems during the first launch
|
||||
|
||||
### Windows reports an unknown application
|
||||
|
||||
KST4Contest is not currently signed with a commercial Windows code-signing certificate. Windows or an additional security product may therefore warn about an unknown or rarely downloaded application.
|
||||
|
||||
Check the following first:
|
||||
|
||||
- Did the file come from the [official KST4Contest release](https://github.com/praktimarc/kst4contest/releases/latest)?
|
||||
- Does the filename match the published release?
|
||||
- Was the file downloaded completely?
|
||||
- Does the security product report a specific detection name or only a general reputation warning?
|
||||
|
||||
A warning alone is neither reliable proof of malware nor automatically a false positive. If the origin of the file is unclear, do not run it or restore it from quarantine.
|
||||
|
||||
Some users have reported quarantine messages from Norton 360 in particular. If the warning can be reproduced, please create a [GitHub issue](https://github.com/praktimarc/kst4contest/issues) and include:
|
||||
|
||||
- the KST4Contest version,
|
||||
- the complete filename,
|
||||
- the security product and its version,
|
||||
- the reported detection name,
|
||||
- a screenshot of the warning, if possible.
|
||||
|
||||
### The AppImage does not start
|
||||
|
||||
Check the executable permission first:
|
||||
|
||||
```bash
|
||||
chmod +x KST4Contest-v<version>-linux-x86_64.AppImage
|
||||
```
|
||||
|
||||
Then start the file from a terminal. Error messages shown there are usually more useful than a double-click that simply produces no visible result.
|
||||
|
||||
### macOS blocks the application
|
||||
|
||||
Use the **Open** function described under [Installing on macOS](#installing-on-macos). Before doing so, verify that the DMG file came from the official GitHub Release.
|
||||
|
||||
### The problem remains
|
||||
|
||||
First check whether the problem has already been reported under [GitHub Issues](https://github.com/praktimarc/kst4contest/issues). If it has not, create a new issue containing:
|
||||
|
||||
- the operating system and version,
|
||||
- the KST4Contest version,
|
||||
- the installation method,
|
||||
- the exact error message,
|
||||
- the steps required to reproduce the problem.
|
||||
|
||||
“It does not work” usually describes the situation accurately, but it is of limited value during diagnosis.
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 63 KiB After Width: | Height: | Size: 48 KiB |
@@ -0,0 +1,16 @@
|
||||
pkgbase = kst4contest-bin
|
||||
pkgdesc = ON4KST Chat Client for VHF/UHF contest operation (pre-built)
|
||||
pkgver = 1.41.1
|
||||
pkgrel = 1
|
||||
url = https://github.com/praktimarc/kst4contest
|
||||
arch = x86_64
|
||||
license = GPL-3.0-only
|
||||
depends = gst-plugins-base
|
||||
depends = gst-plugins-good
|
||||
provides = kst4contest
|
||||
conflicts = kst4contest
|
||||
conflicts = kst4contest-git
|
||||
source = KST4Contest-v1.41.1-archlinux-x86_64.pkg.tar.zst::https://github.com/praktimarc/kst4contest/releases/download/v1.41.1/KST4Contest-v1.41.1-archlinux-x86_64.pkg.tar.zst
|
||||
sha256sums = 8e9a53ff832920c9ef2733635b90c5a4ffcd57a2958aaa251e92bd031142c614
|
||||
|
||||
pkgname = kst4contest-bin
|
||||
@@ -0,0 +1,17 @@
|
||||
# Maintainer: Philipp Wagner <philipp@wagnersnetz.de>
|
||||
pkgname=kst4contest-bin
|
||||
pkgver=1.41.1
|
||||
pkgrel=1
|
||||
pkgdesc="ON4KST Chat Client for VHF/UHF contest operation (pre-built)"
|
||||
arch=('x86_64')
|
||||
url="https://github.com/praktimarc/kst4contest"
|
||||
license=('GPL-3.0-only')
|
||||
depends=('gst-plugins-base' 'gst-plugins-good')
|
||||
provides=('kst4contest')
|
||||
conflicts=('kst4contest' 'kst4contest-git')
|
||||
source=("KST4Contest-v${pkgver}-archlinux-${CARCH}.pkg.tar.zst::https://github.com/praktimarc/kst4contest/releases/download/v${pkgver}/KST4Contest-v${pkgver}-archlinux-${CARCH}.pkg.tar.zst")
|
||||
sha256sums=('8e9a53ff832920c9ef2733635b90c5a4ffcd57a2958aaa251e92bd031142c614')
|
||||
|
||||
package() {
|
||||
cp -a "${srcdir}/usr" "${pkgdir}/"
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
pkgbase = kst4contest-git
|
||||
pkgdesc = ON4KST Chat Client for VHF/UHF contest operation (git)
|
||||
pkgver = 1.42.0.r145.gd885924
|
||||
pkgrel = 1
|
||||
url = https://github.com/praktimarc/kst4contest
|
||||
arch = x86_64
|
||||
license = GPL-3.0-only
|
||||
makedepends = java-environment=21
|
||||
makedepends = maven
|
||||
makedepends = git
|
||||
depends = gst-plugins-base
|
||||
depends = gst-plugins-good
|
||||
provides = kst4contest
|
||||
conflicts = kst4contest
|
||||
conflicts = kst4contest-bin
|
||||
source = kst4contest::git+https://github.com/praktimarc/kst4contest.git
|
||||
sha256sums = SKIP
|
||||
|
||||
pkgname = kst4contest-git
|
||||
@@ -0,0 +1,76 @@
|
||||
# Maintainer: Philipp Wagner <philipp@wagnersnetz.de>
|
||||
pkgname=kst4contest-git
|
||||
pkgver=1.42.0.r145.gd885924
|
||||
pkgrel=1
|
||||
pkgdesc="ON4KST Chat Client for VHF/UHF contest operation (git)"
|
||||
arch=('x86_64')
|
||||
url="https://github.com/praktimarc/kst4contest"
|
||||
license=('GPL-3.0-only')
|
||||
depends=('gst-plugins-base' 'gst-plugins-good')
|
||||
makedepends=('java-environment=21' 'maven' 'git')
|
||||
provides=('kst4contest')
|
||||
conflicts=('kst4contest' 'kst4contest-bin')
|
||||
source=("kst4contest::git+https://github.com/praktimarc/kst4contest.git")
|
||||
sha256sums=('SKIP')
|
||||
|
||||
pkgver() {
|
||||
cd "${srcdir}/kst4contest"
|
||||
BASE=$(grep -m1 '<version>' pom.xml \
|
||||
| sed 's/.*<version>\(.*\)<\/version>.*/\1/' | sed 's/[-.]nightly//')
|
||||
printf '%s.r%s.g%s' "${BASE}" "$(git rev-list --count HEAD)" "$(git rev-parse --short HEAD)"
|
||||
}
|
||||
|
||||
build() {
|
||||
cd "${srcdir}/kst4contest"
|
||||
|
||||
export JAVA_HOME=$(find /usr/lib/jvm -maxdepth 1 -name 'java-21-*' -type d | head -n 1)
|
||||
export PATH="${JAVA_HOME}/bin:${PATH}"
|
||||
|
||||
mvn -B -DskipTests package dependency:copy-dependencies \
|
||||
-DincludeScope=runtime \
|
||||
-DoutputDirectory=target/dist-libs
|
||||
|
||||
cp "$(ls -t target/praktiKST-*.jar | head -n 1)" target/dist-libs/app.jar
|
||||
|
||||
mkdir -p dist
|
||||
jpackage \
|
||||
--type app-image \
|
||||
--name KST4Contest \
|
||||
--input target/dist-libs \
|
||||
--main-jar app.jar \
|
||||
--main-class kst4contest.view.Kst4ContestApplication \
|
||||
--module-path target/dist-libs \
|
||||
--add-modules javafx.controls,javafx.graphics,javafx.fxml,javafx.web,javafx.media,java.sql,java.net.http,jdk.crypto.ec \
|
||||
--dest dist
|
||||
}
|
||||
|
||||
package() {
|
||||
cd "${srcdir}/kst4contest"
|
||||
|
||||
install -dm755 "${pkgdir}/usr/lib/KST4Contest"
|
||||
cp -a dist/KST4Contest/. "${pkgdir}/usr/lib/KST4Contest/"
|
||||
|
||||
install -dm755 "${pkgdir}/usr/bin"
|
||||
printf '#!/bin/sh\nexec /usr/lib/KST4Contest/bin/KST4Contest "$@"\n' \
|
||||
> "${pkgdir}/usr/bin/KST4Contest"
|
||||
chmod 755 "${pkgdir}/usr/bin/KST4Contest"
|
||||
|
||||
install -dm755 "${pkgdir}/usr/share/applications"
|
||||
cat > "${pkgdir}/usr/share/applications/KST4Contest.desktop" << 'EOF'
|
||||
[Desktop Entry]
|
||||
Type=Application
|
||||
Name=KST4Contest
|
||||
Comment=ON4KST Chat Client for VHF/UHF contest operation
|
||||
Exec=KST4Contest
|
||||
Icon=KST4Contest
|
||||
Categories=Network;HamRadio;
|
||||
Terminal=false
|
||||
EOF
|
||||
|
||||
if [[ -f "${pkgdir}/usr/lib/KST4Contest/lib/KST4Contest.png" ]]; then
|
||||
install -Dm644 "${pkgdir}/usr/lib/KST4Contest/lib/KST4Contest.png" \
|
||||
"${pkgdir}/usr/share/icons/hicolor/256x256/apps/KST4Contest.png"
|
||||
fi
|
||||
|
||||
install -Dm644 LICENSE "${pkgdir}/usr/share/licenses/${pkgname}/LICENSE"
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
pkgbase = kst4contest
|
||||
pkgdesc = ON4KST Chat Client for VHF/UHF contest operation
|
||||
pkgver = 1.41.1
|
||||
pkgrel = 1
|
||||
url = https://github.com/praktimarc/kst4contest
|
||||
arch = x86_64
|
||||
license = GPL-3.0-only
|
||||
makedepends = java-environment=21
|
||||
makedepends = maven
|
||||
depends = gst-plugins-base
|
||||
depends = gst-plugins-good
|
||||
provides = kst4contest
|
||||
conflicts = kst4contest-bin
|
||||
conflicts = kst4contest-git
|
||||
source = kst4contest-1.41.1.tar.gz::https://github.com/praktimarc/kst4contest/archive/refs/tags/v1.41.1.tar.gz
|
||||
sha256sums = e96207a2d3fee19d35e34717f5312beb28bb087c040164e352337e749ce53b8d
|
||||
|
||||
pkgname = kst4contest
|
||||
@@ -0,0 +1,69 @@
|
||||
# Maintainer: Philipp Wagner <philipp@wagnersnetz.de>
|
||||
pkgname=kst4contest
|
||||
pkgver=1.41.1
|
||||
pkgrel=1
|
||||
pkgdesc="ON4KST Chat Client for VHF/UHF contest operation"
|
||||
arch=('x86_64')
|
||||
url="https://github.com/praktimarc/kst4contest"
|
||||
license=('GPL-3.0-only')
|
||||
depends=('gst-plugins-base' 'gst-plugins-good')
|
||||
makedepends=('java-environment=21' 'maven')
|
||||
provides=('kst4contest')
|
||||
conflicts=('kst4contest-bin' 'kst4contest-git')
|
||||
source=("${pkgname}-${pkgver}.tar.gz::https://github.com/praktimarc/kst4contest/archive/refs/tags/v${pkgver}.tar.gz")
|
||||
sha256sums=('e96207a2d3fee19d35e34717f5312beb28bb087c040164e352337e749ce53b8d')
|
||||
|
||||
build() {
|
||||
cd "${srcdir}/kst4contest-${pkgver}"
|
||||
|
||||
export JAVA_HOME=$(find /usr/lib/jvm -maxdepth 1 -name 'java-21-*' -type d | head -n 1)
|
||||
export PATH="${JAVA_HOME}/bin:${PATH}"
|
||||
|
||||
mvn -B -DskipTests package dependency:copy-dependencies \
|
||||
-DincludeScope=runtime \
|
||||
-DoutputDirectory=target/dist-libs
|
||||
|
||||
cp "$(ls -t target/praktiKST-*.jar | head -n 1)" target/dist-libs/app.jar
|
||||
|
||||
mkdir -p dist
|
||||
jpackage \
|
||||
--type app-image \
|
||||
--name KST4Contest \
|
||||
--input target/dist-libs \
|
||||
--main-jar app.jar \
|
||||
--main-class kst4contest.view.Kst4ContestApplication \
|
||||
--module-path target/dist-libs \
|
||||
--add-modules javafx.controls,javafx.graphics,javafx.fxml,javafx.web,javafx.media,java.sql,java.net.http,jdk.crypto.ec \
|
||||
--dest dist
|
||||
}
|
||||
|
||||
package() {
|
||||
cd "${srcdir}/kst4contest-${pkgver}"
|
||||
|
||||
install -dm755 "${pkgdir}/usr/lib/KST4Contest"
|
||||
cp -a dist/KST4Contest/. "${pkgdir}/usr/lib/KST4Contest/"
|
||||
|
||||
install -dm755 "${pkgdir}/usr/bin"
|
||||
printf '#!/bin/sh\nexec /usr/lib/KST4Contest/bin/KST4Contest "$@"\n' \
|
||||
> "${pkgdir}/usr/bin/KST4Contest"
|
||||
chmod 755 "${pkgdir}/usr/bin/KST4Contest"
|
||||
|
||||
install -dm755 "${pkgdir}/usr/share/applications"
|
||||
cat > "${pkgdir}/usr/share/applications/KST4Contest.desktop" << 'EOF'
|
||||
[Desktop Entry]
|
||||
Type=Application
|
||||
Name=KST4Contest
|
||||
Comment=ON4KST Chat Client for VHF/UHF contest operation
|
||||
Exec=KST4Contest
|
||||
Icon=KST4Contest
|
||||
Categories=Network;HamRadio;
|
||||
Terminal=false
|
||||
EOF
|
||||
|
||||
if [[ -f "${pkgdir}/usr/lib/KST4Contest/lib/KST4Contest.png" ]]; then
|
||||
install -Dm644 "${pkgdir}/usr/lib/KST4Contest/lib/KST4Contest.png" \
|
||||
"${pkgdir}/usr/share/icons/hicolor/256x256/apps/KST4Contest.png"
|
||||
fi
|
||||
|
||||
install -Dm644 LICENSE "${pkgdir}/usr/share/licenses/${pkgname}/LICENSE"
|
||||
}
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
<groupId>de.x08</groupId>
|
||||
<artifactId>praktiKST</artifactId>
|
||||
<version>1.41.0-nightly</version>
|
||||
<version>1.42.0-nightly</version>
|
||||
|
||||
<name>praktiKST</name>
|
||||
|
||||
@@ -24,12 +24,12 @@
|
||||
<launcher>${project.artifactId}</launcher>
|
||||
<appName>${project.artifactId}</appName>
|
||||
<main.class>kst4contest.view.Kst4ContestApplication</main.class>
|
||||
<java.version>17</java.version>
|
||||
<java.version>21</java.version>
|
||||
<required.maven.version>3.6.3</required.maven.version>
|
||||
<jar.filename>${project.artifactId}-${project.version}</jar.filename>
|
||||
|
||||
<!-- Dependency versions -->
|
||||
<javafx.version>19.0.2.1</javafx.version>
|
||||
<javafx.version>21.0.5</javafx.version>
|
||||
<jetbrains.annotations.version>24.0.1</jetbrains.annotations.version>
|
||||
<junit.version>5.10.1</junit.version>
|
||||
<lombok.version>1.18.44</lombok.version>
|
||||
@@ -50,8 +50,8 @@
|
||||
<maven.wrapper.plugin>3.2.0</maven.wrapper.plugin>
|
||||
<moditect.maven.plugin>1.0.0.RC2</moditect.maven.plugin>
|
||||
<jpackage.maven.plugin>0.1.3</jpackage.maven.plugin>
|
||||
<maven.pmd.version>3.21.2</maven.pmd.version>
|
||||
<pmd.version>6.55.0</pmd.version>
|
||||
<maven.pmd.version>3.28.0</maven.pmd.version>
|
||||
<pmd.version>7.17.0</pmd.version>
|
||||
<codehaus.version.plugin>2.16.1</codehaus.version.plugin>
|
||||
<javafx.maven.plugin>0.0.8</javafx.maven.plugin>
|
||||
<spotbugs.maven.plugin>4.9.8.2</spotbugs.maven.plugin>
|
||||
@@ -93,6 +93,13 @@
|
||||
<version>${javafx.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- JLayer: pure-Java MP3 decoder, used instead of JavaFX Media for Flatpak audio compatibility -->
|
||||
<dependency>
|
||||
<groupId>javazoom</groupId>
|
||||
<artifactId>jlayer</artifactId>
|
||||
<version>1.0.1</version>
|
||||
</dependency>
|
||||
|
||||
<!-- SQLite -->
|
||||
<dependency>
|
||||
<groupId>org.xerial</groupId>
|
||||
@@ -280,30 +287,7 @@
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-pmd-plugin</artifactId>
|
||||
<version>${maven.pmd.version}</version>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>net.sourceforge.pmd</groupId>
|
||||
<artifactId>pmd-core</artifactId>
|
||||
<version>${pmd.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>net.sourceforge.pmd</groupId>
|
||||
<artifactId>pmd-java</artifactId>
|
||||
<version>${pmd.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>net.sourceforge.pmd</groupId>
|
||||
<artifactId>pmd-javascript</artifactId>
|
||||
<version>${pmd.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>net.sourceforge.pmd</groupId>
|
||||
<artifactId>pmd-jsp</artifactId>
|
||||
<version>${pmd.version}</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
<configuration>
|
||||
<sourceEncoding>${project.build.sourceEncoding}</sourceEncoding>
|
||||
<minimumTokens>100</minimumTokens>
|
||||
<targetJdk>${java.version}</targetJdk>
|
||||
<linkXRef>false</linkXRef>
|
||||
@@ -436,6 +420,7 @@
|
||||
<addmodule>javafx.graphics</addmodule>
|
||||
<addmodule>javafx.fxml</addmodule>
|
||||
<addmodule>javafx.web</addmodule>
|
||||
<addmodule>javafx.media</addmodule>
|
||||
<addmodule>java.sql</addmodule>
|
||||
<addmodule>java.net.http</addmodule>
|
||||
<addmodule>jdk.crypto.ec</addmodule>
|
||||
|
||||
@@ -18,11 +18,18 @@ public class ApplicationConstants {
|
||||
public static final String APPLICATION_NAME = "praktiKST";
|
||||
|
||||
/**
|
||||
* Name of file to store preferences in.
|
||||
* Version shown to the user and used for semantic version comparison.
|
||||
*/
|
||||
public static final double APPLICATION_CURRENTVERSIONNUMBER = 1.41;
|
||||
public static final String APPLICATION_CURRENT_VERSION = "1.42";
|
||||
|
||||
public static final String VERSIONINFOURLFORUPDATES_KST4CONTEST = "https://do5amf.funkerportal.de/kst4ContestVersionInfo.xml";
|
||||
/**
|
||||
* Legacy numeric representation used only while older update feeds and
|
||||
* application versions still exist.
|
||||
*/
|
||||
@Deprecated
|
||||
public static final double APPLICATION_CURRENTVERSIONNUMBER = 1.42;
|
||||
|
||||
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";
|
||||
|
||||
+164
-116
@@ -1,150 +1,198 @@
|
||||
package kst4contest.controller;
|
||||
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.DatagramPacket;
|
||||
import java.net.DatagramSocket;
|
||||
import java.net.InetAddress;
|
||||
import java.net.NoRouteToHostException;
|
||||
import java.net.SocketException;
|
||||
import java.net.UnknownHostException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.List;
|
||||
import java.util.TimerTask;
|
||||
import java.util.logging.Level;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
import javafx.collections.ObservableList;
|
||||
import kst4contest.locatorUtils.Location;
|
||||
import kst4contest.model.ChatMember;
|
||||
|
||||
|
||||
/**
|
||||
* Sends periodical path requests and an AirScout watchlist for the currently
|
||||
* active ON4KST stations.
|
||||
*/
|
||||
public class AirScoutPeriodicalAPReflectionInquirerTask extends TimerTask {
|
||||
|
||||
private static final Logger LOGGER = Logger.getLogger(AirScoutPeriodicalAPReflectionInquirerTask.class.getName());
|
||||
private ChatController client;
|
||||
private static final Logger LOGGER = Logger.getLogger(
|
||||
AirScoutPeriodicalAPReflectionInquirerTask.class.getName()
|
||||
);
|
||||
|
||||
public AirScoutPeriodicalAPReflectionInquirerTask(ChatController client) {
|
||||
private static final String BROADCAST_ADDRESS = "255.255.255.255";
|
||||
|
||||
private final ChatController client;
|
||||
|
||||
public AirScoutPeriodicalAPReflectionInquirerTask(
|
||||
ChatController client
|
||||
) {
|
||||
this.client = client;
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
|
||||
Thread.currentThread().setName("AirscoutPeriodicalReflectionInquirierTask");
|
||||
Thread.currentThread().setName(
|
||||
"AirscoutPeriodicalReflectionInquirerTask"
|
||||
);
|
||||
|
||||
String KSTClientsNameForQuery = this.client.getChatPreferences().getAirScout_asClientNameString();
|
||||
String ASServerNameStringForAnswer = this.client.getChatPreferences().getAirScout_asServerNameString();
|
||||
|
||||
//TODO: Manage prefixes kst and as via preferences file and instance
|
||||
//TODO: Check if locator is changeable via the preferences object, need to be correct if it changes
|
||||
DatagramSocket dsocket;
|
||||
|
||||
// String prefix_asSetpath ="ASSETPATH: \"KST\" \"AS\" "; //working original
|
||||
// String prefix_asWatchList = "ASWATCHLIST: \"KST\" \"AS\" "; //working original
|
||||
|
||||
String prefix_asSetpath ="ASSETPATH: \"" + this.client.getChatPreferences().getAirScout_asClientNameString() + "\" \"" + this.client.getChatPreferences().getAirScout_asServerNameString() + "\" ";
|
||||
String prefix_asWatchList = "ASWATCHLIST: \""+ this.client.getChatPreferences().getAirScout_asClientNameString()+ "\" \"" + this.client.getChatPreferences().getAirScout_asServerNameString() + "\" ";
|
||||
|
||||
String bandString = "1440000"; //TODO: this must variable in case of higher bands! ... default: 1440000
|
||||
// String myCallAndMyLocString = this.client.getChatPreferences().getStn_loginCallSign() + "," + this.client.getChatPreferences().getStn_loginLocatorMainCat(); //before fix 1.266
|
||||
|
||||
|
||||
String ownCallSign = this.client.getChatPreferences().getStn_loginCallSign();
|
||||
try {
|
||||
if (this.client.getChatPreferences().getStn_loginCallSign().contains("-")) {
|
||||
ownCallSign = this.client.getChatPreferences().getStn_loginCallSign().split("-")[0];
|
||||
} else {
|
||||
ownCallSign = this.client.getChatPreferences().getStn_loginCallSign();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
LOGGER.log(Level.SEVERE, "[ASPERIODICAL] Error parsing callsign", e);
|
||||
}
|
||||
String myCallAndMyLocString = ownCallSign + "," + this.client.getChatPreferences().getStn_loginLocatorMainCat(); //bugfix, Airscout do not process 9A1W-2 but 9A1W like formatted calls
|
||||
|
||||
|
||||
String suffix = ""; //"FOREIGNCALL,FOREIGNLOC " -- dont forget the space at the end!!!
|
||||
String asWatchListString = prefix_asWatchList + bandString + "," + myCallAndMyLocString;
|
||||
String asWatchListStringSuffix = asWatchListString;
|
||||
|
||||
String host = "255.255.255.255";
|
||||
// int port = 9872;
|
||||
|
||||
int port = client.getChatPreferences().getAirScout_asCommunicationPort();
|
||||
// byte[] message = "ASSETPATH: \"KST\" \"AS\" 1440000,DO5AMF,JN49GL,OK1MZM,JN89IW ".getBytes(); Original, ging
|
||||
InetAddress address;
|
||||
|
||||
|
||||
/**
|
||||
* Iterate over chatmemberlist and asking airscout for plane reflection information
|
||||
* To avoid a concurrentmodifyexception, we have to convert the original list to an array at first
|
||||
* since the iterator brakes if the list changing during the iteration time
|
||||
/*
|
||||
* Keep the scheduled task installed so that AirScout can be enabled at
|
||||
* runtime, but do not send anything while the integration is disabled.
|
||||
*/
|
||||
ObservableList<ChatMember> praktiKSTActiveUserList = this.client.getLst_chatMemberList();
|
||||
|
||||
|
||||
ChatMember[] ary_threadSafeChatMemberArray = new ChatMember[praktiKSTActiveUserList.size()];
|
||||
praktiKSTActiveUserList.toArray(ary_threadSafeChatMemberArray);
|
||||
|
||||
for (ChatMember i : ary_threadSafeChatMemberArray) {
|
||||
if (!client.getChatPreferences().isAirScout_asUDPListenerEnabled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (i.getQrb() < this.client.getChatPreferences().getStn_maxQRBDefault())
|
||||
//Here: check if maximum distance to the chatmember is reached, only ask AS if distance is lower!
|
||||
//this counts for AS request and Aswatchlist
|
||||
{
|
||||
suffix = i.getCallSign() + "," + i.getQra() + " ";
|
||||
String clientIdentifier =
|
||||
client.getChatPreferences().getAirScout_asClientNameString();
|
||||
String serverIdentifier =
|
||||
client.getChatPreferences().getAirScout_asServerNameString();
|
||||
String bandValue =
|
||||
client.getChatPreferences().getAirScout_asBandString();
|
||||
|
||||
String queryStringToAirScout = "";
|
||||
String ownCallSign = normalizeOwnCallSign(
|
||||
client.getChatPreferences().getStn_loginCallSign()
|
||||
);
|
||||
String ownLocator =
|
||||
client.getChatPreferences().getStn_loginLocatorMainCat();
|
||||
|
||||
queryStringToAirScout += prefix_asSetpath + bandString + "," + myCallAndMyLocString + "," + suffix;
|
||||
if (ownCallSign == null
|
||||
|| ownCallSign.isBlank()
|
||||
|| ownLocator == null
|
||||
|| ownLocator.isBlank()) {
|
||||
LOGGER.warning(
|
||||
"AirScout queries were skipped because the own callsign "
|
||||
+ "or locator is missing."
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
byte[] queryStringToAirScoutMSG = queryStringToAirScout.getBytes();
|
||||
String setPathPrefix =
|
||||
"ASSETPATH: \"" + clientIdentifier
|
||||
+ "\" \"" + serverIdentifier + "\" ";
|
||||
|
||||
try {
|
||||
address = InetAddress.getByName("255.255.255.255");
|
||||
DatagramPacket packet = new DatagramPacket(queryStringToAirScoutMSG, queryStringToAirScoutMSG.length, address, port);
|
||||
dsocket = new DatagramSocket();
|
||||
dsocket.setBroadcast(true);
|
||||
dsocket.send(packet);
|
||||
dsocket.close();
|
||||
} catch (UnknownHostException e1) {
|
||||
LOGGER.log(Level.SEVERE, "[ASPERIODICAL] Unknown host", e1);
|
||||
} catch (NoRouteToHostException e) {
|
||||
LOGGER.log(Level.SEVERE, "[ASPERIODICAL] No route to host", e);
|
||||
} catch (SocketException e) {
|
||||
LOGGER.log(Level.SEVERE, "[ASPERIODICAL] Socket error", e);
|
||||
} catch (IOException e) {
|
||||
LOGGER.log(Level.SEVERE, "[ASPERIODICAL] IO error sending query", e);
|
||||
String watchListPrefix =
|
||||
"ASWATCHLIST: \"" + clientIdentifier
|
||||
+ "\" \"" + serverIdentifier + "\" ";
|
||||
|
||||
String ownStation = ownCallSign + "," + ownLocator;
|
||||
StringBuilder watchListMessage = new StringBuilder(
|
||||
watchListPrefix
|
||||
+ bandValue
|
||||
+ ","
|
||||
+ ownStation
|
||||
);
|
||||
|
||||
List<ChatMember> activeMembers = client.snapshotChatMembers();
|
||||
int port = client.getChatPreferences()
|
||||
.getAirScout_asCommunicationPort();
|
||||
|
||||
try (
|
||||
DatagramSocket socket = new DatagramSocket()
|
||||
) {
|
||||
socket.setBroadcast(true);
|
||||
InetAddress broadcastAddress =
|
||||
InetAddress.getByName(BROADCAST_ADDRESS);
|
||||
|
||||
for (ChatMember member : activeMembers) {
|
||||
if (!isUsableAirScoutTarget(member)) {
|
||||
continue;
|
||||
}
|
||||
// System.out.println("[ASUDPTask, info:] sent query " + queryStringToAirScout);
|
||||
|
||||
asWatchListStringSuffix += "," + i.getCallSign() + "," + i.getQra();
|
||||
if (member.getQrb()
|
||||
>= client.getChatPreferences().getStn_maxQRBDefault()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
String targetStation =
|
||||
member.getCallSign() + "," + member.getQra();
|
||||
|
||||
String pathQuery =
|
||||
setPathPrefix
|
||||
+ bandValue
|
||||
+ ","
|
||||
+ ownStation
|
||||
+ ","
|
||||
+ targetStation
|
||||
+ " ";
|
||||
|
||||
sendPacket(
|
||||
socket,
|
||||
broadcastAddress,
|
||||
port,
|
||||
pathQuery
|
||||
);
|
||||
|
||||
watchListMessage
|
||||
.append(",")
|
||||
.append(targetStation);
|
||||
}
|
||||
|
||||
watchListMessage.append(" ");
|
||||
|
||||
sendPacket(
|
||||
socket,
|
||||
broadcastAddress,
|
||||
port,
|
||||
watchListMessage.toString()
|
||||
);
|
||||
} catch (IOException exception) {
|
||||
LOGGER.log(
|
||||
Level.WARNING,
|
||||
"Could not send the periodical AirScout queries.",
|
||||
exception
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* As next we will set the ASWatchlist. All stations in chat will be watched by airscout causing following code.\n\n
|
||||
* ASWATCHLIST: "KST" "AS" 4320000,DO5AMF,JN49GL,DF9QX,JO42HD,DG2KBC,JN58MI,DJ0PY,JO32MF,DL1YDI,JO42FA,DL6BF,JO32QI,F1NZC,JN15MR,F4TXU,JN23CX,F5GHP,IN96LE,F6HTJ,JN12KQ,G0GGG,IO81VE,G0JCC,IO82MA,G0JDL,JO02SI,G0MBL,JO01QH,G4AEP,IO91MB,G4CLA,IO92JL,G4DCV,IO91OF,G4LOH,IO70JC,G4MKF,IO91HJ,G4TRA,IO81WN,G8GXP,IO93FQ,G8VHI,IO92FM,GW0RHC,IO71UN,HA4ND,JN97MJ,I5/HB9SJV/P,JN52JS,IW2DAL,JN45NN,OK1FPR,JO80CE,OK6M,JN99CR,OV3T,JO46CM,OZ2M,JO65FR,PA0V,JO33II,PA2RU,JO32LT,PA3DOL,JO22MT,PA9R,JO22JK,PE1EVX,JO22MP,S51AT,JN75GW,SM7KOJ,JO66ND,SP9TTG,JO90KW�
|
||||
* The watchlist-String is bult by the for loop which builds the AP queries
|
||||
*/
|
||||
asWatchListStringSuffix += " ";
|
||||
|
||||
byte[] queryStringToAirScoutMSG = asWatchListStringSuffix.getBytes();
|
||||
|
||||
try {
|
||||
address = InetAddress.getByName("255.255.255.255");
|
||||
DatagramPacket packet = new DatagramPacket(queryStringToAirScoutMSG, queryStringToAirScoutMSG.length, address, port);
|
||||
dsocket = new DatagramSocket();
|
||||
dsocket.setBroadcast(true);
|
||||
dsocket.send(packet);
|
||||
dsocket.close();
|
||||
} catch (IOException e) {
|
||||
LOGGER.log(Level.SEVERE, "[ASPERIODICAL] IO error sending watchlist", e);
|
||||
}
|
||||
|
||||
// System.out.println("[ASUDPTask, info:] set watchlist: " + asWatchListStringSuffix);
|
||||
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
/**
|
||||
* Removes the ON4KST login suffix because AirScout expects the actual
|
||||
* station callsign, for example 9A1W instead of 9A1W-2.
|
||||
*
|
||||
* @param callSign configured ON4KST login callsign
|
||||
* @return callsign without an ON4KST login suffix
|
||||
*/
|
||||
private String normalizeOwnCallSign(String callSign) {
|
||||
if (callSign == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
String normalizedCallSign = callSign.trim();
|
||||
int suffixSeparator = normalizedCallSign.indexOf("-");
|
||||
|
||||
if (suffixSeparator > 0) {
|
||||
return normalizedCallSign.substring(0, suffixSeparator);
|
||||
}
|
||||
|
||||
return normalizedCallSign;
|
||||
}
|
||||
|
||||
private boolean isUsableAirScoutTarget(ChatMember member) {
|
||||
return member != null
|
||||
&& member.getCallSign() != null
|
||||
&& !member.getCallSign().isBlank()
|
||||
&& member.getQra() != null
|
||||
&& !member.getQra().isBlank();
|
||||
}
|
||||
|
||||
private void sendPacket(
|
||||
DatagramSocket socket,
|
||||
InetAddress address,
|
||||
int port,
|
||||
String message
|
||||
) throws IOException {
|
||||
byte[] payload = message.getBytes(StandardCharsets.UTF_8);
|
||||
|
||||
DatagramPacket packet = new DatagramPacket(
|
||||
payload,
|
||||
payload.length,
|
||||
address,
|
||||
port
|
||||
);
|
||||
|
||||
socket.send(packet);
|
||||
}
|
||||
}
|
||||
@@ -48,7 +48,8 @@ public class BeaconTask extends TimerTask {
|
||||
replaceVariables = replaceVariables.replaceAll("MYCALL", this.chatController.getChatPreferences().getStn_loginCallSign());
|
||||
replaceVariables = replaceVariables.replaceAll("MYLOCATOR", this.chatController.getChatPreferences().getStn_loginLocatorMainCat());
|
||||
replaceVariables = replaceVariables.replaceAll("MYQTF", this.chatController.getChatPreferences().getActualQTF().getValue() + "");
|
||||
replaceVariables = replaceVariables.replaceAll("SECONDQRG", this.chatController.getChatPreferences().getActualQTF().getValue() + "");
|
||||
replaceVariables = replaceVariables.replaceAll("SECONDQRG", this.chatController.getChatPreferences().getMYQRGSecondCat().getValue() + "");
|
||||
|
||||
|
||||
|
||||
beaconMSG.setMessageText(
|
||||
|
||||
@@ -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.*;
|
||||
@@ -31,6 +32,8 @@ import java.util.concurrent.ScheduledExecutorService;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Predicate;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -161,14 +164,16 @@ public class ChatController implements ThreadStatusCallback, PstRotatorEventList
|
||||
|
||||
|
||||
|
||||
public void initRotor() {
|
||||
// Beispiel: PSTRotator läuft lokal auf Port 12060
|
||||
// Der Client wird automatisch auf 12061 hören.
|
||||
rotatorClient = new PstRotatorClient("127.0.0.1", 12000, this, this); //TODO: IP anpassen, Port auch aus den prefs holen, default 12000
|
||||
public void initRotor() {
|
||||
rotatorClient = new PstRotatorClient(
|
||||
chatPreferences.getStn_pstRotatorHost(),
|
||||
chatPreferences.getStn_pstRotatorPort(),
|
||||
this,
|
||||
this
|
||||
);
|
||||
|
||||
// Startet den Thread und das Polling
|
||||
rotatorClient.start();
|
||||
}
|
||||
rotatorClient.start();
|
||||
}
|
||||
|
||||
/**
|
||||
* sets rotator to "AZ DEGREE" by button click <br/><br/>
|
||||
@@ -318,33 +323,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 +357,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;
|
||||
}
|
||||
|
||||
@@ -474,66 +473,106 @@ public class ChatController implements ThreadStatusCallback, PstRotatorEventList
|
||||
* @param remoteChatMember with callsign of the foreign station
|
||||
*/
|
||||
|
||||
public void airScout_SendAsShowPathPacket(ChatMember remoteChatMember) {
|
||||
/**
|
||||
* Requests AirScout to display the path between the own station and the
|
||||
* selected remote station.
|
||||
*
|
||||
* The configured server identifier, client identifier, band and UDP port are
|
||||
* used for every request. This allows several AirScout servers or clients to
|
||||
* coexist in the same network.
|
||||
*
|
||||
* @param remoteChatMember selected remote station
|
||||
*/
|
||||
public void airScout_SendAsShowPathPacket(
|
||||
ChatMember remoteChatMember
|
||||
) {
|
||||
if (!chatPreferences.isAirScout_asUDPListenerEnabled()) {
|
||||
System.out.println(
|
||||
"[AirScout, info]: Show-path request ignored because "
|
||||
+ "the AirScout integration is disabled."
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
DatagramSocket dsocket;
|
||||
if (remoteChatMember == null
|
||||
|| remoteChatMember.getCallSign() == null
|
||||
|| remoteChatMember.getCallSign().isBlank()
|
||||
|| remoteChatMember.getQra() == null
|
||||
|| remoteChatMember.getQra().isBlank()) {
|
||||
System.out.println(
|
||||
"[AirScout, warning]: Show-path request ignored because "
|
||||
+ "the selected station has no usable callsign or locator."
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
String prefix_asSetpath ="ASSHOWPATH: \""+ this.getChatPreferences().getAirScout_asClientNameString()+ "\" \"" + this.getChatPreferences().getAirScout_asServerNameString() + "\" ";
|
||||
String ownCallSign = chatPreferences.getStn_loginCallSign();
|
||||
if (ownCallSign == null || ownCallSign.isBlank()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// String prefix_asSetpath ="ASSHOWPATH: \"KST\" \"AS\" "; Old hard coded
|
||||
String bandString = "1440000";
|
||||
// String myCallAndMyLocString = chatPreferences.getStn_loginCallSign() + "," + chatPreferences.getStn_loginLocatorMainCat(); // original b4 bugfix 1266
|
||||
String remoteCallAndLocString = remoteChatMember.getCallSign() +"," + remoteChatMember.getQra();
|
||||
int suffixSeparator = ownCallSign.indexOf("-");
|
||||
if (suffixSeparator > 0) {
|
||||
ownCallSign = ownCallSign.substring(0, suffixSeparator);
|
||||
}
|
||||
|
||||
String ownCallSign="";
|
||||
try {
|
||||
if (chatPreferences.getStn_loginCallSign().contains("-")) {
|
||||
ownCallSign = chatPreferences.getStn_loginCallSign().split("-")[0];
|
||||
} else {
|
||||
ownCallSign = chatPreferences.getStn_loginCallSign();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
System.out.println("[ASPERIODICAL, Error]: " + e.getMessage());
|
||||
}
|
||||
String ownLocator =
|
||||
chatPreferences.getStn_loginLocatorMainCat();
|
||||
|
||||
String myCallAndMyLocString = ownCallSign + "," + chatPreferences.getStn_loginLocatorMainCat(); // original b4 bugfix 1266
|
||||
if (ownLocator == null || ownLocator.isBlank()) {
|
||||
return;
|
||||
}
|
||||
|
||||
String host = "255.255.255.255";
|
||||
// int port = 9872;
|
||||
int port = chatPreferences.getAirScout_asCommunicationPort();
|
||||
// System.out.println("<<<<<<<<<<<<<<<<<<<<ASPERI: " + port);
|
||||
String clientIdentifier =
|
||||
chatPreferences.getAirScout_asClientNameString();
|
||||
String serverIdentifier =
|
||||
chatPreferences.getAirScout_asServerNameString();
|
||||
String bandValue =
|
||||
chatPreferences.getAirScout_asBandString();
|
||||
int port =
|
||||
chatPreferences.getAirScout_asCommunicationPort();
|
||||
|
||||
// byte[] message = "ASSETPATH: \"KST\" \"AS\" 1440000,DO5AMF,JN49GL,OK1MZM,JN89IW ".getBytes(); Original, ging
|
||||
InetAddress address;
|
||||
String query =
|
||||
"ASSHOWPATH: \""
|
||||
+ clientIdentifier
|
||||
+ "\" \""
|
||||
+ serverIdentifier
|
||||
+ "\" "
|
||||
+ bandValue
|
||||
+ ","
|
||||
+ ownCallSign
|
||||
+ ","
|
||||
+ ownLocator
|
||||
+ ","
|
||||
+ remoteChatMember.getCallSign()
|
||||
+ ","
|
||||
+ remoteChatMember.getQra()
|
||||
+ " ";
|
||||
|
||||
String queryStringToAirScout = "";
|
||||
byte[] payload = query.getBytes(StandardCharsets.UTF_8);
|
||||
|
||||
queryStringToAirScout += prefix_asSetpath + bandString + "," + myCallAndMyLocString + "," + remoteCallAndLocString+ "Å";
|
||||
try (
|
||||
DatagramSocket socket = new DatagramSocket()
|
||||
) {
|
||||
socket.setBroadcast(true);
|
||||
|
||||
byte[] queryStringToAirScoutMSG = queryStringToAirScout.getBytes();
|
||||
|
||||
try {
|
||||
address = InetAddress.getByName("255.255.255.255");
|
||||
DatagramPacket packet = new DatagramPacket(queryStringToAirScoutMSG, queryStringToAirScoutMSG.length, address, port);
|
||||
dsocket = new DatagramSocket();
|
||||
dsocket.setBroadcast(true);
|
||||
dsocket.send(packet);
|
||||
dsocket.close();
|
||||
} catch (UnknownHostException e1) {
|
||||
// TODO Auto-generated catch block
|
||||
e1.printStackTrace();
|
||||
} catch (NoRouteToHostException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
catch (SocketException e) {
|
||||
// TODO Auto-generated catch block
|
||||
e.printStackTrace();
|
||||
} catch (IOException e) {
|
||||
// TODO Auto-generated catch block
|
||||
e.printStackTrace();
|
||||
}
|
||||
InetAddress address =
|
||||
InetAddress.getByName("255.255.255.255");
|
||||
|
||||
DatagramPacket packet = new DatagramPacket(
|
||||
payload,
|
||||
payload.length,
|
||||
address,
|
||||
port
|
||||
);
|
||||
|
||||
socket.send(packet);
|
||||
} catch (IOException exception) {
|
||||
System.out.println(
|
||||
"[AirScout, error]: Could not send show-path request: "
|
||||
+ exception.getMessage()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -584,7 +623,7 @@ public class ChatController implements ThreadStatusCallback, PstRotatorEventList
|
||||
|
||||
stopScoreScheduler();
|
||||
|
||||
this.dxClusterServer.stop();
|
||||
stopDxClusterServer();
|
||||
|
||||
this.setDisconnectionPerformedByUser(true);
|
||||
|
||||
@@ -645,7 +684,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);
|
||||
@@ -693,8 +733,6 @@ public class ChatController implements ThreadStatusCallback, PstRotatorEventList
|
||||
|
||||
dbHandler.closeDBConnection();
|
||||
|
||||
dxClusterServer.stop();
|
||||
|
||||
rotatorClient.stopRotor();
|
||||
rotatorClient.stop();
|
||||
|
||||
@@ -713,7 +751,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 +997,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 +1007,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 +1066,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() {
|
||||
@@ -1115,7 +1171,9 @@ public class ChatController implements ThreadStatusCallback, PstRotatorEventList
|
||||
// mine
|
||||
private FilteredList<ChatMessage> lst_toOtherMessageList = new FilteredList<>(lst_globalChatMessageList);
|
||||
|
||||
private ObservableList<String> lstNotify_QSOSniffer_sniffedCallSignList = FXCollections.observableArrayList();
|
||||
// private ObservableList<String> lstNotify_QSOSniffer_sniffedCallSignList = FXCollections.observableArrayList();
|
||||
private ObservableList<String>
|
||||
lstNotify_QSOSniffer_sniffedCallSignList;
|
||||
/**
|
||||
* we do some trick here with the chatmemberlist to not make it neccessary to change all boolean properties if the
|
||||
* chatmember object to observables. We trigger the list for changes on an object which we change whenever a list
|
||||
@@ -1126,13 +1184,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 +1233,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;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -1585,6 +1905,9 @@ public class ChatController implements ThreadStatusCallback, PstRotatorEventList
|
||||
chatPreferences = new ChatPreferences();
|
||||
chatPreferences.readPreferencesFromXmlFile();
|
||||
// this.statusListener = listener;
|
||||
lstNotify_QSOSniffer_sniffedCallSignList =
|
||||
chatPreferences
|
||||
.getLstNotify_QSOSniffer_sniffedCallSignList();
|
||||
|
||||
String dnsFromPrefs = chatPreferences.getStn_on4kstServersDns();
|
||||
if (dnsFromPrefs != null && !dnsFromPrefs.isEmpty()) {
|
||||
@@ -1725,7 +2048,7 @@ public class ChatController implements ThreadStatusCallback, PstRotatorEventList
|
||||
|
||||
});
|
||||
|
||||
lstNotify_QSOSniffer_sniffedCallSignList.add("DF0GEB");
|
||||
// lstNotify_QSOSniffer_sniffedCallSignList.add("DF0GEB");
|
||||
|
||||
|
||||
lst_toMeMessageList.setPredicate(chatFilterPredicate); //sniffed callsign filter predicate is here!
|
||||
@@ -1868,6 +2191,38 @@ public class ChatController implements ThreadStatusCallback, PstRotatorEventList
|
||||
return dxClusterServer;
|
||||
}
|
||||
|
||||
public synchronized void startDxClusterServerIfEnabled() {
|
||||
if (!chatPreferences.isNotify_dxClusterServerEnabled()
|
||||
|| dxClusterServer != null) {
|
||||
return;
|
||||
}
|
||||
|
||||
dxClusterServer = new DXClusterThreadPooledServer(
|
||||
chatPreferences.getNotify_dxclusterServerPort(),
|
||||
this,
|
||||
this
|
||||
);
|
||||
|
||||
Thread serverThread = new Thread(dxClusterServer);
|
||||
serverThread.setName("DXCluster-thread-pooled-server");
|
||||
serverThread.setDaemon(true);
|
||||
serverThread.start();
|
||||
}
|
||||
|
||||
public synchronized void stopDxClusterServer() {
|
||||
DXClusterThreadPooledServer serverToStop = dxClusterServer;
|
||||
dxClusterServer = null;
|
||||
|
||||
if (serverToStop != null) {
|
||||
serverToStop.stop();
|
||||
}
|
||||
}
|
||||
|
||||
public synchronized void restartDxClusterServerIfEnabled() {
|
||||
stopDxClusterServer();
|
||||
startDxClusterServerIfEnabled();
|
||||
}
|
||||
|
||||
// public void setChatMemberTable(Hashtable<String, ChatMember> chatMemberTable) {
|
||||
// this.chatMemberTable = chatMemberTable;
|
||||
// }
|
||||
@@ -1916,7 +2271,11 @@ public class ChatController implements ThreadStatusCallback, PstRotatorEventList
|
||||
messageTXBus = new LinkedBlockingQueue<ChatMessage>();
|
||||
|
||||
// socket = new Socket(hostname, port);//socket for the on4kst chat server
|
||||
socket = new Socket(chatController.chatPreferences.getStn_on4kstServersDns(), port);//socket for the on4kst chat server
|
||||
socket = new Socket(
|
||||
chatPreferences.getStn_on4kstServersDns(),
|
||||
chatPreferences.getStn_on4kstServersPort()
|
||||
);//socket for the on4kst chat server
|
||||
|
||||
System.out.println("Connected to the chat server: " + socket.isConnected());
|
||||
|
||||
ByteBuffer buffer = ByteBuffer.allocate(1024);
|
||||
@@ -1947,7 +2306,11 @@ public class ChatController implements ThreadStatusCallback, PstRotatorEventList
|
||||
messageProcessor.setName("messagebusManagementThread");
|
||||
messageProcessor.start();
|
||||
|
||||
airScoutUDPReaderThread = new ReadUDPbyAirScoutMessageThread(chatPreferences.getAirScout_asCommunicationPort(), this, this.getChatPreferences().getAirScout_asServerNameString(), this.getChatPreferences().getAirScout_asServerNameString(), this); //working original
|
||||
airScoutUDPReaderThread = new ReadUDPbyAirScoutMessageThread(
|
||||
chatPreferences.getAirScout_asCommunicationPort(),
|
||||
this,
|
||||
this
|
||||
);
|
||||
airScoutUDPReaderThread.setName("airscoutudpreaderThread");
|
||||
airScoutUDPReaderThread.start();
|
||||
|
||||
@@ -1966,8 +2329,7 @@ public class ChatController implements ThreadStatusCallback, PstRotatorEventList
|
||||
/**
|
||||
* DX cluster service running config
|
||||
*/
|
||||
dxClusterServer = new DXClusterThreadPooledServer(this.getChatPreferences().getNotify_dxclusterServerPort(), this, this);
|
||||
new Thread(dxClusterServer).start();
|
||||
startDxClusterServerIfEnabled();
|
||||
|
||||
|
||||
this.setConnectedAndLoggedIn(true);
|
||||
@@ -2011,7 +2373,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");
|
||||
|
||||
@@ -2179,32 +2541,39 @@ public class ChatController implements ThreadStatusCallback, PstRotatorEventList
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true when the member's gross field is not worked on at least one enabled
|
||||
* station band. This is the predicate behind the "new locator" filter.
|
||||
* Returns true when the member's four-character grid square has already been
|
||||
* worked on any band.
|
||||
*
|
||||
* <p>This method intentionally ignores the currently enabled own bands. The grid
|
||||
* status in the user list is meant to be a simple and robust any-band indicator.</p>
|
||||
*
|
||||
* @param member member to inspect
|
||||
* @return true if the station is still a new locator on any enabled band
|
||||
* @return true if the gross grid square was worked on any band
|
||||
*/
|
||||
public boolean isNewLocatorOnAnyEnabledBand(ChatMember member) {
|
||||
public boolean isGridSquareWorkedAny(ChatMember member) {
|
||||
if (member == null || member.getQra() == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
EnumSet<Band> enabledBands = reachabilityService == null
|
||||
? getMyEnabledBandsFromPrefs(chatPreferences)
|
||||
: reachabilityService.getEnabledStationBands();
|
||||
return workedGrossFieldCache.isGrossFieldWorkedAny(member.getQra());
|
||||
}
|
||||
|
||||
if (enabledBands.isEmpty()) {
|
||||
/**
|
||||
* Returns true when the member's four-character grid square has not been worked
|
||||
* on any band yet.
|
||||
*
|
||||
* <p>This is the predicate behind the optional "Only new grids" filter. It no
|
||||
* longer depends on active station band settings.</p>
|
||||
*
|
||||
* @param member member to inspect
|
||||
* @return true if the gross grid square is still new
|
||||
*/
|
||||
public boolean isNewGridSquare(ChatMember member) {
|
||||
if (member == null || member.getQra() == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (Band band : enabledBands) {
|
||||
if (!workedGrossFieldCache.isGrossFieldWorked(band, member.getQra())) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
return !workedGrossFieldCache.isGrossFieldWorkedAny(member.getQra());
|
||||
}
|
||||
|
||||
public long getCurrentEpochTime() {
|
||||
@@ -2249,7 +2618,7 @@ public class ChatController implements ThreadStatusCallback, PstRotatorEventList
|
||||
|
||||
String loginString = "";
|
||||
loginString = "LOGINC|" + chatPreferences.getStn_loginCallSign() + "|" + chatPreferences.getStn_loginPassword()
|
||||
+ "|" + chatPreferences.getLoginChatCategoryMain().getCategoryNumber() + "|praktiKST v" + ApplicationConstants.APPLICATION_CURRENTVERSIONNUMBER
|
||||
+ "|" + chatPreferences.getLoginChatCategoryMain().getCategoryNumber() + "|praktiKST v" + ApplicationConstants.APPLICATION_CURRENT_VERSION
|
||||
+ "|25|0|1|" + "0" + "|0|";
|
||||
|
||||
// System.out.println(loginString);
|
||||
|
||||
@@ -682,8 +682,7 @@ public class DBController {
|
||||
String workedBandColumnName = helper_resolveWorkedBandColumnName(chatMemberToStore);
|
||||
|
||||
if (workedBandColumnName == null) {
|
||||
System.out.println("[DBCtrl, Error]: unknown at which band the qso had been!");
|
||||
return false;
|
||||
return helper_updateWorkedAnyOnChatMember(chatMemberToStore);
|
||||
}
|
||||
|
||||
// String updateWorkedSql =
|
||||
@@ -717,6 +716,47 @@ public class DBController {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates only the global worked-any flag of a stored chatmember row.
|
||||
*
|
||||
* <p>This is used when an external logger confirms that a station was worked,
|
||||
* but the software cannot reliably map the QSO to one of the persisted band
|
||||
* columns. The UI status "x" is based on this global worked flag, so this method
|
||||
* makes no-band or unsupported-band log entries persistent as worked-any.</p>
|
||||
*
|
||||
* @param chatMemberToStore chatmember that contains the worked call and optional locator
|
||||
* @return true if an existing database row was updated
|
||||
* @throws SQLException if the database write fails
|
||||
*/
|
||||
private synchronized boolean helper_updateWorkedAnyOnChatMember(ChatMember chatMemberToStore) throws SQLException {
|
||||
|
||||
if (chatMemberToStore == null
|
||||
|| chatMemberToStore.getCallSignRaw() == null
|
||||
|| chatMemberToStore.getCallSignRaw().isBlank()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
String updateWorkedAnySql =
|
||||
"UPDATE ChatMember SET worked = 1, "
|
||||
+ "qra = CASE WHEN ? IS NOT NULL AND TRIM(?) <> '' AND LOWER(TRIM(?)) <> 'unknown' THEN ? ELSE qra END, "
|
||||
+ "lastFlagsChangeEpochMs = ? WHERE callsign = ?;";
|
||||
|
||||
try (PreparedStatement preparedStatement = connection.prepareStatement(updateWorkedAnySql)) {
|
||||
String qra = chatMemberToStore.getQra();
|
||||
|
||||
preparedStatement.setString(1, qra);
|
||||
preparedStatement.setString(2, qra);
|
||||
preparedStatement.setString(3, qra);
|
||||
preparedStatement.setString(4, qra);
|
||||
preparedStatement.setLong(5, System.currentTimeMillis());
|
||||
preparedStatement.setString(6, chatMemberToStore.getCallSignRaw());
|
||||
|
||||
int affectedRows = preparedStatement.executeUpdate();
|
||||
return affectedRows > 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Updates all not-QRV flags for a chatmember row. The method uses the normalized
|
||||
* raw callsign and updates the timestamp so that automatic contest cleanup can
|
||||
|
||||
@@ -1,238 +1,363 @@
|
||||
package kst4contest.controller;
|
||||
|
||||
import kst4contest.model.ChatMember;
|
||||
import kst4contest.model.ChatPreferences;
|
||||
import kst4contest.model.ThreadStateMessage;
|
||||
|
||||
import java.io.*;
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
import java.net.ServerSocket;
|
||||
import java.net.Socket;
|
||||
import java.util.*;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.logging.Level;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
public class DXClusterThreadPooledServer implements Runnable{
|
||||
public class DXClusterThreadPooledServer implements Runnable {
|
||||
|
||||
private static final Logger LOGGER = Logger.getLogger(DXClusterThreadPooledServer.class.getName());
|
||||
private List<Socket> clientSockets = Collections.synchronizedList(new ArrayList<>()); //list of all connected clients
|
||||
private static final Logger LOGGER =
|
||||
Logger.getLogger(DXClusterThreadPooledServer.class.getName());
|
||||
private static final String THREAD_NICKNAME = "DXCluster-Server";
|
||||
|
||||
private ThreadStatusCallback callBackToController;
|
||||
private String ThreadNickName = "DXCluster-Server";
|
||||
ChatController chatController = null;
|
||||
protected int serverPort = 8080;
|
||||
protected ServerSocket serverSocket = null;
|
||||
protected boolean isStopped = false;
|
||||
protected Thread runningThread= null;
|
||||
protected ExecutorService threadPool =
|
||||
private final List<Socket> clientSockets =
|
||||
Collections.synchronizedList(new ArrayList<>());
|
||||
|
||||
private final ChatController chatController;
|
||||
private final ThreadStatusCallback callBackToController;
|
||||
private final int serverPort;
|
||||
|
||||
private final ExecutorService threadPool =
|
||||
Executors.newFixedThreadPool(10);
|
||||
Socket clientSocket;
|
||||
|
||||
public DXClusterThreadPooledServer(int port, ChatController chatController, ThreadStatusCallback callback){
|
||||
private final ScheduledExecutorService keepAliveExecutor =
|
||||
Executors.newSingleThreadScheduledExecutor();
|
||||
|
||||
private volatile boolean stopped;
|
||||
private ServerSocket serverSocket;
|
||||
|
||||
public DXClusterThreadPooledServer(
|
||||
int port,
|
||||
ChatController chatController,
|
||||
ThreadStatusCallback callback
|
||||
) {
|
||||
this.serverPort = port;
|
||||
this.chatController = chatController;
|
||||
this.callBackToController = callback;
|
||||
}
|
||||
|
||||
public void run(){
|
||||
@Override
|
||||
public void run() {
|
||||
Thread.currentThread().setName("DXCluster-thread-pooled-server");
|
||||
|
||||
ThreadStateMessage threadStateMessage = new ThreadStateMessage(this.ThreadNickName, true, "initialized", false);
|
||||
callBackToController.onThreadStatus(ThreadNickName,threadStateMessage);
|
||||
try {
|
||||
serverSocket = new ServerSocket(serverPort);
|
||||
|
||||
synchronized(this){
|
||||
this.runningThread = Thread.currentThread();
|
||||
runningThread.setName("DXCluster-thread-pooled-server");
|
||||
}
|
||||
openServerSocket();
|
||||
while(! isStopped()){
|
||||
clientSocket = null;
|
||||
try {
|
||||
clientSocket = this.serverSocket.accept();
|
||||
|
||||
synchronized(clientSockets) {
|
||||
clientSockets.add(clientSocket); // add dx cluster client to the "clients list" for broadcasting
|
||||
}
|
||||
|
||||
} catch (IOException e) {
|
||||
if(isStopped()) {
|
||||
System.out.println("Server Stopped.") ;
|
||||
break;
|
||||
}
|
||||
throw new RuntimeException(
|
||||
"Error accepting client connection", e);
|
||||
if (stopped) {
|
||||
return;
|
||||
}
|
||||
|
||||
DXClusterServerWorkerRunnable worker = new DXClusterServerWorkerRunnable(clientSocket, "Thread Pooled DXCluster Server ", chatController, clientSockets, chatController);
|
||||
callBackToController.onThreadStatus(
|
||||
THREAD_NICKNAME,
|
||||
new ThreadStateMessage(
|
||||
THREAD_NICKNAME,
|
||||
true,
|
||||
"Listening on TCP port " + serverPort,
|
||||
false
|
||||
)
|
||||
);
|
||||
|
||||
this.threadPool.execute(worker);
|
||||
keepAliveExecutor.scheduleAtFixedRate(
|
||||
this::sendKeepAlive,
|
||||
30,
|
||||
30,
|
||||
TimeUnit.SECONDS
|
||||
);
|
||||
|
||||
}
|
||||
this.threadPool.shutdown();
|
||||
System.out.println("Server Stopped.") ;
|
||||
}
|
||||
while (!stopped) {
|
||||
try {
|
||||
Socket clientSocket = serverSocket.accept();
|
||||
clientSockets.add(clientSocket);
|
||||
|
||||
private synchronized boolean isStopped() {
|
||||
return this.isStopped;
|
||||
}
|
||||
|
||||
public synchronized void stop(){
|
||||
this.isStopped = true;
|
||||
try {
|
||||
this.serverSocket.close();
|
||||
synchronized(clientSockets) {
|
||||
for (Socket socket : clientSockets) {
|
||||
socket.close(); // close all client connections
|
||||
threadPool.execute(
|
||||
new DXClusterServerWorkerRunnable(
|
||||
clientSocket,
|
||||
clientSockets
|
||||
)
|
||||
);
|
||||
} catch (IOException exception) {
|
||||
if (!stopped) {
|
||||
LOGGER.log(
|
||||
Level.SEVERE,
|
||||
"Error accepting DX Cluster client connection",
|
||||
exception
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException("DXCCSERVER Error closing server", e);
|
||||
} catch (IOException exception) {
|
||||
if (!stopped) {
|
||||
LOGGER.log(
|
||||
Level.SEVERE,
|
||||
"Cannot open DX Cluster TCP port " + serverPort,
|
||||
exception
|
||||
);
|
||||
|
||||
callBackToController.onThreadStatus(
|
||||
THREAD_NICKNAME,
|
||||
new ThreadStateMessage(
|
||||
THREAD_NICKNAME,
|
||||
false,
|
||||
"Cannot open TCP port "
|
||||
+ serverPort
|
||||
+ ": "
|
||||
+ exception.getMessage(),
|
||||
true
|
||||
)
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
closeServerSocket();
|
||||
closeClientSockets();
|
||||
keepAliveExecutor.shutdownNow();
|
||||
threadPool.shutdownNow();
|
||||
}
|
||||
}
|
||||
|
||||
private void openServerSocket() {
|
||||
try {
|
||||
this.serverSocket = new ServerSocket(this.serverPort);
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException("DXCCSERVER Cannot open port ", e);
|
||||
public synchronized void stop() {
|
||||
stopped = true;
|
||||
closeServerSocket();
|
||||
closeClientSockets();
|
||||
keepAliveExecutor.shutdownNow();
|
||||
threadPool.shutdownNow();
|
||||
}
|
||||
|
||||
public boolean hasConnectedClients() {
|
||||
synchronized (clientSockets) {
|
||||
removeClosedClients();
|
||||
return !clientSockets.isEmpty();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a DX cluster message to ALL connected log programs via telnet, returns true if sent
|
||||
* Sends one DX Cluster spot to all currently connected clients.
|
||||
*
|
||||
* @param aChatMember
|
||||
* @return boolean true if message had been sent
|
||||
* @return true if the spot was delivered to at least one client
|
||||
*/
|
||||
public boolean broadcastSingleDXClusterEntryToLoggers(ChatMember aChatMember) {
|
||||
synchronized(clientSockets) {
|
||||
public boolean broadcastSingleDXClusterEntryToLoggers(
|
||||
ChatMember chatMember
|
||||
) {
|
||||
final String clusterMessage;
|
||||
|
||||
System.out.println("DXClusterSrvr: broadcasting message to clients: " + clientSockets.size());
|
||||
try {
|
||||
String frequency = Utils4KST.normalizeFrequencyString(
|
||||
chatMember.getFrequency().getValue(),
|
||||
chatController
|
||||
.getChatPreferences()
|
||||
.getNotify_optionalFrequencyPrefix()
|
||||
);
|
||||
|
||||
try {
|
||||
clusterMessage =
|
||||
"DX de "
|
||||
+ chatController
|
||||
.getChatPreferences()
|
||||
.getNotify_DXCSrv_SpottersCallSign()
|
||||
.getValue()
|
||||
+ ": "
|
||||
+ frequency
|
||||
+ " "
|
||||
+ chatMember.getCallSign().toUpperCase()
|
||||
+ " "
|
||||
+ chatMember.getQra().toUpperCase()
|
||||
+ " "
|
||||
+ new Utils4KST()
|
||||
.time_generateCurrenthhmmZTimeStringForClusterMessage()
|
||||
+ ((char) 7)
|
||||
+ ((char) 7)
|
||||
+ "\r\n";
|
||||
} catch (Exception exception) {
|
||||
LOGGER.log(
|
||||
Level.SEVERE,
|
||||
"Cannot build DX Cluster message",
|
||||
exception
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
System.out.println("-------------> ORIGINALEE VAL: " + aChatMember.getFrequency().getValue());
|
||||
System.out.println("-------------> NORMALIZED VAL: " + Utils4KST.normalizeFrequencyString(aChatMember.getFrequency().getValue(), chatController.getChatPreferences().getNotify_optionalFrequencyPrefix()) + " ");
|
||||
} catch (Exception e) {
|
||||
LOGGER.log(Level.SEVERE, "DXCThPooledServer: Error accessing value in chatmember object", e);
|
||||
}
|
||||
int deliveredClients = 0;
|
||||
|
||||
for (Socket socket : clientSockets) {
|
||||
synchronized (clientSockets) {
|
||||
Iterator<Socket> iterator = clientSockets.iterator();
|
||||
|
||||
while (iterator.hasNext()) {
|
||||
Socket socket = iterator.next();
|
||||
|
||||
if (socket == null || socket.isClosed()) {
|
||||
iterator.remove();
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
OutputStream output = socket.getOutputStream();
|
||||
output.write(
|
||||
clusterMessage.getBytes(
|
||||
StandardCharsets.US_ASCII
|
||||
)
|
||||
);
|
||||
output.flush();
|
||||
deliveredClients++;
|
||||
} catch (IOException exception) {
|
||||
LOGGER.log(
|
||||
Level.WARNING,
|
||||
"DX Cluster client disconnected while sending a spot",
|
||||
exception
|
||||
);
|
||||
|
||||
OutputStream output = socket.getOutputStream();
|
||||
|
||||
String singleDXClusterMessage = "DX de ";
|
||||
|
||||
// singleDXClusterMessage += chatController.getChatPreferences().getLoginCallSign() + ": ";
|
||||
|
||||
|
||||
|
||||
|
||||
singleDXClusterMessage += this.chatController.getChatPreferences().getNotify_DXCSrv_SpottersCallSign().getValue() + ": ";
|
||||
singleDXClusterMessage += Utils4KST.normalizeFrequencyString(aChatMember.getFrequency().getValue(), chatController.getChatPreferences().getNotify_optionalFrequencyPrefix()) + " ";
|
||||
singleDXClusterMessage += aChatMember.getCallSign().toUpperCase() + " "; //we need such an amount of spaces for n1mm to work, otherwise bullshit happens
|
||||
singleDXClusterMessage += aChatMember.getQra().toUpperCase() + " ";
|
||||
singleDXClusterMessage += new Utils4KST().time_generateCurrenthhmmZTimeStringForClusterMessage() + ((char)7) + ((char)7) + "\r\n";
|
||||
|
||||
// singleDXClusterMessage += chatController.getChatPreferences().getLoginCallSign() + ": ";
|
||||
// singleDXClusterMessage += Utils4KST.normalizeFrequencyString(aChatMember.getFrequency().getValue(), chatController.getChatPreferences().getNotify_optionalFrequencyPrefix()) + " ";
|
||||
// singleDXClusterMessage += aChatMember.getCallSign().toUpperCase() + " ";
|
||||
// singleDXClusterMessage += aChatMember.getQra().toUpperCase() + " ";
|
||||
// singleDXClusterMessage += new Utils4KST().time_generateCurrenthhmmZTimeStringForClusterMessage() + ((char)7) + ((char)7) + "\r\n";
|
||||
|
||||
output.write((singleDXClusterMessage).getBytes());
|
||||
|
||||
ThreadStateMessage threadStateMessage = new ThreadStateMessage(this.ThreadNickName, true, "Last msg to " + clientSockets.size() + " Cluster Clients:\n" + singleDXClusterMessage, false);
|
||||
callBackToController.onThreadStatus(ThreadNickName,threadStateMessage);
|
||||
|
||||
} catch (IOException e) {
|
||||
LOGGER.log(Level.SEVERE, "[DXClusterSrvr] broadcasting DXC-message to clients went wrong", e);
|
||||
return false;
|
||||
closeSocket(socket);
|
||||
iterator.remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
return true; //if message had been sent, return true for "ok"
|
||||
|
||||
if (deliveredClients > 0) {
|
||||
callBackToController.onThreadStatus(
|
||||
THREAD_NICKNAME,
|
||||
new ThreadStateMessage(
|
||||
THREAD_NICKNAME,
|
||||
true,
|
||||
"Last spot sent to "
|
||||
+ deliveredClients
|
||||
+ " DX Cluster client(s):\n"
|
||||
+ clusterMessage,
|
||||
false
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return deliveredClients > 0;
|
||||
}
|
||||
|
||||
private void sendKeepAlive() {
|
||||
synchronized (clientSockets) {
|
||||
Iterator<Socket> iterator = clientSockets.iterator();
|
||||
|
||||
while (iterator.hasNext()) {
|
||||
Socket socket = iterator.next();
|
||||
|
||||
if (socket == null || socket.isClosed()) {
|
||||
iterator.remove();
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
OutputStream output = socket.getOutputStream();
|
||||
output.write(
|
||||
"\r\n".getBytes(StandardCharsets.US_ASCII)
|
||||
);
|
||||
output.flush();
|
||||
} catch (IOException exception) {
|
||||
closeSocket(socket);
|
||||
iterator.remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void removeClosedClients() {
|
||||
clientSockets.removeIf(
|
||||
socket -> socket == null || socket.isClosed()
|
||||
);
|
||||
}
|
||||
|
||||
private synchronized void closeServerSocket() {
|
||||
if (serverSocket == null || serverSocket.isClosed()) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
serverSocket.close();
|
||||
} catch (IOException exception) {
|
||||
LOGGER.log(
|
||||
Level.WARNING,
|
||||
"Error closing DX Cluster server socket",
|
||||
exception
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private void closeClientSockets() {
|
||||
synchronized (clientSockets) {
|
||||
for (Socket socket : clientSockets) {
|
||||
closeSocket(socket);
|
||||
}
|
||||
|
||||
clientSockets.clear();
|
||||
}
|
||||
}
|
||||
|
||||
private static void closeSocket(Socket socket) {
|
||||
if (socket == null || socket.isClosed()) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
socket.close();
|
||||
} catch (IOException ignored) {
|
||||
// The connection is already unusable.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class DXClusterServerWorkerRunnable implements Runnable{
|
||||
class DXClusterServerWorkerRunnable implements Runnable {
|
||||
|
||||
private static final Logger LOGGER = Logger.getLogger(DXClusterServerWorkerRunnable.class.getName());
|
||||
protected Socket clientSocket = null;
|
||||
protected String serverText = null;
|
||||
private ChatController client = null;
|
||||
private List<Socket> dxClusterClientSocketsConnectedList;
|
||||
private ThreadStatusCallback callBackToController;
|
||||
private String ThreadNickName = "DXCluster-Server";
|
||||
private static final Logger LOGGER =
|
||||
Logger.getLogger(DXClusterServerWorkerRunnable.class.getName());
|
||||
|
||||
public DXClusterServerWorkerRunnable(Socket clientSocket, String serverText, ChatController chatController, List<Socket> clientSockets, ThreadStatusCallback callback) {
|
||||
private final Socket clientSocket;
|
||||
private final List<Socket> clientSockets;
|
||||
|
||||
DXClusterServerWorkerRunnable(
|
||||
Socket clientSocket,
|
||||
List<Socket> clientSockets
|
||||
) {
|
||||
this.clientSocket = clientSocket;
|
||||
this.serverText = serverText;
|
||||
this.client = chatController;
|
||||
this.dxClusterClientSocketsConnectedList = clientSockets;
|
||||
this.callBackToController = callback;
|
||||
|
||||
this.clientSockets = clientSockets;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
OutputStream output = clientSocket.getOutputStream();
|
||||
dxClusterClientSocketsConnectedList.add(clientSocket);
|
||||
output.write(
|
||||
"login: ".getBytes(StandardCharsets.US_ASCII)
|
||||
);
|
||||
output.flush();
|
||||
|
||||
Timer dXCkeepAliveTimer = new Timer();
|
||||
dXCkeepAliveTimer.schedule(new TimerTask() {
|
||||
System.out.println(
|
||||
"[DXClusterServer] New client connected: "
|
||||
+ clientSocket.getInetAddress()
|
||||
);
|
||||
} catch (IOException exception) {
|
||||
LOGGER.log(
|
||||
Level.WARNING,
|
||||
"Cannot initialise DX Cluster client connection",
|
||||
exception
|
||||
);
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
synchronized (clientSockets) {
|
||||
clientSockets.remove(clientSocket);
|
||||
}
|
||||
|
||||
StringBuilder connectedClients = new StringBuilder(); //only for statistics
|
||||
|
||||
for (Socket socket : dxClusterClientSocketsConnectedList) {
|
||||
|
||||
connectedClients.append(socket.getInetAddress()).append("\n");
|
||||
|
||||
try {
|
||||
OutputStream output = socket.getOutputStream();
|
||||
output.write(("\r\n").getBytes());
|
||||
|
||||
} catch (IOException e) {
|
||||
LOGGER.log(Level.SEVERE, "[DXClusterSrvr] keep-alive broadcast to client failed", e);
|
||||
dXCkeepAliveTimer.purge();
|
||||
|
||||
try {
|
||||
socket.close();
|
||||
} catch (IOException ex) {
|
||||
LOGGER.log(Level.SEVERE, "[DXClusterSrvr] error closing client socket", ex);
|
||||
}
|
||||
finally {
|
||||
this.cancel();
|
||||
}
|
||||
dxClusterClientSocketsConnectedList.remove(socket); //if socket is closed by client, remove it from the broadcast list and close it
|
||||
}
|
||||
}
|
||||
|
||||
// ThreadStateMessage threadStateMessage = new ThreadStateMessage(ThreadNickName, true, "Connected clients: " + connectedClients.toString(), false);
|
||||
// callBackToController.onThreadStatus(ThreadNickName,threadStateMessage);
|
||||
|
||||
}
|
||||
}, 30000, 30000);
|
||||
|
||||
|
||||
output.write(("login: ").getBytes()); //say hello to the client, it will answer with a callsign
|
||||
System.out.println("[DXClusterThreadPooledServer, Info:] New cluster client connected! "); //TODO: maybe integrate non blocking reader for client identification
|
||||
|
||||
} catch (IOException e) {
|
||||
LOGGER.log(Level.SEVERE, "[DXClusterSrvr] error in worker runnable", e);
|
||||
} finally {
|
||||
synchronized(dxClusterClientSocketsConnectedList) {
|
||||
dxClusterClientSocketsConnectedList.remove(clientSocket); // Entferne den Client nach Verarbeitung
|
||||
try {
|
||||
clientSocket.close();
|
||||
} catch (IOException ignored) {
|
||||
// The connection is already unusable.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,9 +1,13 @@
|
||||
package kst4contest.controller;
|
||||
|
||||
import java.io.*;
|
||||
import java.net.*;
|
||||
import java.util.ArrayList;
|
||||
import java.io.IOException;
|
||||
import java.net.DatagramPacket;
|
||||
import java.net.DatagramSocket;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.net.SocketException;
|
||||
import java.net.SocketTimeoutException;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.collections.ObservableList;
|
||||
@@ -21,188 +25,245 @@ import kst4contest.model.ThreadStateMessage;
|
||||
* @author www.codejava.net
|
||||
*/
|
||||
public class ReadUDPbyAirScoutMessageThread extends Thread {
|
||||
private BufferedReader reader;
|
||||
private Socket socket;
|
||||
private ChatController client;
|
||||
private int localPort;
|
||||
private String ASIdentificator, ChatClientIdentificator;
|
||||
private ThreadStatusCallback callBackToController;
|
||||
private String ThreadNickName = "AirScout msg";
|
||||
// public ReadUDPbyAirScoutMessageThread(int localPort) {
|
||||
// this.localPort = localPort;
|
||||
// }
|
||||
|
||||
public ReadUDPbyAirScoutMessageThread(int localPort, ChatController client, String ASIdentificator,
|
||||
String ChatClientIdentificator, ThreadStatusCallback callback) {
|
||||
private final ChatController client;
|
||||
private final int localPort;
|
||||
private final ThreadStatusCallback callBackToController;
|
||||
|
||||
this.callBackToController = callback;
|
||||
private final String threadNickName = "AirScout msg";
|
||||
|
||||
private DatagramSocket socket;
|
||||
|
||||
public ReadUDPbyAirScoutMessageThread(
|
||||
int localPort,
|
||||
ChatController client,
|
||||
ThreadStatusCallback callback
|
||||
) {
|
||||
this.localPort = localPort;
|
||||
this.client = client;
|
||||
this.ASIdentificator = ASIdentificator;
|
||||
this.ChatClientIdentificator = ChatClientIdentificator;
|
||||
this.callBackToController = callback;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void interrupt() {
|
||||
System.out.println("ReadUDP");
|
||||
super.interrupt();
|
||||
try {
|
||||
if (this.socket != null) {
|
||||
|
||||
this.socket.close();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
// TODO Auto-generated catch block
|
||||
e.printStackTrace();
|
||||
|
||||
if (socket != null && !socket.isClosed()) {
|
||||
socket.close();
|
||||
}
|
||||
}
|
||||
|
||||
private void callThreadStateToUi (ThreadStateMessage threadStateMessage) {
|
||||
if (callBackToController != null) {
|
||||
//update the visual control of running thread
|
||||
callBackToController.onThreadStatus("AirScout", threadStateMessage);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Checks whether an AirScout response is addressed to the currently
|
||||
* configured server and client identifiers.
|
||||
*
|
||||
* Outgoing message:
|
||||
* ASSETPATH: "client" "server" ...
|
||||
*
|
||||
* Corresponding response:
|
||||
* ASNEAREST: "server" "client" ...
|
||||
*
|
||||
* The comparison is deliberately case-sensitive. In a setup with several
|
||||
* clients, KST-A and kst-a must not silently become the same destination.
|
||||
*
|
||||
* @param message received UDP message
|
||||
* @return true if the response belongs to this KST4Contest instance
|
||||
*/
|
||||
private boolean isMessageForConfiguredClient(String message) {
|
||||
if (message == null || !message.startsWith("ASNEAREST:")) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
String[] quotedParts = message.split("\"");
|
||||
if (quotedParts.length < 4) {
|
||||
return false;
|
||||
}
|
||||
|
||||
String receivedServerIdentifier = quotedParts[1].trim();
|
||||
String receivedClientIdentifier = quotedParts[3].trim();
|
||||
|
||||
String configuredServerIdentifier =
|
||||
client.getChatPreferences()
|
||||
.getAirScout_asServerNameString();
|
||||
String configuredClientIdentifier =
|
||||
client.getChatPreferences()
|
||||
.getAirScout_asClientNameString();
|
||||
|
||||
if (configuredServerIdentifier == null
|
||||
|| configuredClientIdentifier == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return configuredServerIdentifier.equals(
|
||||
receivedServerIdentifier
|
||||
) && configuredClientIdentifier.equals(
|
||||
receivedClientIdentifier
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
Thread.currentThread().setName("ReadUDPByAirScoutThread");
|
||||
|
||||
DatagramSocket socket = null;
|
||||
boolean running;
|
||||
byte[] buf = new byte[1777];
|
||||
DatagramPacket packet;
|
||||
// DatagramPacket packet = new DatagramPacket(buf, buf.length); //changed due to save memory
|
||||
packet = new DatagramPacket(buf, buf.length);
|
||||
Thread.currentThread().setName(
|
||||
"ReadUDPByAirScoutThread"
|
||||
);
|
||||
|
||||
try {
|
||||
|
||||
socket = new DatagramSocket(null);
|
||||
socket.setReuseAddress(true);
|
||||
socket.bind(new InetSocketAddress(localPort));
|
||||
socket.receive(packet);
|
||||
socket.setSoTimeout(3000);
|
||||
|
||||
} catch (SocketException e) {
|
||||
// TODO Auto-generated catch block
|
||||
e.printStackTrace();
|
||||
|
||||
} catch (IOException e) {
|
||||
// TODO Auto-generated catch block
|
||||
// e.printStackTrace();
|
||||
}
|
||||
|
||||
|
||||
while (true) {
|
||||
// packet = new DatagramPacket(buf, buf.length);
|
||||
// DatagramPacket packet = new DatagramPacket(SRPDefinitions.BYTE_BUFFER_MAX_LENGTH);
|
||||
try {
|
||||
if (this.client.isDisconnectionPerformedByUser()) {
|
||||
break;//TODO: what if it´s not the finally closage but a band channel change?
|
||||
while (!Thread.currentThread().isInterrupted()) {
|
||||
if (client.isDisconnectionPerformedByUser()) {
|
||||
break;
|
||||
}
|
||||
|
||||
socket.receive(packet);
|
||||
|
||||
byte[] buffer = new byte[1777];
|
||||
DatagramPacket packet = new DatagramPacket(
|
||||
buffer,
|
||||
buffer.length
|
||||
);
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
} catch (SocketTimeoutException e2) {
|
||||
// this will catch the repeating Sockettimeoutexception...nothing to do
|
||||
// e2.printStackTrace();
|
||||
}
|
||||
catch (IOException e) {
|
||||
// TODO Auto-generated catch block
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
InetAddress address = packet.getAddress();
|
||||
int port = packet.getPort();
|
||||
// packet = new DatagramPacket(buf, buf.length, address, port);
|
||||
String received = new String(packet.getData(), packet.getOffset(), packet.getLength());
|
||||
received = received.trim();
|
||||
|
||||
if (received.contains(ApplicationConstants.DISCONNECT_RDR_POISONPILL)) {
|
||||
System.out.println("ReadUdpByASMsgTh, Info: got poison, now dieing....");
|
||||
try {
|
||||
terminateConnection();
|
||||
} catch (Exception e) {
|
||||
System.out.println("ASUDPRDR: catched error " + e.getMessage());
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
if (received.contains("ASSETPATH") || received.contains("ASWATCHLIST")) {
|
||||
// do nothing, that is your own message
|
||||
} else if (received.contains("ASNEAREST:")) { //answer by airscout
|
||||
|
||||
// processASUDPMessage(received); //TODO: 2025-11-Zeile deaktiviert. Fand hier Doppelberechnung statt?!
|
||||
|
||||
AirPlaneReflectionInfo apReflectInfoForChatMember;
|
||||
|
||||
apReflectInfoForChatMember = processASUDPMessage(received);
|
||||
if (!this.client.getLst_chatMemberList().isEmpty()) {
|
||||
|
||||
try {
|
||||
|
||||
// this.client.getLst_chatMemberList()
|
||||
// .get(this.client.checkListForChatMemberIndexByCallSign(
|
||||
// apReflectInfoForChatMember.getReceiver()))
|
||||
// .setAirPlaneReflectInfo(apReflectInfoForChatMember); // TODO: here we set the ap info at
|
||||
// // the central instance of
|
||||
// // chatmember list .... -1 is a
|
||||
// // problem!
|
||||
|
||||
ArrayList<Integer> addApInfoToThese = this.client.checkListForChatMemberIndexesByCallSign(apReflectInfoForChatMember.getReceiver());
|
||||
addApInfoToThese.forEach((integerIndex) -> {this.client.getLst_chatMemberList().get(integerIndex).setAirPlaneReflectInfo(apReflectInfoForChatMember); });
|
||||
|
||||
// AirScout availability strongly affects priority => request recompute the score of the chatmember
|
||||
this.client.getScoreService().requestRecompute("airscout-update");
|
||||
|
||||
/**
|
||||
* CK| MSGBUS BGFX Listactualizer Exception in thread "Thread-10"
|
||||
* java.util.ConcurrentModificationException at
|
||||
* java.base/java.util.AbstractList$Itr.checkForComodification(AbstractList.java:399)
|
||||
* at java.base/java.util.AbstractList$Itr.next(AbstractList.java:368) at
|
||||
* kst4contest.controller.ChatController.checkListForChatMemberIndexByCallSign(ChatController.java:173)
|
||||
* at
|
||||
* kst4contest.controller.ReadUDPbyAirScoutMessageThread.run(ReadUDPbyAirScoutMessageThread.java:93)
|
||||
*
|
||||
*/
|
||||
// System.out.println("[ReadUdpByASth, AP-Info catched: ] " + apReflectInfoForChatMember.toString());
|
||||
// }
|
||||
} catch (Exception e) {
|
||||
|
||||
System.out.println("ReadUdpByAsMsgTh, Warning:"
|
||||
+ apReflectInfoForChatMember.getReceiver().getCallSign()
|
||||
+ " is not in the Chatmemberlist or the Chatmemberlist is modified by another Thread");
|
||||
// TODO: handle exception
|
||||
}
|
||||
|
||||
// String[] newState = new String[3];
|
||||
// newState[0] = "On";
|
||||
// newState[1] = "received line";
|
||||
// newState[2] = apReflectInfoForChatMember.toString();
|
||||
// callThreadStateToUi(newState);
|
||||
ThreadStateMessage threadStateMessage = new ThreadStateMessage(this.ThreadNickName, true, "received line\n" + apReflectInfoForChatMember.toString(), false);
|
||||
callBackToController.onThreadStatus(ThreadNickName,threadStateMessage);
|
||||
socket.receive(packet);
|
||||
} catch (SocketTimeoutException timeoutException) {
|
||||
/*
|
||||
* AirScout may remain silent for some time. Continue with a
|
||||
* new packet instead of processing the previous packet again.
|
||||
*/
|
||||
continue;
|
||||
}
|
||||
|
||||
}
|
||||
// packet = null; //reset packet
|
||||
buf = new byte[1777]; // reset buffer for future smaller packets
|
||||
String received = new String(
|
||||
packet.getData(),
|
||||
packet.getOffset(),
|
||||
packet.getLength()
|
||||
).trim();
|
||||
|
||||
if (received.contains(
|
||||
ApplicationConstants.DISCONNECT_RDR_POISONPILL
|
||||
)) {
|
||||
System.out.println(
|
||||
"[AirScout UDP, info]: Received shutdown packet."
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
/*
|
||||
* The socket remains bound so that AirScout can be enabled
|
||||
* without reconnecting. Disabled means that received data is
|
||||
* discarded and no station state is changed.
|
||||
*/
|
||||
if (!client.getChatPreferences()
|
||||
.isAirScout_asUDPListenerEnabled()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!isMessageForConfiguredClient(received)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
processAirScoutResponse(received);
|
||||
}
|
||||
} catch (SocketException exception) {
|
||||
if (!Thread.currentThread().isInterrupted()) {
|
||||
System.out.println(
|
||||
"[AirScout UDP, error]: Could not use UDP port "
|
||||
+ localPort
|
||||
+ ": "
|
||||
+ exception.getMessage()
|
||||
);
|
||||
}
|
||||
} catch (IOException exception) {
|
||||
if (!Thread.currentThread().isInterrupted()) {
|
||||
System.out.println(
|
||||
"[AirScout UDP, error]: Communication failed: "
|
||||
+ exception.getMessage()
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
if (socket != null && !socket.isClosed()) {
|
||||
socket.close();
|
||||
}
|
||||
|
||||
socket = null;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses an AirScout response and applies it to every active category
|
||||
* instance of the reported station.
|
||||
*
|
||||
* @param received received ASNEAREST message
|
||||
*/
|
||||
private void processAirScoutResponse(String received) {
|
||||
try {
|
||||
AirPlaneReflectionInfo reflectionInfo =
|
||||
processASUDPMessage(received);
|
||||
|
||||
if (reflectionInfo == null
|
||||
|| reflectionInfo.getReceiver() == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
String receiverCallSign =
|
||||
reflectionInfo.getReceiver().getCallSignRaw();
|
||||
|
||||
if (receiverCallSign == null
|
||||
|| receiverCallSign.isBlank()) {
|
||||
receiverCallSign =
|
||||
reflectionInfo.getReceiver().getCallSign();
|
||||
}
|
||||
|
||||
if (receiverCallSign == null
|
||||
|| receiverCallSign.isBlank()) {
|
||||
return;
|
||||
}
|
||||
|
||||
List<ChatMember> matchingMembers =
|
||||
client.findActiveChatMembersByRawCall(
|
||||
receiverCallSign
|
||||
);
|
||||
|
||||
for (ChatMember matchingMember : matchingMembers) {
|
||||
matchingMember.setAirPlaneReflectInfo(
|
||||
reflectionInfo
|
||||
);
|
||||
}
|
||||
|
||||
if (!matchingMembers.isEmpty()) {
|
||||
client.getScoreService().requestRecompute(
|
||||
"airscout-update"
|
||||
);
|
||||
}
|
||||
|
||||
if (callBackToController != null) {
|
||||
ThreadStateMessage threadStateMessage =
|
||||
new ThreadStateMessage(
|
||||
threadNickName,
|
||||
true,
|
||||
"Received AirScout response\n"
|
||||
+ reflectionInfo,
|
||||
false
|
||||
);
|
||||
|
||||
callBackToController.onThreadStatus(
|
||||
threadNickName,
|
||||
threadStateMessage
|
||||
);
|
||||
}
|
||||
} catch (RuntimeException exception) {
|
||||
System.out.println(
|
||||
"[AirScout UDP, warning]: Could not process response: "
|
||||
+ exception.getMessage()
|
||||
);
|
||||
}
|
||||
}
|
||||
public AirPlaneReflectionInfo processASUDPMessage(String udpStringToProcess) {
|
||||
|
||||
// System.out.println("RDUDPAS RECV: " + udpStringToProcess);
|
||||
|
||||
// TODO: filter messages which are directed to another client
|
||||
|
||||
|
||||
/*
|
||||
* Example mesage: ASNEAREST: "AS" "KST"
|
||||
@@ -304,11 +365,8 @@ public class ReadUDPbyAirScoutMessageThread extends Thread {
|
||||
}
|
||||
|
||||
public boolean terminateConnection() {
|
||||
|
||||
try {
|
||||
this.socket.close();
|
||||
} catch (Exception e) {
|
||||
System.out.println("udpbyas: catched " + e.getMessage());
|
||||
if (socket != null && !socket.isClosed()) {
|
||||
socket.close();
|
||||
}
|
||||
|
||||
return true;
|
||||
|
||||
@@ -319,6 +319,14 @@ public class ReadUDPbyUCXMessageThread extends Thread {
|
||||
NodeList list = doc.getElementsByTagName("contactinfo");
|
||||
if (list.getLength() != 0) {
|
||||
|
||||
/*
|
||||
* QSO and TRX information share the same UDP receiver. The log-sync
|
||||
* preference therefore controls processing, not ownership of the socket.
|
||||
*/
|
||||
if (!client.getChatPreferences().isLogsynch_ucxUDPWkdCallListenerEnabled()) {
|
||||
return "";
|
||||
}
|
||||
|
||||
for (int temp = 0; temp < list.getLength(); temp++) {
|
||||
|
||||
Node node = list.item(temp);
|
||||
@@ -591,6 +599,15 @@ public class ReadUDPbyUCXMessageThread extends Thread {
|
||||
} else {
|
||||
list = doc.getElementsByTagName("RadioInfo");
|
||||
|
||||
/*
|
||||
* RadioInfo packets may arrive on the shared UDP port even when automatic
|
||||
* QRG synchronization is disabled. Ignore them unless TRX sync is enabled.
|
||||
*/
|
||||
if (list.getLength() != 0
|
||||
&& !client.getChatPreferences().isTrxSynch_ucxLogUDPListenerEnabled()) {
|
||||
return "";
|
||||
}
|
||||
|
||||
for (int temp = 0; temp < list.getLength(); temp++) {
|
||||
|
||||
Node node = list.item(temp);
|
||||
|
||||
@@ -108,6 +108,16 @@ public class UpdateChecker {
|
||||
|
||||
Element element = (Element) node;
|
||||
updateInfos.setLatestVersionNumberOnServer(Double.parseDouble(element.getElementsByTagName("versionNumber").item(0).getTextContent()));
|
||||
|
||||
NodeList semanticVersionNodes =
|
||||
element.getElementsByTagName("semanticVersion");
|
||||
|
||||
if (semanticVersionNodes.getLength() > 0) {
|
||||
updateInfos.setLatestSemanticVersionOnServer(
|
||||
semanticVersionNodes.item(0).getTextContent()
|
||||
);
|
||||
}
|
||||
|
||||
updateInfos.setAdminMessage(element.getElementsByTagName("adminMessage").item(0).getTextContent());
|
||||
updateInfos.setMajorChanges(element.getElementsByTagName("majorChanges").item(0)
|
||||
.getTextContent());
|
||||
|
||||
@@ -26,6 +26,14 @@ public class UserActualizationTask extends TimerTask {
|
||||
|
||||
Thread.currentThread().setName("UserActualizationTask");
|
||||
|
||||
/*
|
||||
* File-based log synchronization is optional. Do not create, open or parse the
|
||||
* configured file while the feature is disabled.
|
||||
*/
|
||||
if (!client.getChatPreferences().isLogsynch_fileBasedWkdCallInterpreterEnabled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// System.out.println("[Useract: ] Thread runned now");
|
||||
|
||||
// System.out.println("***********************Useract started");
|
||||
|
||||
@@ -119,6 +119,31 @@ public final class WorkedGrossFieldCache {
|
||||
.contains(grossField);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether a locator gross field has already been worked on any band.
|
||||
*
|
||||
* <p>The UI grid status and the "Only new grids" filter use this any-band logic.
|
||||
* This avoids user errors caused by wrong active-band settings and fits single
|
||||
* band contests such as NAC better.</p>
|
||||
*
|
||||
* @param locatorOrGrossField six-character locator or four-character gross field
|
||||
* @return true if the gross field exists in the cache on any band
|
||||
*/
|
||||
public synchronized boolean isGrossFieldWorkedAny(String locatorOrGrossField) {
|
||||
String grossField = extractGrossField(locatorOrGrossField);
|
||||
if (grossField == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (Set<String> workedGrossFields : workedGrossFieldsByBand.values()) {
|
||||
if (workedGrossFields != null && workedGrossFields.contains(grossField)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tries to normalize a locator. Accepts a plain six-character locator or extracts
|
||||
* one from exchange strings such as "001JO41HK".
|
||||
|
||||
@@ -50,7 +50,7 @@ public class ChatPreferences {
|
||||
* Reading must stay backwards compatible: missing/unknown tags should fall back to defaults.
|
||||
*/
|
||||
// private static final int CONFIG_VERSION = 2;
|
||||
public static final int CONFIG_VERSION = 3;
|
||||
public static final int CONFIG_VERSION = 4;
|
||||
|
||||
// Prefer writing tag names that mirror variable names (human readable). Keep legacy tags for compatibility.
|
||||
private static final String TAG_CONFIG_VERSION = "configVersion";
|
||||
@@ -159,6 +159,8 @@ public class ChatPreferences {
|
||||
int stn_on4kstServersPort = 23001;
|
||||
|
||||
boolean stn_pstRotatorEnabled = false;
|
||||
String stn_pstRotatorHost = "127.0.0.1";
|
||||
int stn_pstRotatorPort = 12000;
|
||||
|
||||
boolean stn_loginAFKState = false; //always start as here
|
||||
String stn_loginCallSign = "do5amf";
|
||||
@@ -238,9 +240,11 @@ public class ChatPreferences {
|
||||
/**
|
||||
* AirScout prefs
|
||||
*/
|
||||
boolean AirScout_asUDPListenerEnabled;
|
||||
String AirScout_asServerNameString, AirScout_asClientNameString, AirScout_asBandString;
|
||||
int AirScout_asCommunicationPort;
|
||||
boolean AirScout_asUDPListenerEnabled = true;
|
||||
String AirScout_asServerNameString = "AS";
|
||||
String AirScout_asClientNameString = "KST";
|
||||
String AirScout_asBandString = "1440000";
|
||||
int AirScout_asCommunicationPort = 9872;
|
||||
|
||||
/**
|
||||
* Notification prefs
|
||||
@@ -263,6 +267,9 @@ public class ChatPreferences {
|
||||
boolean notify_DXClusterServerTriggerBearing;
|
||||
boolean notify_DXClusterServerTriggerOnQRGDetect;
|
||||
|
||||
ObservableList<String>
|
||||
lstNotify_QSOSniffer_sniffedCallSignList =
|
||||
FXCollections.observableArrayList();
|
||||
// ObservableList<String> lstNotify_QSOSniffer_sniffedCallSignList = FXCollections.observableArrayList();
|
||||
ObservableList<String> lstNotify_QSOSniffer_sniffedWordsList = FXCollections.observableArrayList();
|
||||
ObservableList<String> lstNotify_QSOSniffer_sniffedPrefixLocList = FXCollections.observableArrayList();
|
||||
@@ -307,8 +314,8 @@ public class ChatPreferences {
|
||||
String messageHandling_unworkedStnRequesterBeaconsText;
|
||||
String messageHandling_beaconUnworkedstationsPrefix;
|
||||
|
||||
String messageHandling_autoAnswerTextMainCat = "Hi, sry I am not qrv, just testing new features of KST4Contest " + ApplicationConstants.APPLICATION_CURRENTVERSIONNUMBER;
|
||||
String messageHandling_autoAnswerTextSecondCat = "Hi, sry I am not qrv, just testing new features of KST4Contest " + ApplicationConstants.APPLICATION_CURRENTVERSIONNUMBER;
|
||||
String messageHandling_autoAnswerTextMainCat = "Hi, sry I am not qrv, just testing new features of KST4Contest " + ApplicationConstants.APPLICATION_CURRENT_VERSION;
|
||||
String messageHandling_autoAnswerTextSecondCat = "Hi, sry I am not qrv, just testing new features of KST4Contest " + ApplicationConstants.APPLICATION_CURRENT_VERSION;
|
||||
|
||||
boolean messageHandling_autoAnswerEnabled = false;
|
||||
boolean messageHandling_autoAnswerEnabledSecondCat = false;
|
||||
@@ -451,6 +458,11 @@ public class ChatPreferences {
|
||||
this.stn_on4kstServersDns = stn_on4kstServersDns;
|
||||
}
|
||||
|
||||
public ObservableList<String>
|
||||
getLstNotify_QSOSniffer_sniffedCallSignList() {
|
||||
return lstNotify_QSOSniffer_sniffedCallSignList;
|
||||
}
|
||||
|
||||
public ObservableList<String> getLstNotify_QSOSniffer_sniffedWordsList() {
|
||||
return lstNotify_QSOSniffer_sniffedWordsList;
|
||||
}
|
||||
@@ -761,6 +773,30 @@ public class ChatPreferences {
|
||||
this.stn_pstRotatorEnabled = stn_pstRotatorEnabled;
|
||||
}
|
||||
|
||||
public String getStn_pstRotatorHost() {
|
||||
return stn_pstRotatorHost;
|
||||
}
|
||||
|
||||
public void setStn_pstRotatorHost(String stn_pstRotatorHost) {
|
||||
if (stn_pstRotatorHost == null || stn_pstRotatorHost.isBlank()) {
|
||||
this.stn_pstRotatorHost = "127.0.0.1";
|
||||
} else {
|
||||
this.stn_pstRotatorHost = stn_pstRotatorHost.trim();
|
||||
}
|
||||
}
|
||||
|
||||
public int getStn_pstRotatorPort() {
|
||||
return stn_pstRotatorPort;
|
||||
}
|
||||
|
||||
public void setStn_pstRotatorPort(int stn_pstRotatorPort) {
|
||||
if (stn_pstRotatorPort < 1 || stn_pstRotatorPort > 65535) {
|
||||
this.stn_pstRotatorPort = 12000;
|
||||
} else {
|
||||
this.stn_pstRotatorPort = stn_pstRotatorPort;
|
||||
}
|
||||
}
|
||||
|
||||
public SimpleStringProperty getNotify_optionalFrequencyPrefix() {
|
||||
return notify_optionalFrequencyPrefix;
|
||||
}
|
||||
@@ -773,8 +809,16 @@ public class ChatPreferences {
|
||||
return notify_dxclusterServerPort;
|
||||
}
|
||||
|
||||
public void setNotify_dxclusterServerPort(int notify_dxclusterServerPort) {
|
||||
this.notify_dxclusterServerPort = notify_dxclusterServerPort;
|
||||
public void setNotify_dxclusterServerPort(
|
||||
int notify_dxclusterServerPort
|
||||
) {
|
||||
if (notify_dxclusterServerPort < 1
|
||||
|| notify_dxclusterServerPort > 65535) {
|
||||
this.notify_dxclusterServerPort = 8000;
|
||||
} else {
|
||||
this.notify_dxclusterServerPort =
|
||||
notify_dxclusterServerPort;
|
||||
}
|
||||
}
|
||||
|
||||
public double[] getGUIscn_ChatwindowMainSceneSizeHW() {
|
||||
@@ -895,35 +939,100 @@ public class ChatPreferences {
|
||||
return stn_loginCallSignRaw;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Normalizes an AirScout routing identifier.
|
||||
*
|
||||
* AirScout encloses the identifiers in quotation marks. Empty identifiers,
|
||||
* quotation marks and line breaks would therefore produce an invalid protocol
|
||||
* message and are replaced with the supplied default value.
|
||||
*
|
||||
* @param identifier configured identifier
|
||||
* @param defaultIdentifier fallback value
|
||||
* @return normalized identifier
|
||||
*/
|
||||
private String normalizeAirScoutIdentifier(
|
||||
String identifier,
|
||||
String defaultIdentifier
|
||||
) {
|
||||
if (identifier == null) {
|
||||
return defaultIdentifier;
|
||||
}
|
||||
|
||||
String normalizedIdentifier = identifier.trim();
|
||||
|
||||
if (normalizedIdentifier.isEmpty()
|
||||
|| normalizedIdentifier.contains("\"")
|
||||
|| normalizedIdentifier.contains("\r")
|
||||
|| normalizedIdentifier.contains("\n")) {
|
||||
return defaultIdentifier;
|
||||
}
|
||||
|
||||
return normalizedIdentifier;
|
||||
}
|
||||
|
||||
public String getAirScout_asBandString() {
|
||||
return AirScout_asBandString;
|
||||
}
|
||||
|
||||
public void setAirScout_asBandString(String airScout_asBandString) {
|
||||
AirScout_asBandString = airScout_asBandString;
|
||||
if (airScout_asBandString == null) {
|
||||
AirScout_asBandString = "1440000";
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
long parsedBandValue = Long.parseLong(
|
||||
airScout_asBandString.trim()
|
||||
);
|
||||
|
||||
AirScout_asBandString = parsedBandValue > 0
|
||||
? Long.toString(parsedBandValue)
|
||||
: "1440000";
|
||||
} catch (NumberFormatException exception) {
|
||||
AirScout_asBandString = "1440000";
|
||||
}
|
||||
}
|
||||
|
||||
public String getAirScout_asServerNameString() {
|
||||
return AirScout_asServerNameString;
|
||||
}
|
||||
|
||||
public void setAirScout_asServerNameString(String airScout_asServerNameString) {
|
||||
AirScout_asServerNameString = airScout_asServerNameString;
|
||||
public void setAirScout_asServerNameString(
|
||||
String airScout_asServerNameString
|
||||
) {
|
||||
AirScout_asServerNameString = normalizeAirScoutIdentifier(
|
||||
airScout_asServerNameString,
|
||||
"AS"
|
||||
);
|
||||
}
|
||||
|
||||
public String getAirScout_asClientNameString() {
|
||||
return AirScout_asClientNameString;
|
||||
}
|
||||
|
||||
public void setAirScout_asClientNameString(String airScout_asClientNameString) {
|
||||
AirScout_asClientNameString = airScout_asClientNameString;
|
||||
public void setAirScout_asClientNameString(
|
||||
String airScout_asClientNameString
|
||||
) {
|
||||
AirScout_asClientNameString = normalizeAirScoutIdentifier(
|
||||
airScout_asClientNameString,
|
||||
"KST"
|
||||
);
|
||||
}
|
||||
|
||||
public int getAirScout_asCommunicationPort() {
|
||||
return AirScout_asCommunicationPort;
|
||||
}
|
||||
|
||||
public void setAirScout_asCommunicationPort(int airScout_asCommunicationPort) {
|
||||
public void setAirScout_asCommunicationPort(
|
||||
int airScout_asCommunicationPort
|
||||
) {
|
||||
if (airScout_asCommunicationPort < 1
|
||||
|| airScout_asCommunicationPort > 65535) {
|
||||
AirScout_asCommunicationPort = 9872;
|
||||
return;
|
||||
}
|
||||
|
||||
AirScout_asCommunicationPort = airScout_asCommunicationPort;
|
||||
}
|
||||
|
||||
@@ -931,10 +1040,13 @@ public class ChatPreferences {
|
||||
return AirScout_asUDPListenerEnabled;
|
||||
}
|
||||
|
||||
public void setAirScout_asUDPListenerEnabled(boolean airScout_asUDPListenerEnabled) {
|
||||
public void setAirScout_asUDPListenerEnabled(
|
||||
boolean airScout_asUDPListenerEnabled
|
||||
) {
|
||||
AirScout_asUDPListenerEnabled = airScout_asUDPListenerEnabled;
|
||||
}
|
||||
|
||||
|
||||
public String getChatState() {
|
||||
return chatState;
|
||||
}
|
||||
@@ -1418,6 +1530,13 @@ public class ChatPreferences {
|
||||
stn_pstRotatorEnabled.setTextContent(this.stn_pstRotatorEnabled + "");
|
||||
station.appendChild(stn_pstRotatorEnabled);
|
||||
|
||||
Element stn_pstRotatorHost = doc.createElement("stn_pstRotatorHost");
|
||||
stn_pstRotatorHost.setTextContent(this.stn_pstRotatorHost);
|
||||
station.appendChild(stn_pstRotatorHost);
|
||||
|
||||
Element stn_pstRotatorPort = doc.createElement("stn_pstRotatorPort");
|
||||
stn_pstRotatorPort.setTextContent(Integer.toString(this.stn_pstRotatorPort));
|
||||
station.appendChild(stn_pstRotatorPort);
|
||||
|
||||
|
||||
/**
|
||||
@@ -1634,6 +1753,18 @@ public class ChatPreferences {
|
||||
snifferWords.appendChild(temp);
|
||||
}
|
||||
|
||||
Element snifferCallSigns =
|
||||
doc.createElement("snifferCallSigns");
|
||||
rootElement.appendChild(snifferCallSigns);
|
||||
|
||||
for (String callSign
|
||||
: lstNotify_QSOSniffer_sniffedCallSignList) {
|
||||
Element temp = doc.createElement("callSign");
|
||||
temp.setTextContent(callSign);
|
||||
snifferCallSigns.appendChild(temp);
|
||||
}
|
||||
|
||||
|
||||
Element snifferPrefixes = doc.createElement("snifferPrefixes");
|
||||
rootElement.appendChild(snifferPrefixes);
|
||||
|
||||
@@ -2092,6 +2223,22 @@ public class ChatPreferences {
|
||||
|
||||
stn_pstRotatorEnabled = getBoolean(stationEl, stn_pstRotatorEnabled, "stn_pstRotatorEnabled");
|
||||
|
||||
setStn_pstRotatorHost(
|
||||
getText(
|
||||
stationEl,
|
||||
stn_pstRotatorHost,
|
||||
"stn_pstRotatorHost"
|
||||
)
|
||||
);
|
||||
|
||||
setStn_pstRotatorPort(
|
||||
getInt(
|
||||
stationEl,
|
||||
stn_pstRotatorPort,
|
||||
"stn_pstRotatorPort"
|
||||
)
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -2216,7 +2363,13 @@ public class ChatPreferences {
|
||||
notify_dxClusterServerEnabled = getBoolean(notificationsEl, notify_dxClusterServerEnabled, "notify_dxClusterServerEnabled");
|
||||
notify_DXClusterServerTriggerBearing = getBoolean(notificationsEl, notify_DXClusterServerTriggerBearing, "notify_DXClusterServerTriggerBearing");
|
||||
notify_DXClusterServerTriggerOnQRGDetect = getBoolean(notificationsEl, notify_DXClusterServerTriggerOnQRGDetect, "notify_DXClusterServerTriggerOnQRGDetect");
|
||||
notify_dxclusterServerPort = getInt(notificationsEl, notify_dxclusterServerPort, "notify_dxclusterServerPort");
|
||||
setNotify_dxclusterServerPort(
|
||||
getInt(
|
||||
notificationsEl,
|
||||
notify_dxclusterServerPort,
|
||||
"notify_dxclusterServerPort"
|
||||
)
|
||||
);
|
||||
|
||||
String spotter = getText(notificationsEl, null, "notify_DXCSrv_SpottersCallSign");
|
||||
if (spotter != null) {
|
||||
@@ -2267,15 +2420,58 @@ public class ChatPreferences {
|
||||
|
||||
Element airScoutEl = getFirstElement(doc, "AirScoutQuerier");
|
||||
if (airScoutEl != null) {
|
||||
AirScout_asUDPListenerEnabled = getBoolean(airScoutEl, AirScout_asUDPListenerEnabled, "asQry_airScoutCommunicationEnabled");
|
||||
AirScout_asServerNameString = getText(airScoutEl, AirScout_asServerNameString, "asQry_airScoutServerName");
|
||||
AirScout_asClientNameString = getText(airScoutEl, AirScout_asClientNameString, "asQry_airScoutClientName");
|
||||
AirScout_asCommunicationPort = getInt(airScoutEl, AirScout_asCommunicationPort, "asQry_airScoutUDPPort");
|
||||
AirScout_asBandString = getText(airScoutEl, AirScout_asBandString, "asQry_airScoutBandValue");
|
||||
setAirScout_asUDPListenerEnabled(
|
||||
getBoolean(
|
||||
airScoutEl,
|
||||
AirScout_asUDPListenerEnabled,
|
||||
"asQry_airScoutCommunicationEnabled"
|
||||
)
|
||||
);
|
||||
|
||||
setAirScout_asServerNameString(
|
||||
getText(
|
||||
airScoutEl,
|
||||
AirScout_asServerNameString,
|
||||
"asQry_airScoutServerName"
|
||||
)
|
||||
);
|
||||
|
||||
setAirScout_asClientNameString(
|
||||
getText(
|
||||
airScoutEl,
|
||||
AirScout_asClientNameString,
|
||||
"asQry_airScoutClientName"
|
||||
)
|
||||
);
|
||||
|
||||
setAirScout_asCommunicationPort(
|
||||
getInt(
|
||||
airScoutEl,
|
||||
AirScout_asCommunicationPort,
|
||||
"asQry_airScoutUDPPort"
|
||||
)
|
||||
);
|
||||
|
||||
setAirScout_asBandString(
|
||||
getText(
|
||||
airScoutEl,
|
||||
AirScout_asBandString,
|
||||
"asQry_airScoutBandValue"
|
||||
)
|
||||
);
|
||||
|
||||
System.out.println(
|
||||
"[ChatPreferences, info]: AirScout querier enabled=" + AirScout_asUDPListenerEnabled
|
||||
+ ", band=" + AirScout_asBandString);
|
||||
"[ChatPreferences, info]: AirScout integration enabled="
|
||||
+ AirScout_asUDPListenerEnabled
|
||||
+ ", server identifier="
|
||||
+ AirScout_asServerNameString
|
||||
+ ", client identifier="
|
||||
+ AirScout_asClientNameString
|
||||
+ ", port="
|
||||
+ AirScout_asCommunicationPort
|
||||
+ ", band="
|
||||
+ AirScout_asBandString
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -2325,6 +2521,45 @@ public class ChatPreferences {
|
||||
/**
|
||||
* Case QSO-sniffer lists (added later; older configs won't have them)
|
||||
*/
|
||||
|
||||
list = doc.getElementsByTagName("snifferCallSigns");
|
||||
|
||||
if (list != null && list.getLength() != 0) {
|
||||
lstNotify_QSOSniffer_sniffedCallSignList.clear();
|
||||
|
||||
for (int temp = 0; temp < list.getLength(); temp++) {
|
||||
Node node = list.item(temp);
|
||||
|
||||
if (node.getNodeType() == Node.ELEMENT_NODE) {
|
||||
NodeList children = node.getChildNodes();
|
||||
|
||||
for (int i = 0; i < children.getLength(); i++) {
|
||||
Node child = children.item(i);
|
||||
|
||||
if (child.getNodeType()
|
||||
== Node.ELEMENT_NODE) {
|
||||
String callSign =
|
||||
child.getTextContent();
|
||||
|
||||
if (callSign != null
|
||||
&& !callSign.isBlank()) {
|
||||
String normalizedCallSign =
|
||||
callSign
|
||||
.trim()
|
||||
.toUpperCase();
|
||||
|
||||
if (!lstNotify_QSOSniffer_sniffedCallSignList
|
||||
.contains(normalizedCallSign)) {
|
||||
lstNotify_QSOSniffer_sniffedCallSignList
|
||||
.add(normalizedCallSign);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
list = doc.getElementsByTagName("snifferWords");
|
||||
if (list != null && list.getLength() != 0) {
|
||||
// reset to avoid duplicates when reloading
|
||||
|
||||
@@ -3,7 +3,10 @@ package kst4contest.model;
|
||||
import java.util.ArrayList;
|
||||
|
||||
public class UpdateInformation {
|
||||
double latestVersionNumberOnServer = 1.26; //dummy value to prevent nullpointerexc
|
||||
// double latestVersionNumberOnServer = 1.26; //dummy value to prevent nullpointerexc
|
||||
double latestVersionNumberOnServer = 0.0;
|
||||
String latestSemanticVersionOnServer = "";
|
||||
|
||||
String adminMessage ="";
|
||||
String majorChanges ="";
|
||||
String latestVersionPathOnWebserver="";
|
||||
@@ -36,6 +39,27 @@ public class UpdateInformation {
|
||||
public void setLatestVersionNumberOnServer(double latestVersionNumberOnServer) {
|
||||
this.latestVersionNumberOnServer = latestVersionNumberOnServer;
|
||||
}
|
||||
public String getLatestSemanticVersionOnServer() {
|
||||
return latestSemanticVersionOnServer;
|
||||
}
|
||||
|
||||
public void setLatestSemanticVersionOnServer(String latestSemanticVersionOnServer) {
|
||||
this.latestSemanticVersionOnServer =
|
||||
latestSemanticVersionOnServer == null ? "" : latestSemanticVersionOnServer.trim();
|
||||
}
|
||||
|
||||
public boolean hasSemanticVersion() {
|
||||
return !latestSemanticVersionOnServer.isBlank();
|
||||
}
|
||||
|
||||
public String getLatestVersionForDisplay() {
|
||||
if (hasSemanticVersion()) {
|
||||
return latestSemanticVersionOnServer;
|
||||
}
|
||||
|
||||
return Double.toString(latestVersionNumberOnServer);
|
||||
}
|
||||
|
||||
|
||||
public String getAdminMessage() {
|
||||
return adminMessage;
|
||||
|
||||
@@ -1,13 +1,18 @@
|
||||
package kst4contest.utils;
|
||||
|
||||
import javafx.scene.media.Media;
|
||||
import javafx.scene.media.MediaPlayer;
|
||||
import javazoom.jl.decoder.JavaLayerException;
|
||||
import javazoom.jl.player.Player;
|
||||
import kst4contest.ApplicationConstants;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.BufferedInputStream;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
import java.util.Arrays;
|
||||
import java.util.LinkedList;
|
||||
import java.util.Queue;
|
||||
import java.util.concurrent.ConcurrentLinkedQueue;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
/**
|
||||
* This part of the client drives the sounds. Its a singleton instance. All audio outs are directed to this instance.<br>
|
||||
@@ -113,8 +118,16 @@ public class PlayAudioUtils {
|
||||
|
||||
}
|
||||
|
||||
private Queue<Media> musicList = new LinkedList<Media>();
|
||||
private MediaPlayer mediaPlayer ;
|
||||
private final Queue<String> musicList = new ConcurrentLinkedQueue<>();
|
||||
/** True once audio fails; prevents repeated error logs and further play attempts. */
|
||||
private final AtomicBoolean audioUnavailable = new AtomicBoolean(false);
|
||||
/** True while the drain loop is running; prevents duplicate concurrent drains. */
|
||||
private final AtomicBoolean playing = new AtomicBoolean(false);
|
||||
private final ExecutorService audioThread = Executors.newSingleThreadExecutor(r -> {
|
||||
Thread t = new Thread(r, "audio-player");
|
||||
t.setDaemon(true);
|
||||
return t;
|
||||
});
|
||||
|
||||
/**
|
||||
* Plays notification sounds out of the windws 95 box by given action character<br/>
|
||||
@@ -131,40 +144,31 @@ public class PlayAudioUtils {
|
||||
*/
|
||||
public void playNoiseLauncher(char actionChar) {
|
||||
|
||||
switch (actionChar){
|
||||
case '-':
|
||||
musicList.add(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/tick.mp3"));
|
||||
break;
|
||||
case '!':
|
||||
musicList.add(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/NOISESTARTUP.mp3"));
|
||||
break;
|
||||
case 'C':
|
||||
musicList.add(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/NOISECQWINDOW.mp3"));
|
||||
break;
|
||||
case 'P':
|
||||
musicList.add(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/NOISEPMWINDOW.mp3"));
|
||||
break;
|
||||
case 'E':
|
||||
musicList.add(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/NOISEERROR.mp3"));
|
||||
break;
|
||||
case 'N':
|
||||
musicList.add(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/NOISENOTIFY.mp3"));
|
||||
break;
|
||||
|
||||
|
||||
// ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/NOISESTARTUP.mp3");
|
||||
|
||||
|
||||
switch (actionChar){
|
||||
case '-':
|
||||
musicList.add(new Media(new File (ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/tick.mp3")).toURI().toString()));
|
||||
break;
|
||||
case '!':
|
||||
musicList.add(new Media(new File (ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/NOISESTARTUP.mp3")).toURI().toString()));
|
||||
break;
|
||||
case 'C':
|
||||
musicList.add(new Media(new File (ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/NOISECQWINDOW.mp3")).toURI().toString()));
|
||||
break;
|
||||
case 'P':
|
||||
musicList.add(new Media(new File (ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/NOISEPMWINDOW.mp3")).toURI().toString()));
|
||||
break;
|
||||
case 'E':
|
||||
musicList.add(new Media(new File (ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/NOISEERROR.mp3")).toURI().toString()));
|
||||
break;
|
||||
case 'N':
|
||||
musicList.add(new Media(new File (ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/NOISENOTIFY.mp3")).toURI().toString()));
|
||||
break;
|
||||
// case 'M':
|
||||
// musicList.add(new Media(new File ("VOICE.mp3").toURI().toString()));
|
||||
// break;
|
||||
|
||||
default:
|
||||
System.out.println("[KST4ContestApp, warning, letter not defined!]");
|
||||
default:
|
||||
System.out.println("[KST4ContestApp, warning, letter not defined!]");
|
||||
|
||||
}
|
||||
playMusic();
|
||||
// mediaPlayer.dispose();
|
||||
|
||||
}
|
||||
|
||||
@@ -183,125 +187,124 @@ public class PlayAudioUtils {
|
||||
for (char letterToPlay: playThisInCW){
|
||||
switch (letterToPlay){
|
||||
case 'A':
|
||||
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/LTTRA.mp3")).toURI().toString()));
|
||||
musicList.add(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/LTTRA.mp3"));
|
||||
break;
|
||||
case 'B':
|
||||
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/LTTRB.mp3")).toURI().toString()));
|
||||
musicList.add(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/LTTRB.mp3"));
|
||||
break;
|
||||
case 'C':
|
||||
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/LTTRC.mp3")).toURI().toString()));
|
||||
musicList.add(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/LTTRC.mp3"));
|
||||
break;
|
||||
case 'D':
|
||||
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/LTTRD.mp3")).toURI().toString()));
|
||||
musicList.add(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/LTTRD.mp3"));
|
||||
break;
|
||||
case 'E':
|
||||
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/LTTRE.mp3")).toURI().toString()));
|
||||
musicList.add(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/LTTRE.mp3"));
|
||||
break;
|
||||
case 'F':
|
||||
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/LTTRF.mp3")).toURI().toString()));
|
||||
musicList.add(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/LTTRF.mp3"));
|
||||
break;
|
||||
case 'G':
|
||||
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/LTTRG.mp3")).toURI().toString()));
|
||||
musicList.add(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/LTTRG.mp3"));
|
||||
break;
|
||||
case 'H':
|
||||
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/LTTRH.mp3")).toURI().toString()));
|
||||
musicList.add(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/LTTRH.mp3"));
|
||||
break;
|
||||
case 'I':
|
||||
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/LTTRI.mp3")).toURI().toString()));
|
||||
musicList.add(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/LTTRI.mp3"));
|
||||
break;
|
||||
case 'J':
|
||||
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/LTTRJ.mp3")).toURI().toString()));
|
||||
musicList.add(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/LTTRJ.mp3"));
|
||||
break;
|
||||
case 'K':
|
||||
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/LTTRK.mp3")).toURI().toString()));
|
||||
musicList.add(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/LTTRK.mp3"));
|
||||
break;
|
||||
case 'L':
|
||||
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/LTTRL.mp3")).toURI().toString()));
|
||||
musicList.add(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/LTTRL.mp3"));
|
||||
break;
|
||||
case 'M':
|
||||
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/LTTRM.mp3")).toURI().toString()));
|
||||
musicList.add(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/LTTRM.mp3"));
|
||||
break;
|
||||
case 'N':
|
||||
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/LTTRN.mp3")).toURI().toString()));
|
||||
musicList.add(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/LTTRN.mp3"));
|
||||
break;
|
||||
case 'O':
|
||||
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/LTTRO.mp3")).toURI().toString()));
|
||||
musicList.add(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/LTTRO.mp3"));
|
||||
break;
|
||||
case 'P':
|
||||
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/LTTRP.mp3")).toURI().toString()));
|
||||
musicList.add(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/LTTRP.mp3"));
|
||||
break;
|
||||
case 'Q':
|
||||
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/LTTRQ.mp3")).toURI().toString()));
|
||||
musicList.add(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/LTTRQ.mp3"));
|
||||
break;
|
||||
case 'R':
|
||||
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/LTTRR.mp3")).toURI().toString()));
|
||||
musicList.add(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/LTTRR.mp3"));
|
||||
break;
|
||||
case 'S':
|
||||
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/LTTRS.mp3")).toURI().toString()));
|
||||
musicList.add(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/LTTRS.mp3"));
|
||||
break;
|
||||
case 'T':
|
||||
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/LTTRT.mp3")).toURI().toString()));
|
||||
musicList.add(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/LTTRT.mp3"));
|
||||
break;
|
||||
case 'U':
|
||||
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/LTTRU.mp3")).toURI().toString()));
|
||||
musicList.add(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/LTTRU.mp3"));
|
||||
break;
|
||||
case 'V':
|
||||
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/LTTRV.mp3")).toURI().toString()));
|
||||
musicList.add(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/LTTRV.mp3"));
|
||||
break;
|
||||
case 'W':
|
||||
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/LTTRW.mp3")).toURI().toString()));
|
||||
musicList.add(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/LTTRW.mp3"));
|
||||
break;
|
||||
case 'X':
|
||||
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/LTTRX.mp3")).toURI().toString()));
|
||||
musicList.add(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/LTTRX.mp3"));
|
||||
break;
|
||||
case 'Y':
|
||||
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/LTTRY.mp3")).toURI().toString()));
|
||||
musicList.add(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/LTTRY.mp3"));
|
||||
break;
|
||||
case 'Z':
|
||||
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/LTTRZ.mp3")).toURI().toString()));
|
||||
musicList.add(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/LTTRZ.mp3"));
|
||||
break;
|
||||
case '1':
|
||||
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/LTTR1.mp3")).toURI().toString()));
|
||||
musicList.add(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/LTTR1.mp3"));
|
||||
break;
|
||||
case '2':
|
||||
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/LTTR2.mp3")).toURI().toString()));
|
||||
musicList.add(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/LTTR2.mp3"));
|
||||
break;
|
||||
case '3':
|
||||
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/LTTR3.mp3")).toURI().toString()));
|
||||
musicList.add(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/LTTR3.mp3"));
|
||||
break;
|
||||
case '4':
|
||||
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/LTTR4.mp3")).toURI().toString()));
|
||||
musicList.add(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/LTTR4.mp3"));
|
||||
break;
|
||||
case '5':
|
||||
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/LTTR5.mp3")).toURI().toString()));
|
||||
musicList.add(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/LTTR5.mp3"));
|
||||
break;
|
||||
case '6':
|
||||
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/LTTR6.mp3")).toURI().toString()));
|
||||
musicList.add(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/LTTR6.mp3"));
|
||||
break;
|
||||
case '7':
|
||||
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/LTTR7.mp3")).toURI().toString()));
|
||||
musicList.add(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/LTTR7.mp3"));
|
||||
break;
|
||||
case '8':
|
||||
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/LTTR8.mp3")).toURI().toString()));
|
||||
musicList.add(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/LTTR8.mp3"));
|
||||
break;
|
||||
case '9':
|
||||
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/LTTR9.mp3")).toURI().toString()));
|
||||
musicList.add(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/LTTR9.mp3"));
|
||||
break;
|
||||
case '0':
|
||||
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/LTTR0.mp3")).toURI().toString()));
|
||||
musicList.add(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/LTTR0.mp3"));
|
||||
break;
|
||||
case '/':
|
||||
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/LTTRSTROKE.mp3")).toURI().toString()));
|
||||
musicList.add(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/LTTRSTROKE.mp3"));
|
||||
break;
|
||||
case ' ':
|
||||
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/LTTRSPACE.mp3")).toURI().toString()));
|
||||
musicList.add(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/LTTRSPACE.mp3"));
|
||||
break;
|
||||
default:
|
||||
System.out.println("[KST4ContestApp, warning, letter not defined:] cwLetters = " + Arrays.toString(playThisInCW));
|
||||
}
|
||||
}
|
||||
playMusic();
|
||||
// mediaPlayer.dispose();
|
||||
|
||||
}
|
||||
|
||||
@@ -323,162 +326,166 @@ public class PlayAudioUtils {
|
||||
for (char letterToPlay: spellThisWithVoice){
|
||||
switch (letterToPlay){
|
||||
case '!':
|
||||
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/VOICEBELL.mp3")).toURI().toString()));
|
||||
musicList.add(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/VOICEBELL.mp3"));
|
||||
break;
|
||||
case '?':
|
||||
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/VOICEYOUGOTMAIL.mp3")).toURI().toString()));
|
||||
musicList.add(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/VOICEYOUGOTMAIL.mp3"));
|
||||
break;
|
||||
case '#':
|
||||
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/VOICEHELLO.mp3")).toURI().toString()));
|
||||
musicList.add(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/VOICEHELLO.mp3"));
|
||||
break;
|
||||
case '*':
|
||||
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/VOICE73.mp3")).toURI().toString()));
|
||||
musicList.add(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/VOICE73.mp3"));
|
||||
break;
|
||||
case '$':
|
||||
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/VOICESTROKEPORTABLE.mp3")).toURI().toString()));
|
||||
musicList.add(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/VOICESTROKEPORTABLE.mp3"));
|
||||
break;
|
||||
case 'A':
|
||||
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/VOICEA.mp3")).toURI().toString()));
|
||||
musicList.add(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/VOICEA.mp3"));
|
||||
break;
|
||||
case 'B':
|
||||
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/VOICEB.mp3")).toURI().toString()));
|
||||
musicList.add(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/VOICEB.mp3"));
|
||||
break;
|
||||
case 'C':
|
||||
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/VOICEC.mp3")).toURI().toString()));
|
||||
musicList.add(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/VOICEC.mp3"));
|
||||
break;
|
||||
case 'D':
|
||||
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/VOICED.mp3")).toURI().toString()));
|
||||
musicList.add(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/VOICED.mp3"));
|
||||
break;
|
||||
case 'E':
|
||||
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/VOICEE.mp3")).toURI().toString()));
|
||||
musicList.add(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/VOICEE.mp3"));
|
||||
break;
|
||||
case 'F':
|
||||
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/VOICEF.mp3")).toURI().toString()));
|
||||
musicList.add(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/VOICEF.mp3"));
|
||||
break;
|
||||
case 'G':
|
||||
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/VOICEG.mp3")).toURI().toString()));
|
||||
musicList.add(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/VOICEG.mp3"));
|
||||
break;
|
||||
case 'H':
|
||||
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/VOICEH.mp3")).toURI().toString()));
|
||||
musicList.add(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/VOICEH.mp3"));
|
||||
break;
|
||||
case 'I':
|
||||
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/VOICEI.mp3")).toURI().toString()));
|
||||
musicList.add(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/VOICEI.mp3"));
|
||||
break;
|
||||
case 'J':
|
||||
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/VOICEJ.mp3")).toURI().toString()));
|
||||
musicList.add(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/VOICEJ.mp3"));
|
||||
break;
|
||||
case 'K':
|
||||
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/VOICEK.mp3")).toURI().toString()));
|
||||
musicList.add(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/VOICEK.mp3"));
|
||||
break;
|
||||
case 'L':
|
||||
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/VOICEL.mp3")).toURI().toString()));
|
||||
musicList.add(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/VOICEL.mp3"));
|
||||
break;
|
||||
case 'M':
|
||||
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/VOICEM.mp3")).toURI().toString()));
|
||||
musicList.add(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/VOICEM.mp3"));
|
||||
break;
|
||||
case 'N':
|
||||
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/VOICEN.mp3")).toURI().toString()));
|
||||
musicList.add(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/VOICEN.mp3"));
|
||||
break;
|
||||
case 'O':
|
||||
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/VOICEO.mp3")).toURI().toString()));
|
||||
musicList.add(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/VOICEO.mp3"));
|
||||
break;
|
||||
case 'P':
|
||||
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/VOICEP.mp3")).toURI().toString()));
|
||||
musicList.add(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/VOICEP.mp3"));
|
||||
break;
|
||||
case 'Q':
|
||||
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/VOICEQ.mp3")).toURI().toString()));
|
||||
musicList.add(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/VOICEQ.mp3"));
|
||||
break;
|
||||
case 'R':
|
||||
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/VOICER.mp3")).toURI().toString()));
|
||||
musicList.add(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/VOICER.mp3"));
|
||||
break;
|
||||
case 'S':
|
||||
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/VOICES.mp3")).toURI().toString()));
|
||||
musicList.add(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/VOICES.mp3"));
|
||||
break;
|
||||
case 'T':
|
||||
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/VOICET.mp3")).toURI().toString()));
|
||||
musicList.add(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/VOICET.mp3"));
|
||||
break;
|
||||
case 'U':
|
||||
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/VOICEU.mp3")).toURI().toString()));
|
||||
musicList.add(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/VOICEU.mp3"));
|
||||
break;
|
||||
case 'V':
|
||||
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/VOICEV.mp3")).toURI().toString()));
|
||||
musicList.add(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/VOICEV.mp3"));
|
||||
break;
|
||||
case 'W':
|
||||
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/VOICEW.mp3")).toURI().toString()));
|
||||
musicList.add(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/VOICEW.mp3"));
|
||||
break;
|
||||
case 'X':
|
||||
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/VOICEX.mp3")).toURI().toString()));
|
||||
musicList.add(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/VOICEX.mp3"));
|
||||
break;
|
||||
case 'Y':
|
||||
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/VOICEY.mp3")).toURI().toString()));
|
||||
musicList.add(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/VOICEY.mp3"));
|
||||
break;
|
||||
case 'Z':
|
||||
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/VOICEZ.mp3")).toURI().toString()));
|
||||
musicList.add(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/VOICEZ.mp3"));
|
||||
break;
|
||||
case '1':
|
||||
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/VOICE1.mp3")).toURI().toString()));
|
||||
musicList.add(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/VOICE1.mp3"));
|
||||
break;
|
||||
case '2':
|
||||
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/VOICE2.mp3")).toURI().toString()));
|
||||
musicList.add(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/VOICE2.mp3"));
|
||||
break;
|
||||
case '3':
|
||||
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/VOICE3.mp3")).toURI().toString()));
|
||||
musicList.add(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/VOICE3.mp3"));
|
||||
break;
|
||||
case '4':
|
||||
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/VOICE4.mp3")).toURI().toString()));
|
||||
musicList.add(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/VOICE4.mp3"));
|
||||
break;
|
||||
case '5':
|
||||
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/VOICE5.mp3")).toURI().toString()));
|
||||
musicList.add(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/VOICE5.mp3"));
|
||||
break;
|
||||
case '6':
|
||||
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/VOICE6.mp3")).toURI().toString()));
|
||||
musicList.add(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/VOICE6.mp3"));
|
||||
break;
|
||||
case '7':
|
||||
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/VOICE7.mp3")).toURI().toString()));
|
||||
musicList.add(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/VOICE7.mp3"));
|
||||
break;
|
||||
case '8':
|
||||
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/VOICE8.mp3")).toURI().toString()));
|
||||
musicList.add(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/VOICE8.mp3"));
|
||||
break;
|
||||
case '9':
|
||||
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/VOICE9.mp3")).toURI().toString()));
|
||||
musicList.add(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/VOICE9.mp3"));
|
||||
break;
|
||||
case '0':
|
||||
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/VOICE0.mp3")).toURI().toString()));
|
||||
musicList.add(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/VOICE0.mp3"));
|
||||
break;
|
||||
case '/':
|
||||
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/VOICESTROKE.mp3")).toURI().toString()));
|
||||
musicList.add(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/VOICESTROKE.mp3"));
|
||||
break;
|
||||
// case ' ':
|
||||
// musicList.add(new Media(new File ("VOICESPACE.mp3").toURI().toString()));
|
||||
// break;
|
||||
default:
|
||||
System.out.println("[KST4ContestApp, warning, letter not defined:] cwLetters = " + Arrays.toString(spellThisWithVoice));
|
||||
}
|
||||
}
|
||||
playMusic();
|
||||
// mediaPlayer.dispose();
|
||||
|
||||
}
|
||||
private void playMusic() {
|
||||
|
||||
// System.out.println("Kst4ContestApplication.playMusic");
|
||||
if(musicList.peek() == null)
|
||||
{
|
||||
private void playMusic() {
|
||||
if (audioUnavailable.get()) {
|
||||
musicList.clear();
|
||||
return;
|
||||
}
|
||||
mediaPlayer = new MediaPlayer(musicList.poll());
|
||||
mediaPlayer.setRate(1.0);
|
||||
|
||||
mediaPlayer.setOnReady(() -> {
|
||||
mediaPlayer.play();
|
||||
mediaPlayer.setOnEndOfMedia(() -> {
|
||||
// mediaPlayer.dispose();
|
||||
playMusic();
|
||||
if (musicList.isEmpty()) {
|
||||
// mediaPlayer.dispose();
|
||||
// Only start a drain loop if none is running; the running loop will pick up new items itself.
|
||||
if (!playing.compareAndSet(false, true)) return;
|
||||
audioThread.submit(() -> {
|
||||
String path;
|
||||
while ((path = musicList.poll()) != null) {
|
||||
if (audioUnavailable.get()) break;
|
||||
try (FileInputStream fis = new FileInputStream(path);
|
||||
BufferedInputStream bis = new BufferedInputStream(fis)) {
|
||||
new Player(bis).play();
|
||||
} catch (IOException | JavaLayerException e) {
|
||||
audioUnavailable.set(true);
|
||||
musicList.clear();
|
||||
System.out.println("[KST4ContestApp, warning, audio playback disabled (could not create media player): " + e + "]");
|
||||
break;
|
||||
}
|
||||
});
|
||||
}
|
||||
playing.set(false);
|
||||
// Items may have been enqueued between the last poll() and playing.set(false);
|
||||
// restart the drain loop if so.
|
||||
if (!musicList.isEmpty() && !audioUnavailable.get()) {
|
||||
playMusic();
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
package kst4contest.utils;
|
||||
|
||||
public final class VersionUtils {
|
||||
|
||||
private VersionUtils() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Compares numeric release versions such as 1.41, 1.41.1 and 1.41.10.
|
||||
*
|
||||
* Pre-release and build suffixes are ignored because the update feed
|
||||
* currently publishes stable releases only.
|
||||
*/
|
||||
public static int compareStableVersions(String left, String right) {
|
||||
int[] leftParts = parseVersion(left);
|
||||
int[] rightParts = parseVersion(right);
|
||||
int partCount = Math.max(leftParts.length, rightParts.length);
|
||||
|
||||
for (int index = 0; index < partCount; index++) {
|
||||
int leftPart = index < leftParts.length ? leftParts[index] : 0;
|
||||
int rightPart = index < rightParts.length ? rightParts[index] : 0;
|
||||
|
||||
int comparison = Integer.compare(leftPart, rightPart);
|
||||
if (comparison != 0) {
|
||||
return comparison;
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
private static int[] parseVersion(String version) {
|
||||
if (version == null || version.isBlank()) {
|
||||
throw new IllegalArgumentException("Version must not be empty");
|
||||
}
|
||||
|
||||
String normalized = version.trim();
|
||||
|
||||
if (normalized.startsWith("v") || normalized.startsWith("V")) {
|
||||
normalized = normalized.substring(1);
|
||||
}
|
||||
|
||||
int hyphenIndex = normalized.indexOf('-');
|
||||
int plusIndex = normalized.indexOf('+');
|
||||
int suffixIndex;
|
||||
|
||||
if (hyphenIndex < 0) {
|
||||
suffixIndex = plusIndex;
|
||||
} else if (plusIndex < 0) {
|
||||
suffixIndex = hyphenIndex;
|
||||
} else {
|
||||
suffixIndex = Math.min(hyphenIndex, plusIndex);
|
||||
}
|
||||
|
||||
if (suffixIndex >= 0) {
|
||||
normalized = normalized.substring(0, suffixIndex);
|
||||
}
|
||||
|
||||
String[] textParts = normalized.split("\\.");
|
||||
int[] numericParts = new int[textParts.length];
|
||||
|
||||
for (int index = 0; index < textParts.length; index++) {
|
||||
numericParts[index] = Integer.parseInt(textParts[index]);
|
||||
}
|
||||
|
||||
return numericParts;
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -20,6 +20,7 @@ public record MapCallsignRawSnapshot(
|
||||
double longitudeDeg,
|
||||
String bandSummary,
|
||||
Map<String, String> lastKnownFrequenciesByBand,
|
||||
boolean offersSelectedBand,
|
||||
boolean warningToMyDirection,
|
||||
boolean worked,
|
||||
boolean selected,
|
||||
@@ -59,9 +60,10 @@ public record MapCallsignRawSnapshot(
|
||||
}
|
||||
|
||||
public String markerLabel() {
|
||||
return bandSummary.isBlank()
|
||||
String baseLabel = bandSummary.isBlank()
|
||||
? displayCallSign
|
||||
: displayCallSign + " (" + bandSummary + ")";
|
||||
return offersSelectedBand ? baseLabel + " B+" : baseLabel;
|
||||
}
|
||||
|
||||
public String detailFrequencyText() {
|
||||
|
||||
@@ -8,11 +8,13 @@ import kst4contest.model.ChatMember;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Comparator;
|
||||
import java.util.EnumSet;
|
||||
import java.util.EnumMap;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* Builds immutable map snapshots from the currently visible chat members.
|
||||
@@ -22,8 +24,11 @@ import java.util.Map;
|
||||
*/
|
||||
public final class MapCallsignRawSnapshotBuilder {
|
||||
|
||||
private static final Pattern TOKEN_SPLIT_PATTERN = Pattern.compile("[^A-Z0-9]+");
|
||||
|
||||
public List<MapCallsignRawSnapshot> buildSnapshots(Collection<ChatMember> visibleChatMembers,
|
||||
ChatMember selectedChatMember) {
|
||||
ChatMember selectedChatMember,
|
||||
EnumSet<Band> selectedBands) {
|
||||
|
||||
if (visibleChatMembers == null || visibleChatMembers.isEmpty()) {
|
||||
return List.of();
|
||||
@@ -69,7 +74,9 @@ public final class MapCallsignRawSnapshotBuilder {
|
||||
Location location = new Location(locator6);
|
||||
|
||||
LinkedHashMap<String, String> frequenciesByBand = collectLastKnownFrequenciesByBand(variants);
|
||||
String bandSummary = String.join(", ", frequenciesByBand.keySet());
|
||||
EnumSet<Band> sureBands = collectSureBands(variants, frequenciesByBand);
|
||||
String bandSummary = buildBandSummary(sureBands);
|
||||
boolean offersSelectedBand = hasAnySelectedBand(sureBands, selectedBands);
|
||||
|
||||
boolean warningToMyDirection = variants.stream().anyMatch(ChatMember::isInAngleAndRange);
|
||||
boolean worked = variants.stream().anyMatch(this::isWorkedAtAnyBand);
|
||||
@@ -92,6 +99,7 @@ public final class MapCallsignRawSnapshotBuilder {
|
||||
location.getLongitude().toDegrees(),
|
||||
bandSummary,
|
||||
frequenciesByBand,
|
||||
offersSelectedBand,
|
||||
warningToMyDirection,
|
||||
worked,
|
||||
selected,
|
||||
@@ -182,6 +190,101 @@ public final class MapCallsignRawSnapshotBuilder {
|
||||
return ordered;
|
||||
}
|
||||
|
||||
private EnumSet<Band> collectSureBands(List<ChatMember> variants,
|
||||
LinkedHashMap<String, String> frequenciesByBand) {
|
||||
EnumSet<Band> sureBands = EnumSet.noneOf(Band.class);
|
||||
|
||||
if (frequenciesByBand != null) {
|
||||
for (String label : frequenciesByBand.keySet()) {
|
||||
Band mappedBand = bandFromDisplayLabel(label);
|
||||
if (mappedBand != null) {
|
||||
sureBands.add(mappedBand);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (ChatMember variant : variants) {
|
||||
if (variant == null || variant.getName() == null || variant.getName().isBlank()) {
|
||||
continue;
|
||||
}
|
||||
sureBands.addAll(detectBandsFromStationName(variant.getName()));
|
||||
}
|
||||
|
||||
return sureBands;
|
||||
}
|
||||
|
||||
private EnumSet<Band> detectBandsFromStationName(String stationName) {
|
||||
EnumSet<Band> detectedBands = EnumSet.noneOf(Band.class);
|
||||
if (stationName == null || stationName.isBlank()) {
|
||||
return detectedBands;
|
||||
}
|
||||
|
||||
String normalized = stationName.toUpperCase(Locale.ROOT);
|
||||
String[] tokens = TOKEN_SPLIT_PATTERN.split(normalized);
|
||||
for (String token : tokens) {
|
||||
if (token == null || token.isBlank()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
switch (token) {
|
||||
case "2", "2M", "144", "144MHZ" -> detectedBands.add(Band.B_144);
|
||||
case "70", "70CM", "432", "432MHZ" -> detectedBands.add(Band.B_432);
|
||||
case "23", "23CM", "1296", "1296MHZ" -> detectedBands.add(Band.B_1296);
|
||||
case "13", "13CM", "2300", "2320", "2320MHZ" -> detectedBands.add(Band.B_2320);
|
||||
case "9", "9CM", "3400", "3400MHZ" -> detectedBands.add(Band.B_3400);
|
||||
case "6", "6CM", "5600", "5760", "5760MHZ" -> detectedBands.add(Band.B_5760);
|
||||
case "3", "3CM", "10G", "10GHZ", "10368", "10368MHZ" -> detectedBands.add(Band.B_10G);
|
||||
case "24G", "24GHZ", "24048", "24048MHZ" -> detectedBands.add(Band.B_24G);
|
||||
default -> {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return detectedBands;
|
||||
}
|
||||
|
||||
private String buildBandSummary(EnumSet<Band> sureBands) {
|
||||
if (sureBands == null || sureBands.isEmpty()) {
|
||||
return "";
|
||||
}
|
||||
|
||||
List<String> labels = sureBands.stream()
|
||||
.sorted()
|
||||
.map(this::toBandDisplayLabel)
|
||||
.toList();
|
||||
return String.join(", ", labels);
|
||||
}
|
||||
|
||||
private boolean hasAnySelectedBand(EnumSet<Band> sureBands, EnumSet<Band> selectedBands) {
|
||||
if (sureBands == null || sureBands.isEmpty() || selectedBands == null || selectedBands.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
for (Band band : selectedBands) {
|
||||
if (sureBands.contains(band)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private Band bandFromDisplayLabel(String label) {
|
||||
if (label == null || label.isBlank()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return switch (label) {
|
||||
case "144" -> Band.B_144;
|
||||
case "432" -> Band.B_432;
|
||||
case "1296" -> Band.B_1296;
|
||||
case "2320" -> Band.B_2320;
|
||||
case "3400" -> Band.B_3400;
|
||||
case "5760" -> Band.B_5760;
|
||||
case "10368" -> Band.B_10G;
|
||||
case "24048" -> Band.B_24G;
|
||||
default -> null;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Fallback for stations where the current displayed QRG exists but the
|
||||
* knownActiveBands history has not yet been filled.
|
||||
|
||||
@@ -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;
|
||||
@@ -214,15 +323,17 @@ public final class MapHtmlResources {
|
||||
<body class="kst-theme-light">
|
||||
<div id="map"></div>
|
||||
|
||||
<script>
|
||||
// JavaFX 21 WebView can render Leaflet's CSS translate3d tile positioning incorrectly.
|
||||
// Disable Leaflet 3D transforms before leaflet.js is loaded to keep tile rendering stable.
|
||||
window.L_DISABLE_3D = true;
|
||||
</script>
|
||||
<script>
|
||||
""" + leafletJs + """
|
||||
</script>
|
||||
<script>window._kstTileProxyPort=__TILE_PROXY_PORT__;</script>
|
||||
|
||||
<script>
|
||||
|
||||
|
||||
|
||||
window.kstMapApi = (function () {
|
||||
let map;
|
||||
let stationLayer;
|
||||
@@ -230,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));
|
||||
@@ -353,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;
|
||||
@@ -366,18 +868,43 @@ public final class MapHtmlResources {
|
||||
applyThemeClass();
|
||||
|
||||
map = L.map('map', {
|
||||
zoomControl: true
|
||||
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,
|
||||
inertia: false
|
||||
}).setView([51.0, 10.0], 6);
|
||||
|
||||
jsLog('Leaflet map initialized');
|
||||
|
||||
L.tileLayer(
|
||||
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>'
|
||||
}
|
||||
).addTo(map);
|
||||
);
|
||||
|
||||
tileLayer.on('tileerror', function (event) {
|
||||
const source = event && event.tile ? event.tile.src : 'unknown';
|
||||
jsError('OSM tile load failed: ' + source);
|
||||
});
|
||||
|
||||
tileLayer.addTo(map);
|
||||
|
||||
map.createPane('beamPane');
|
||||
map.getPane('beamPane').style.zIndex = 410;
|
||||
@@ -401,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();
|
||||
});
|
||||
|
||||
@@ -416,37 +943,81 @@ public final class MapHtmlResources {
|
||||
}
|
||||
|
||||
function invalidateSize() {
|
||||
if (!map) {
|
||||
return;
|
||||
}
|
||||
|
||||
jsLog('invalidateSize');
|
||||
|
||||
map.invalidateSize(false);
|
||||
|
||||
if (invalidateNotifyTimer) {
|
||||
window.clearTimeout(invalidateNotifyTimer);
|
||||
}
|
||||
|
||||
invalidateNotifyTimer = window.setTimeout(function () {
|
||||
notifyViewport();
|
||||
}, 120);
|
||||
}
|
||||
|
||||
function zoomIn() {
|
||||
if (!map) {
|
||||
return;
|
||||
}
|
||||
|
||||
map.setZoom(map.getZoom() + 1, { animate: false });
|
||||
const mapElement = document.getElementById('map');
|
||||
const domWidth = mapElement ? mapElement.clientWidth : -1;
|
||||
const domHeight = mapElement ? mapElement.clientHeight : -1;
|
||||
|
||||
jsLog('invalidateSize dom=' + domWidth + 'x' + domHeight
|
||||
+ ' leafletBefore=' + map.getSize().x + 'x' + map.getSize().y);
|
||||
|
||||
window.requestAnimationFrame(function () {
|
||||
map.invalidateSize({
|
||||
animate: false,
|
||||
pan: false,
|
||||
debounceMoveend: true
|
||||
});
|
||||
|
||||
window.setTimeout(function () {
|
||||
map.invalidateSize({
|
||||
animate: false,
|
||||
pan: false,
|
||||
debounceMoveend: true
|
||||
});
|
||||
|
||||
jsLog('invalidateSize leafletAfter=' + map.getSize().x + 'x' + map.getSize().y);
|
||||
|
||||
if (invalidateNotifyTimer) {
|
||||
window.clearTimeout(invalidateNotifyTimer);
|
||||
}
|
||||
|
||||
invalidateNotifyTimer = window.setTimeout(function () {
|
||||
notifyViewport();
|
||||
}, 120);
|
||||
}, 80);
|
||||
});
|
||||
}
|
||||
|
||||
function resize() {
|
||||
invalidateSize();
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
|
||||
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() {
|
||||
@@ -479,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();
|
||||
@@ -499,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) {
|
||||
@@ -508,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) {
|
||||
@@ -549,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, {
|
||||
@@ -578,8 +1140,6 @@ public final class MapHtmlResources {
|
||||
return;
|
||||
}
|
||||
|
||||
jsLog('setConnection');
|
||||
|
||||
L.polyline(points.map(point => [point.lat, point.lon]), {
|
||||
pane: 'connectionPane',
|
||||
color: connectionColor(),
|
||||
@@ -628,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(
|
||||
@@ -669,47 +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,
|
||||
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));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,7 @@ import kst4contest.model.ChatPreferences;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.EnumSet;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Objects;
|
||||
@@ -142,8 +143,13 @@ public final class StationMapBridge {
|
||||
private void refreshNow() {
|
||||
List<ChatMember> visibleChatMembers = new ArrayList<>(chatController.getLst_chatMemberSortedFilteredList());
|
||||
ChatMember selectedChatMember = chatController.getScoreService().getSelectedChatMember();
|
||||
EnumSet<Band> selectedBands = chatController.getReachabilityService().getEnabledStationBands();
|
||||
|
||||
List<MapCallsignRawSnapshot> snapshots = snapshotBuilder.buildSnapshots(visibleChatMembers, selectedChatMember);
|
||||
List<MapCallsignRawSnapshot> snapshots = snapshotBuilder.buildSnapshots(
|
||||
visibleChatMembers,
|
||||
selectedChatMember,
|
||||
selectedBands
|
||||
);
|
||||
|
||||
MapCallsignRawSnapshot selectedSnapshot = null;
|
||||
if (selectedChatMember != null && selectedChatMember.getCallSignRaw() != null) {
|
||||
|
||||
@@ -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("-");
|
||||
|
||||
@@ -49,6 +57,17 @@ public final class StationMapView {
|
||||
private final Stage stage = new Stage();
|
||||
private final WebView webView = new WebView();
|
||||
private final WebEngine webEngine = webView.getEngine();
|
||||
|
||||
/**
|
||||
* Keep a strong Java reference to the bridge object.
|
||||
*
|
||||
* JavaFX WebView/JSObject does not guarantee that a Java object passed through
|
||||
* window.setMember(...) remains strongly reachable from the Java side. Newer
|
||||
* JavaFX/WebKit/GC combinations can otherwise lose callbacks after a while.
|
||||
*/
|
||||
private final JavaMapBridge javaMapBridge = new JavaMapBridge();
|
||||
|
||||
|
||||
private TileProxyServer tileProxyServer;
|
||||
|
||||
private Scene scene;
|
||||
@@ -85,6 +104,22 @@ public final class StationMapView {
|
||||
private List<MapCallsignRawSnapshot> lastSnapshots = List.of();
|
||||
private MapCallsignRawSnapshot lastSelectedSnapshot;
|
||||
private boolean filteredViewActive;
|
||||
/**
|
||||
* Last JSON payloads sent into Leaflet. Used to avoid clearing/recreating
|
||||
* layers every score/table refresh. Rebuilding DOM markers every few seconds
|
||||
* is visible as flicker in JavaFX 21 WebView.
|
||||
*/
|
||||
private String lastRenderedStationsJson = "";
|
||||
private String lastRenderedBeamJson = "";
|
||||
private String lastRenderedConnectionJson = "";
|
||||
private String lastRenderedGridJson = "";
|
||||
|
||||
/**
|
||||
* Prevent periodic refreshes from panning the map back to the selected station.
|
||||
* Explicit calls to focusCallsignRaw(...) still pan immediately.
|
||||
*/
|
||||
private String lastAutoFocusedCallsignRaw = "";
|
||||
|
||||
|
||||
private double homeLatitudeDeg = Double.NaN;
|
||||
private double homeLongitudeDeg = Double.NaN;
|
||||
@@ -192,8 +227,11 @@ public final class StationMapView {
|
||||
return;
|
||||
}
|
||||
|
||||
String normalizedCallsignRaw = callSignRaw.trim().toUpperCase(Locale.ROOT);
|
||||
lastAutoFocusedCallsignRaw = normalizedCallsignRaw;
|
||||
|
||||
executeMapScriptSafely(
|
||||
"window.kstMapApi.focusCallsignRaw(" + toJsStringLiteral(callSignRaw.trim().toUpperCase(Locale.ROOT)) + ");"
|
||||
"window.kstMapApi.focusCallsignRaw(" + toJsStringLiteral(normalizedCallsignRaw) + ");"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -257,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();");
|
||||
@@ -292,6 +335,8 @@ public final class StationMapView {
|
||||
webView.setMinWidth(0);
|
||||
webView.setMinHeight(220);
|
||||
webView.setPrefHeight(420);
|
||||
webView.setMaxWidth(Double.MAX_VALUE);
|
||||
webView.setMaxHeight(Double.MAX_VALUE);
|
||||
VBox.setVgrow(webView, Priority.ALWAYS);
|
||||
|
||||
profileSection.setMinWidth(0);
|
||||
@@ -623,7 +668,7 @@ public final class StationMapView {
|
||||
webEngine.getLoadWorker().stateProperty().addListener((obs, oldState, newState) -> {
|
||||
if (newState == javafx.concurrent.Worker.State.SUCCEEDED) {
|
||||
JSObject window = (JSObject) webEngine.executeScript("window");
|
||||
window.setMember("javaMapBridge", new JavaMapBridge());
|
||||
window.setMember("javaMapBridge", javaMapBridge);
|
||||
|
||||
executeMapScriptSafely("window.kstMapApi.init();");
|
||||
|
||||
@@ -643,11 +688,13 @@ public final class StationMapView {
|
||||
return;
|
||||
}
|
||||
|
||||
Platform.runLater(() ->
|
||||
Platform.runLater(() ->
|
||||
executeMapScriptSafely("window.kstMapApi.invalidateSize();")));
|
||||
Platform.runLater(() -> Platform.runLater(() ->
|
||||
executeMapScriptSafely("window.kstMapApi.resize();")
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
|
||||
private void handleWebViewClick(MouseEvent event) {
|
||||
logWebViewMouseEvent("MOUSE_CLICKED", event);
|
||||
|
||||
@@ -821,7 +868,11 @@ public final class StationMapView {
|
||||
detailLocatorValue.setText(selectedSnapshot.locator6());
|
||||
detailQrbValue.setText(String.format(Locale.US, "%.0f km", selectedSnapshot.qrbKm()));
|
||||
detailQtfValue.setText(String.format(Locale.US, "%.0f°", selectedSnapshot.qtfDeg()));
|
||||
detailBandsValue.setText(selectedSnapshot.bandSummary().isBlank() ? "-" : selectedSnapshot.bandSummary());
|
||||
String bandText = selectedSnapshot.bandSummary().isBlank() ? "-" : selectedSnapshot.bandSummary();
|
||||
if (selectedSnapshot.offersSelectedBand()) {
|
||||
bandText += " B+";
|
||||
}
|
||||
detailBandsValue.setText(bandText);
|
||||
detailFrequenciesArea.setText(selectedSnapshot.detailFrequencyText());
|
||||
detailAirplanesValue.setText(String.valueOf(selectedSnapshot.reachableAirplanes()));
|
||||
triggerClusterSpotButton.setDisable(false);
|
||||
@@ -910,9 +961,6 @@ public final class StationMapView {
|
||||
detailPathProfileChart.setObstructionSummary(result.obstructionSummary());
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
private void renderAll() {
|
||||
if (!mapReady) {
|
||||
return;
|
||||
@@ -930,49 +978,78 @@ public final class StationMapView {
|
||||
renderBeam();
|
||||
renderConnectionLine();
|
||||
renderGridIfViewportKnown();
|
||||
focusSelectedStationOnlyWhenSelectionChanged();
|
||||
}
|
||||
|
||||
if (lastSelectedSnapshot != null) {
|
||||
focusCallsignRaw(lastSelectedSnapshot.callSignRaw());
|
||||
private void focusSelectedStationOnlyWhenSelectionChanged() {
|
||||
if (lastSelectedSnapshot == null || lastSelectedSnapshot.callSignRaw() == null) {
|
||||
lastAutoFocusedCallsignRaw = "";
|
||||
return;
|
||||
}
|
||||
|
||||
String selectedCallsignRaw = lastSelectedSnapshot.callSignRaw().trim().toUpperCase(Locale.ROOT);
|
||||
if (selectedCallsignRaw.isBlank() || selectedCallsignRaw.equals(lastAutoFocusedCallsignRaw)) {
|
||||
return;
|
||||
}
|
||||
|
||||
focusCallsignRaw(selectedCallsignRaw);
|
||||
}
|
||||
|
||||
private void renderStations() {
|
||||
String stationsJson = toStationsJson(lastSnapshots);
|
||||
if (stationsJson.equals(lastRenderedStationsJson)) {
|
||||
return;
|
||||
}
|
||||
|
||||
lastRenderedStationsJson = stationsJson;
|
||||
|
||||
executeMapScriptSafely(
|
||||
"window.kstMapApi.setStations(" + toJsStringLiteral(toStationsJson(lastSnapshots)) + ");"
|
||||
"window.kstMapApi.setStations(" + toJsStringLiteral(stationsJson) + ");"
|
||||
);
|
||||
}
|
||||
|
||||
private void renderBeam() {
|
||||
if (!Double.isFinite(homeLatitudeDeg)
|
||||
|| !Double.isFinite(homeLongitudeDeg)
|
||||
|| beamWidthDeg <= 0.0
|
||||
|| maxQrbKm <= 0.0) {
|
||||
String beamJson = "null";
|
||||
|
||||
executeMapScriptSafely("window.kstMapApi.setBeam('null');");
|
||||
return;
|
||||
if (Double.isFinite(homeLatitudeDeg)
|
||||
&& Double.isFinite(homeLongitudeDeg)
|
||||
&& beamWidthDeg > 0.0
|
||||
&& maxQrbKm > 0.0) {
|
||||
|
||||
List<double[]> sectorPoints = buildBeamPolygon(homeLatitudeDeg, homeLongitudeDeg, antennaAzimuthDeg, beamWidthDeg, maxQrbKm);
|
||||
beamJson = toPointArrayJson(sectorPoints);
|
||||
}
|
||||
|
||||
List<double[]> sectorPoints = buildBeamPolygon(homeLatitudeDeg, homeLongitudeDeg, antennaAzimuthDeg, beamWidthDeg, maxQrbKm);
|
||||
if (beamJson.equals(lastRenderedBeamJson)) {
|
||||
return;
|
||||
}
|
||||
lastRenderedBeamJson = beamJson;
|
||||
|
||||
executeMapScriptSafely(
|
||||
"window.kstMapApi.setBeam(" + toJsStringLiteral(toPointArrayJson(sectorPoints)) + ");"
|
||||
"window.kstMapApi.setBeam(" + toJsStringLiteral(beamJson) + ");"
|
||||
);
|
||||
}
|
||||
|
||||
private void renderConnectionLine() {
|
||||
if (lastSelectedSnapshot == null || !lastSelectedSnapshot.hasUsablePosition()
|
||||
|| !Double.isFinite(homeLatitudeDeg) || !Double.isFinite(homeLongitudeDeg)) {
|
||||
String connectionJson = "null";
|
||||
|
||||
executeMapScriptSafely("window.kstMapApi.setConnection('null');");
|
||||
return;
|
||||
if (lastSelectedSnapshot != null && lastSelectedSnapshot.hasUsablePosition()
|
||||
&& Double.isFinite(homeLatitudeDeg) && Double.isFinite(homeLongitudeDeg)) {
|
||||
|
||||
List<double[]> points = List.of(
|
||||
new double[]{homeLatitudeDeg, homeLongitudeDeg},
|
||||
new double[]{lastSelectedSnapshot.latitudeDeg(), lastSelectedSnapshot.longitudeDeg()}
|
||||
);
|
||||
connectionJson = toPointArrayJson(points);
|
||||
}
|
||||
|
||||
List<double[]> points = List.of(
|
||||
new double[]{homeLatitudeDeg, homeLongitudeDeg},
|
||||
new double[]{lastSelectedSnapshot.latitudeDeg(), lastSelectedSnapshot.longitudeDeg()}
|
||||
);
|
||||
if (connectionJson.equals(lastRenderedConnectionJson)) {
|
||||
return;
|
||||
}
|
||||
lastRenderedConnectionJson = connectionJson;
|
||||
|
||||
executeMapScriptSafely(
|
||||
"window.kstMapApi.setConnection(" + toJsStringLiteral(toPointArrayJson(points)) + ");"
|
||||
"window.kstMapApi.setConnection(" + toJsStringLiteral(connectionJson) + ");"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1011,11 +1088,19 @@ public final class StationMapView {
|
||||
+ " cellPx=" + String.format(Locale.US, "%.1f/%.1f", renderPlan.estimatedCellWidthPx(), renderPlan.estimatedCellHeightPx())
|
||||
+ " cells=" + visibleGridCells.size());
|
||||
|
||||
String gridJson = toGridJson(visibleGridCells, renderPlan);
|
||||
if (gridJson.equals(lastRenderedGridJson)) {
|
||||
return;
|
||||
}
|
||||
lastRenderedGridJson = gridJson;
|
||||
|
||||
executeMapScriptSafely(
|
||||
"window.kstMapApi.setGrid(" + toJsStringLiteral(toGridJson(visibleGridCells, renderPlan)) + ");"
|
||||
"window.kstMapApi.setGrid(" + toJsStringLiteral(gridJson) + ");"
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
|
||||
private List<double[]> buildBeamPolygon(double startLatDeg,
|
||||
double startLonDeg,
|
||||
double centerAzimuthDeg,
|
||||
|
||||
@@ -19,8 +19,10 @@ import java.util.concurrent.Executors;
|
||||
*/
|
||||
final class TileProxyServer {
|
||||
|
||||
private static final int CACHE_MAX = 512;
|
||||
private static final int CACHE_MAX = 2048;
|
||||
private static final String USER_AGENT = "kst4contest/1.0 amateur-radio-contest-tool";
|
||||
private static final int TILE_PROXY_THREADS = 12;
|
||||
private static final int TILE_PROXY_BACKLOG = 128;
|
||||
|
||||
private final ServerSocket serverSocket;
|
||||
private final ExecutorService executor;
|
||||
@@ -39,9 +41,9 @@ final class TileProxyServer {
|
||||
}
|
||||
});
|
||||
|
||||
this.serverSocket = new ServerSocket(0, 16, InetAddress.getByName("127.0.0.1"));
|
||||
this.serverSocket = new ServerSocket(0, TILE_PROXY_BACKLOG, InetAddress.getByName("127.0.0.1"));
|
||||
|
||||
this.executor = Executors.newFixedThreadPool(4, r -> {
|
||||
this.executor = Executors.newFixedThreadPool(TILE_PROXY_THREADS, r -> {
|
||||
Thread t = new Thread(r, "tile-proxy");
|
||||
t.setDaemon(true);
|
||||
return t;
|
||||
@@ -132,6 +134,7 @@ final class TileProxyServer {
|
||||
+ "Content-Type: image/png\r\n"
|
||||
+ "Content-Length: " + tileData.length + "\r\n"
|
||||
+ "Cache-Control: max-age=86400\r\n"
|
||||
+ "Access-Control-Allow-Origin: *\r\n"
|
||||
+ "Connection: close\r\n"
|
||||
+ "\r\n";
|
||||
out.write(header.getBytes());
|
||||
|
||||
@@ -6,6 +6,7 @@ module praktiKST {
|
||||
requires java.sql;
|
||||
requires javafx.media;
|
||||
requires jdk.jsobject;
|
||||
requires jlayer;
|
||||
requires java.net.http;
|
||||
requires java.desktop;
|
||||
requires jdk.crypto.ec;
|
||||
@@ -17,4 +18,4 @@ module praktiKST {
|
||||
|
||||
|
||||
opens kst4contest.view.map to javafx.web;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -650,6 +650,8 @@ svg.leaflet-image-layer.leaflet-interactive path {
|
||||
border-right-color: #fff;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/* Printing */
|
||||
|
||||
@media print {
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
package kst4contest.test;
|
||||
|
||||
import kst4contest.model.Band;
|
||||
import kst4contest.model.ChatMember;
|
||||
import kst4contest.view.map.MapCallsignRawSnapshot;
|
||||
import kst4contest.view.map.MapCallsignRawSnapshotBuilder;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.EnumSet;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
class MapCallsignRawSnapshotBuilderTest {
|
||||
|
||||
@Test
|
||||
void marksSnapshotWhenNameAdvertisesSelectedBand() {
|
||||
ChatMember station = buildStation("DL1ABC", "QRV 2-70-23", "JN58TD", 1_000L);
|
||||
|
||||
MapCallsignRawSnapshotBuilder builder = new MapCallsignRawSnapshotBuilder();
|
||||
List<MapCallsignRawSnapshot> snapshots = builder.buildSnapshots(
|
||||
List.of(station),
|
||||
null,
|
||||
EnumSet.of(Band.B_1296)
|
||||
);
|
||||
|
||||
assertEquals(1, snapshots.size());
|
||||
assertTrue(snapshots.get(0).offersSelectedBand());
|
||||
}
|
||||
|
||||
@Test
|
||||
void keepsSnapshotUnmarkedWhenNoSelectedBandMatches() {
|
||||
ChatMember station = buildStation("DL1ABC", "QRV 2m only", "JN58TD", 1_000L);
|
||||
|
||||
MapCallsignRawSnapshotBuilder builder = new MapCallsignRawSnapshotBuilder();
|
||||
List<MapCallsignRawSnapshot> snapshots = builder.buildSnapshots(
|
||||
List.of(station),
|
||||
null,
|
||||
EnumSet.of(Band.B_2320)
|
||||
);
|
||||
|
||||
assertEquals(1, snapshots.size());
|
||||
assertFalse(snapshots.get(0).offersSelectedBand());
|
||||
}
|
||||
|
||||
private ChatMember buildStation(String callSign, String name, String locator, long activityEpoch) {
|
||||
ChatMember chatMember = new ChatMember();
|
||||
chatMember.setCallSign(callSign);
|
||||
chatMember.setName(name);
|
||||
chatMember.setQra(locator);
|
||||
chatMember.setActivityTimeLastInEpoch(activityEpoch);
|
||||
return chatMember;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,220 @@
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const markdownIt = require("markdown-it");
|
||||
const markdownItAnchor = require("markdown-it-anchor");
|
||||
|
||||
const MANUAL_PAGE_ORDER = {
|
||||
de: [
|
||||
"home",
|
||||
"installation",
|
||||
"konfiguration",
|
||||
"log-synchronisation",
|
||||
"airscout-integration",
|
||||
"dx-cluster-server",
|
||||
"funktionen",
|
||||
"makros-und-variablen",
|
||||
"benutzeroberflaeche",
|
||||
"changelog"
|
||||
],
|
||||
en: [
|
||||
"home",
|
||||
"installation",
|
||||
"configuration",
|
||||
"log-sync",
|
||||
"airscout-integration",
|
||||
"dx-cluster-server",
|
||||
"features",
|
||||
"macros-and-variables",
|
||||
"user-interface",
|
||||
"changelog"
|
||||
]
|
||||
};
|
||||
|
||||
function markdownToPlainText(value) {
|
||||
return (value || "")
|
||||
.replace(/!\[([^\]]*)\]\([^)]+\)/g, "$1")
|
||||
.replace(/\[([^\]]+)\]\([^)]+\)/g, "$1")
|
||||
.replace(/<[^>]+>/g, "")
|
||||
.replace(/[*_~`]/g, "")
|
||||
.replace(/\s+/g, " ")
|
||||
.trim();
|
||||
}
|
||||
|
||||
function extractManualTitle(raw, fallbackTitle) {
|
||||
const heading = (raw || "").match(/^#\s+(.+)$/m);
|
||||
return heading ? markdownToPlainText(heading[1]) : fallbackTitle;
|
||||
}
|
||||
|
||||
function extractManualSummary(raw) {
|
||||
const blocks = (raw || "").split(/\r?\n\s*\r?\n/);
|
||||
|
||||
for (const block of blocks) {
|
||||
const trimmed = block.trim();
|
||||
|
||||
if (!trimmed
|
||||
|| /^(#|>|\||---|```|!\[)/.test(trimmed)
|
||||
|| /^[-*+]\s/.test(trimmed)
|
||||
|| /^\d+[.)]\s/.test(trimmed)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
return markdownToPlainText(trimmed);
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
function getManualPageOrder(lang, slug) {
|
||||
const configuredOrder = MANUAL_PAGE_ORDER[lang] || [];
|
||||
const index = configuredOrder.indexOf(slug);
|
||||
|
||||
return index >= 0 ? index : 999;
|
||||
}
|
||||
|
||||
function rewriteManualLinks(content, lang) {
|
||||
return (content || "")
|
||||
|
||||
// Manual images are stored next to the Markdown sources in github_docs.
|
||||
// The website publishes them from one shared, language-neutral directory.
|
||||
.replace(
|
||||
/!\[([^\]]*)\]\((?!https?:\/\/|\/)([^)\s]+\.(?:png|jpe?g|gif|svg|webp))\)/gi,
|
||||
(match, altText, imagePath) => {
|
||||
const fileName = path.posix.basename(
|
||||
imagePath.replace(/\\/g, "/")
|
||||
);
|
||||
|
||||
return ``;
|
||||
}
|
||||
)
|
||||
|
||||
// GitHub-Wiki-Links im Stil [[de-Installation]] oder [[en-Installation]]
|
||||
.replace(/\[\[(en|de)-([^\]|]+)\]\]/g, (match, linkLang, slug) => {
|
||||
return `[${slug.replace(/-/g, " ")}](/manual/${linkLang}/${slug.toLowerCase()}/)`;
|
||||
})
|
||||
|
||||
// GitHub-Wiki-Links mit Label: [[Text|de-Installation]]
|
||||
.replace(/\[\[([^\]|]+)\|(en|de)-([^\]]+)\]\]/g, (match, label, linkLang, slug) => {
|
||||
return `[${label}](/manual/${linkLang}/${slug.toLowerCase()}/)`;
|
||||
})
|
||||
|
||||
// Markdown-Links auf de-/en-Dateien ohne .md
|
||||
.replace(/\]\((en|de)-([^)#]+)\)/g, (match, linkLang, slug) => {
|
||||
return `](/manual/${linkLang}/${slug.toLowerCase()}/)`;
|
||||
})
|
||||
|
||||
// Markdown-Links auf de-/en-Dateien mit .md
|
||||
.replace(/\]\((en|de)-([^)#]+)\.md\)/g, (match, linkLang, slug) => {
|
||||
return `](/manual/${linkLang}/${slug.toLowerCase()}/)`;
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = function (eleventyConfig) {
|
||||
|
||||
eleventyConfig.addFilter("dateToIso", function (dateObj) {
|
||||
return new Date(dateObj).toISOString().split("T")[0];
|
||||
});
|
||||
|
||||
eleventyConfig.addFilter("dateToRfc822", function (dateObj) {
|
||||
return new Date(dateObj).toUTCString();
|
||||
});
|
||||
|
||||
eleventyConfig.addFilter("readableDate", function (dateObj) {
|
||||
return new Intl.DateTimeFormat("en", {
|
||||
year: "numeric",
|
||||
month: "long",
|
||||
day: "numeric"
|
||||
}).format(dateObj);
|
||||
});
|
||||
|
||||
eleventyConfig.addCollection("sortedFeatures", function (collectionApi) {
|
||||
return collectionApi.getFilteredByTag("features").sort((a, b) => {
|
||||
return (a.data.order || 999) - (b.data.order || 999);
|
||||
});
|
||||
});
|
||||
|
||||
eleventyConfig.addCollection("featuredFeatures", function (collectionApi) {
|
||||
return collectionApi.getFilteredByTag("features")
|
||||
.filter(item => item.data.featured)
|
||||
.sort((a, b) => {
|
||||
return (a.data.order || 999) - (b.data.order || 999);
|
||||
});
|
||||
});
|
||||
|
||||
eleventyConfig.addFilter("whereTag", function(collection, tag) {
|
||||
return collection.filter(item => item.data.tags && item.data.tags.includes(tag));
|
||||
});
|
||||
|
||||
eleventyConfig.addPassthroughCopy({ "src/assets": "assets" });
|
||||
|
||||
eleventyConfig.addPassthroughCopy({
|
||||
"../github_docs/*.{png,jpg,jpeg,gif,svg,webp}": "manual/assets"
|
||||
});
|
||||
|
||||
const md = markdownIt({
|
||||
html: true,
|
||||
linkify: true,
|
||||
typographer: true
|
||||
}).use(markdownItAnchor, {
|
||||
permalink: markdownItAnchor.permalink.headerLink()
|
||||
});
|
||||
|
||||
eleventyConfig.addFilter("markdown", function (value) {
|
||||
return md.render(value || "");
|
||||
});
|
||||
|
||||
eleventyConfig.addCollection("manualPages", function () {
|
||||
const docsDir = path.join(__dirname, "..", "github_docs");
|
||||
|
||||
if (!fs.existsSync(docsDir)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return fs.readdirSync(docsDir)
|
||||
.filter(file => /^(en|de)-.+\.md$/.test(file))
|
||||
.map(file => {
|
||||
const fullPath = path.join(docsDir, file);
|
||||
const raw = fs.readFileSync(fullPath, "utf8");
|
||||
|
||||
const lang = file.startsWith("de-") ? "de" : "en";
|
||||
const slug = file
|
||||
.replace(/^en-/, "")
|
||||
.replace(/^de-/, "")
|
||||
.replace(/\.md$/, "")
|
||||
.toLowerCase();
|
||||
|
||||
const fallbackTitle = file
|
||||
.replace(/^en-/, "")
|
||||
.replace(/^de-/, "")
|
||||
.replace(/\.md$/, "")
|
||||
.replace(/-/g, " ");
|
||||
|
||||
const title = extractManualTitle(raw, fallbackTitle);
|
||||
|
||||
return {
|
||||
file,
|
||||
lang,
|
||||
slug,
|
||||
title,
|
||||
summary: extractManualSummary(raw),
|
||||
order: getManualPageOrder(lang, slug),
|
||||
content: rewriteManualLinks(raw, lang)
|
||||
};
|
||||
})
|
||||
.sort((a, b) => {
|
||||
return a.lang.localeCompare(b.lang)
|
||||
|| a.order - b.order
|
||||
|| a.title.localeCompare(b.title);
|
||||
});
|
||||
});
|
||||
|
||||
return {
|
||||
dir: {
|
||||
input: "src",
|
||||
output: "_site",
|
||||
includes: "_includes",
|
||||
layouts: "_layouts"
|
||||
},
|
||||
markdownTemplateEngine: "njk",
|
||||
htmlTemplateEngine: "njk"
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,51 @@
|
||||
# KST4Contest Website Architecture
|
||||
|
||||
## Goals
|
||||
|
||||
KST4Contest uses GitHub as the central source for code, documentation and releases.
|
||||
|
||||
The website is generated statically with Eleventy (11ty) and deployed to:
|
||||
|
||||
https://kst4contest.hamradioonline.de
|
||||
|
||||
## Documentation Source
|
||||
|
||||
The existing `github_docs/` directory remains the single source of truth for the manual.
|
||||
|
||||
It is used for:
|
||||
|
||||
- GitHub Wiki
|
||||
- PDF manual generation
|
||||
- Website manual pages
|
||||
|
||||
## Website Source
|
||||
|
||||
The `website/` directory contains the Eleventy project.
|
||||
|
||||
Manual content must not be duplicated inside `website/`.
|
||||
|
||||
Marketing, SEO, news, screenshots and download pages belong into `website/src/`.
|
||||
|
||||
## URL Structure
|
||||
|
||||
- `/`
|
||||
- `/features/`
|
||||
- `/download/`
|
||||
- `/screenshots/`
|
||||
- `/news/`
|
||||
- `/manual/`
|
||||
- `/manual/en/`
|
||||
- `/manual/de/`
|
||||
- `/faq/`
|
||||
|
||||
## Deployment
|
||||
|
||||
The generated static output is deployed to:
|
||||
|
||||
`/srv/www/kst4contest/current`
|
||||
|
||||
Nginx serves this directory.
|
||||
|
||||
## Build Strategy
|
||||
|
||||
GitHub Actions will later build the 11ty website and deploy the generated output to the server.
|
||||
Generated
+1718
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"name": "kst4contest-website",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"start": "eleventy --serve",
|
||||
"build": "eleventy"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@11ty/eleventy": "^3.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"markdown-it": "^14.3.0",
|
||||
"markdown-it-anchor": "^9.2.0"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
module.exports = {
|
||||
version: Date.now()
|
||||
};
|
||||
@@ -0,0 +1,152 @@
|
||||
const REPO = "praktimarc/kst4contest";
|
||||
const API = `https://api.github.com/repos/${REPO}`;
|
||||
const LATEST_RELEASE_URL = `https://github.com/${REPO}/releases/latest`;
|
||||
|
||||
async function fetchLatestReleaseTag() {
|
||||
const headers = { Accept: "application/vnd.github+json" };
|
||||
|
||||
if (process.env.GITHUB_TOKEN) {
|
||||
headers.Authorization = `Bearer ${process.env.GITHUB_TOKEN}`;
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await fetch(`${API}/releases/latest`, { headers });
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error(
|
||||
`GitHub API /releases/latest failed: ${res.status}`
|
||||
);
|
||||
}
|
||||
|
||||
const release = await res.json();
|
||||
|
||||
if (!release.tag_name) {
|
||||
throw new Error(
|
||||
"GitHub API response did not contain a release tag"
|
||||
);
|
||||
}
|
||||
|
||||
return release.tag_name;
|
||||
} catch (err) {
|
||||
console.warn(
|
||||
`[downloads] Could not load the latest release tag. ` +
|
||||
`Download buttons will open the latest release page instead: ${err.message}`
|
||||
);
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a direct link to an asset from the latest Stable release.
|
||||
*
|
||||
* If the GitHub API was unavailable during the build, the function deliberately
|
||||
* returns the generic latest-release page instead of using a hard-coded and
|
||||
* increasingly outdated fallback version.
|
||||
*/
|
||||
function createAssetUrl(latestTag, filenameTemplate) {
|
||||
if (!latestTag) {
|
||||
return LATEST_RELEASE_URL;
|
||||
}
|
||||
|
||||
const filename = filenameTemplate.replace(/\{tag\}/g, latestTag);
|
||||
|
||||
return `https://github.com/${REPO}/releases/download/${latestTag}/${filename}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the download list from the latest Stable GitHub release.
|
||||
*
|
||||
* The filenames must remain aligned with the artifacts produced by
|
||||
* .github/workflows/tagged-release.yml.
|
||||
*/
|
||||
module.exports = async function () {
|
||||
const latestTag = await fetchLatestReleaseTag();
|
||||
const assetUrl = (filenameTemplate) =>
|
||||
createAssetUrl(latestTag, filenameTemplate);
|
||||
|
||||
return [
|
||||
{
|
||||
os: "Windows",
|
||||
format: "ZIP x64",
|
||||
icon: "🪟",
|
||||
recommended: true,
|
||||
note: "Extract the complete archive, then start praktiKST.exe. No separate Java installation is required.",
|
||||
url: assetUrl("praktiKST-{tag}-windows-x64.zip")
|
||||
},
|
||||
{
|
||||
os: "Linux",
|
||||
format: "Flatpak (.flatpakref)",
|
||||
icon: "🐧",
|
||||
recommended: true,
|
||||
note: "Installs through Flatpak and can be updated through Flatpak or the desktop software centre.",
|
||||
url: assetUrl("de.x08.KST4Contest.flatpakref")
|
||||
},
|
||||
{
|
||||
os: "Linux",
|
||||
format: "AppImage x86_64",
|
||||
icon: "🐧",
|
||||
recommended: false,
|
||||
note: "Portable build without package installation. Make the downloaded file executable before the first launch.",
|
||||
url: assetUrl("KST4Contest-{tag}-linux-x86_64.AppImage")
|
||||
},
|
||||
{
|
||||
os: "Debian / Ubuntu",
|
||||
format: "DEB amd64",
|
||||
icon: "📦",
|
||||
recommended: false,
|
||||
note: "Native package for Debian, Ubuntu and distributions based on them.",
|
||||
url: assetUrl("KST4Contest-{tag}-debian-amd64.deb")
|
||||
},
|
||||
{
|
||||
os: "Fedora",
|
||||
format: "RPM x86_64",
|
||||
icon: "📦",
|
||||
recommended: false,
|
||||
note: "Native package built for Fedora and compatible RPM-based systems.",
|
||||
url: assetUrl("KST4Contest-{tag}-fedora-x86_64.rpm")
|
||||
},
|
||||
{
|
||||
os: "Arch Linux",
|
||||
format: "pkg.tar.zst",
|
||||
icon: "📦",
|
||||
recommended: false,
|
||||
note: "Release package for direct installation with pacman. AUR alternatives are described in the installation guide.",
|
||||
url: assetUrl(
|
||||
"KST4Contest-{tag}-archlinux-x86_64.pkg.tar.zst"
|
||||
)
|
||||
},
|
||||
{
|
||||
os: "macOS Apple Silicon",
|
||||
format: "DMG arm64",
|
||||
icon: "🍎",
|
||||
recommended: false,
|
||||
note: "Best-effort build for Apple Silicon Macs. The application is not currently notarized by Apple.",
|
||||
url: assetUrl("KST4Contest-{tag}-macos-arm64.dmg")
|
||||
},
|
||||
{
|
||||
os: "macOS Intel",
|
||||
format: "DMG x86_64",
|
||||
icon: "🍎",
|
||||
recommended: false,
|
||||
note: "Best-effort build for Intel Macs. The application is not currently notarized by Apple.",
|
||||
url: assetUrl("KST4Contest-{tag}-macos-x86_64.dmg")
|
||||
},
|
||||
{
|
||||
os: "Manual English",
|
||||
format: "PDF",
|
||||
icon: "📘",
|
||||
recommended: false,
|
||||
note: "English PDF manual included with the Stable release.",
|
||||
url: assetUrl("KST4Contest-{tag}-manual-en.pdf")
|
||||
},
|
||||
{
|
||||
os: "Manual German",
|
||||
format: "PDF",
|
||||
icon: "📘",
|
||||
recommended: false,
|
||||
note: "German PDF manual included with the Stable release.",
|
||||
url: assetUrl("KST4Contest-{tag}-manual-de.pdf")
|
||||
}
|
||||
];
|
||||
};
|
||||
@@ -0,0 +1,12 @@
|
||||
module.exports = [
|
||||
{ title: "Home", url: "/" },
|
||||
{ title: "Features", url: "/features/" },
|
||||
{ title: "Screenshots", url: "/screenshots/" },
|
||||
{ title: "Manual", url: "/manual/" },
|
||||
{ title: "News", url: "/news/" },
|
||||
{ title: "Roadmap", url: "/roadmap/" },
|
||||
{ title: "About", url: "/about/" },
|
||||
{ title: "FAQ", url: "/faq/" },
|
||||
{ title: "Support", url: "/support/"
|
||||
}
|
||||
];
|
||||
@@ -0,0 +1,84 @@
|
||||
const REPO = "praktimarc/kst4contest";
|
||||
const API = `https://api.github.com/repos/${REPO}`;
|
||||
|
||||
async function githubGet(path) {
|
||||
const headers = { Accept: "application/vnd.github+json" };
|
||||
if (process.env.GITHUB_TOKEN) {
|
||||
headers.Authorization = `Bearer ${process.env.GITHUB_TOKEN}`;
|
||||
}
|
||||
const res = await fetch(`${API}${path}`, { headers });
|
||||
if (!res.ok) {
|
||||
throw new Error(`GitHub API ${path} failed: ${res.status}`);
|
||||
}
|
||||
return res.json();
|
||||
}
|
||||
|
||||
// Builds the roadmap from all GitHub issues tagged "enhancement", grouped by
|
||||
// their version milestone (open or already released). Issues without a
|
||||
// milestone are shown as planned but with no assigned version yet. Issues
|
||||
// closed as "not planned" are kept, struck through and labeled accordingly.
|
||||
// Falls back to an empty list if the API is unreachable (e.g. offline build)
|
||||
// so the site build never fails.
|
||||
module.exports = async function () {
|
||||
try {
|
||||
const issues = await githubGet(
|
||||
"/issues?labels=enhancement&state=all&per_page=100"
|
||||
);
|
||||
|
||||
const groups = new Map();
|
||||
|
||||
for (const issue of issues) {
|
||||
if (issue.pull_request) continue;
|
||||
|
||||
const milestone = issue.milestone;
|
||||
const key = milestone ? milestone.number : "unscheduled";
|
||||
|
||||
if (!groups.has(key)) {
|
||||
groups.set(key, {
|
||||
version: milestone ? milestone.title : null,
|
||||
description: milestone ? milestone.description : null,
|
||||
url: milestone ? milestone.html_url : null,
|
||||
released: milestone ? milestone.state === "closed" : false,
|
||||
dueOn: milestone && milestone.due_on ? new Date(milestone.due_on) : null,
|
||||
releasedOn: milestone && milestone.closed_at ? new Date(milestone.closed_at) : null,
|
||||
enhancements: []
|
||||
});
|
||||
}
|
||||
|
||||
const notPlanned = issue.state === "closed" && issue.state_reason === "not_planned";
|
||||
|
||||
groups.get(key).enhancements.push({
|
||||
title: issue.title,
|
||||
url: issue.html_url,
|
||||
done: issue.state === "closed" && !notPlanned,
|
||||
notPlanned
|
||||
});
|
||||
}
|
||||
|
||||
const versions = [...groups.values()].filter((v) => v.enhancements.length > 0);
|
||||
|
||||
// Order: upcoming dated versions first (soonest due date), then
|
||||
// upcoming undated versions, then already released versions (newest
|
||||
// first), unscheduled enhancements at the very end.
|
||||
versions.sort((a, b) => {
|
||||
if (!a.version) return 1;
|
||||
if (!b.version) return -1;
|
||||
if (a.released !== b.released) return a.released ? 1 : -1;
|
||||
|
||||
if (!a.released) {
|
||||
if (a.dueOn && b.dueOn) return a.dueOn - b.dueOn;
|
||||
if (a.dueOn) return -1;
|
||||
if (b.dueOn) return 1;
|
||||
return a.version.localeCompare(b.version);
|
||||
}
|
||||
|
||||
if (a.releasedOn && b.releasedOn) return b.releasedOn - a.releasedOn;
|
||||
return b.version.localeCompare(a.version);
|
||||
});
|
||||
|
||||
return versions;
|
||||
} catch (err) {
|
||||
console.warn(`[roadmap] Could not load GitHub issues: ${err.message}`);
|
||||
return [];
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,23 @@
|
||||
module.exports = [
|
||||
{
|
||||
id: "main-window",
|
||||
title: "Main Window",
|
||||
image: "/assets/screenshots/main-window.png",
|
||||
alt: "KST4Contest ON4KST contest chat client main window",
|
||||
caption: "Contest-oriented ON4KST chat workflow with candidate awareness."
|
||||
},
|
||||
{
|
||||
id: "priority-candidates",
|
||||
title: "Priority Candidates",
|
||||
image: "/assets/screenshots/priority-candidates.png",
|
||||
alt: "Priority candidate list in KST4Contest",
|
||||
caption: "Score-based candidate ranking for faster operator decisions."
|
||||
},
|
||||
{
|
||||
id: "timeline",
|
||||
title: "Timeline View",
|
||||
image: "/assets/screenshots/timeline.png",
|
||||
alt: "KST4Contest AP timeline view",
|
||||
caption: "Timeline support for AP windows and candidate timing."
|
||||
}
|
||||
];
|
||||
@@ -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,209 @@
|
||||
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 toSemanticVersion(tag) {
|
||||
return tag
|
||||
.replace(/^v/, "")
|
||||
.split("-")[0]
|
||||
.split("+")[0];
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
parts.push(" <latestVersion>");
|
||||
if (stable) {
|
||||
const latestIssues = await getIssuesForRelease(
|
||||
"1970-01-01T00:00:00Z",
|
||||
stable.publishedAt
|
||||
);
|
||||
|
||||
// Kept for application versions that still expect the old numeric format.
|
||||
parts.push(
|
||||
` <versionNumber>${escapeXml(toAppVersionNumber(latestTag))}</versionNumber>`
|
||||
);
|
||||
|
||||
// Used by current application versions for correct semantic comparison
|
||||
// and display, for example 1.41.1 instead of 1.411.
|
||||
parts.push(
|
||||
` <semanticVersion>${escapeXml(toSemanticVersion(latestTag))}</semanticVersion>`
|
||||
);
|
||||
|
||||
parts.push(" <adminMessage></adminMessage>");
|
||||
parts.push(
|
||||
` <majorChanges>${escapeXml(latestIssues.added.slice(0, 300))}</majorChanges>`
|
||||
);
|
||||
|
||||
// The release page is deliberately platform-neutral. The application
|
||||
// cannot know whether the user needs Windows, Linux or macOS packages.
|
||||
parts.push(
|
||||
` <latestVersionPathOnWebserver>https://github.com/${REPO}/releases/tag/${latestTag}</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(toSemanticVersion(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;
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,84 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>{{ title or "KST4Contest" }}</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="theme-color" content="#08110b">
|
||||
<meta name="description" content="{{ description or 'KST4Contest connects ON4KST chat, candidate selection, sked planning and station software for VHF, UHF and SHF contest operation.' }}">
|
||||
<link rel="canonical" href="https://kst4contest.hamradioonline.de{{ page.url }}">
|
||||
|
||||
<meta property="og:type" content="website">
|
||||
<meta property="og:title" content="{{ title or 'KST4Contest' }}">
|
||||
<meta property="og:description" content="{{ description or 'ON4KST chat, candidate selection, sked planning and station software in one VHF, UHF and SHF contest workflow.' }}">
|
||||
<meta property="og:url" content="https://kst4contest.hamradioonline.de{{ page.url }}">
|
||||
<meta property="og:site_name" content="KST4Contest">
|
||||
<meta property="og:locale" content="en_GB">
|
||||
<meta property="og:image" content="https://kst4contest.hamradioonline.de/assets/social/kst4contest-og.png">
|
||||
<meta property="og:image:width" content="1200">
|
||||
<meta property="og:image:height" content="630">
|
||||
|
||||
<meta name="twitter:card" content="summary_large_image">
|
||||
<meta name="twitter:title" content="{{ title or 'KST4Contest' }}">
|
||||
<meta name="twitter:description" content="{{ description or 'ON4KST chat, candidate selection, sked planning and station software in one VHF, UHF and SHF contest workflow.' }}">
|
||||
<meta name="twitter:image" content="https://kst4contest.hamradioonline.de/assets/social/kst4contest-og.png">
|
||||
|
||||
<link rel="stylesheet" href="/assets/css/main.css?v={{ build.version }}">
|
||||
<link rel="alternate" type="application/rss+xml" title="KST4Contest News" href="/feed.xml">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="site-bg" aria-hidden="true"></div>
|
||||
|
||||
<header class="site-header">
|
||||
<a class="brand" href="/" aria-label="KST4Contest home">
|
||||
<span class="brand-mark" aria-hidden="true">◎</span>
|
||||
<span>
|
||||
<strong>KST4Contest</strong>
|
||||
<small>ON4KST Contest Client</small>
|
||||
</span>
|
||||
</a>
|
||||
|
||||
<nav class="site-nav" aria-label="Main navigation">
|
||||
{% for item in navigation %}
|
||||
<a href="{{ item.url }}">{{ item.title }}</a>
|
||||
{% endfor %}
|
||||
<a class="nav-cta" href="/download/">Download</a>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<main>{{ content | safe }}</main>
|
||||
|
||||
<footer class="site-footer">
|
||||
<div class="footer-grid">
|
||||
<div>
|
||||
<strong>KST4Contest</strong>
|
||||
<p>ON4KST chat, candidate selection and sked planning for VHF, UHF and SHF contest operation.</p>
|
||||
</div>
|
||||
<div>
|
||||
<strong>Project</strong>
|
||||
<p><a href="/features/">Features</a></p>
|
||||
<p><a href="/download/">Download</a></p>
|
||||
<p><a href="/manual/">Manual</a></p>
|
||||
<p><a href="/roadmap/">Roadmap</a></p>
|
||||
</div>
|
||||
<div>
|
||||
<strong>Community</strong>
|
||||
<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>
|
||||
<strong>Legal</strong>
|
||||
<p><a href="/legal-notice/">Legal Notice</a></p>
|
||||
<p><a href="/privacy/">Privacy Policy</a></p>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
{% if heroFx %}
|
||||
<script src="/assets/js/hero-radio-fx.js?v={{ build.version }}" defer></script>
|
||||
{% endif %}
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,58 @@
|
||||
---
|
||||
layout: base.njk
|
||||
---
|
||||
|
||||
<section class="hero">
|
||||
<p class="badge">{{ category }} · since {{ since }}</p>
|
||||
<h1>{{ title }}</h1>
|
||||
<p class="lead">{{ summary }}</p>
|
||||
</section>
|
||||
|
||||
<section class="section narrow">
|
||||
<article class="card content-card">
|
||||
<div class="feature-icon large">{{ icon }}</div>
|
||||
|
||||
{{ content | safe }}
|
||||
|
||||
{% if tagsList %}
|
||||
<h2>Keywords</h2>
|
||||
<div class="tag-list">
|
||||
{% for tag in tagsList %}
|
||||
<span>{{ tag }}</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if related %}
|
||||
<h2>Related features</h2>
|
||||
<div class="actions">
|
||||
{% for slug in related %}
|
||||
{% for item in collections.sortedFeatures %}
|
||||
{% if item.fileSlug == slug %}
|
||||
<a class="button secondary" href="{{ item.url }}">{{ item.data.title }}</a>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</article>
|
||||
</section>
|
||||
|
||||
<script type="application/ld+json">
|
||||
{
|
||||
"@context": "https://schema.org",
|
||||
"@type": "TechArticle",
|
||||
"headline": "{{ title }}",
|
||||
"description": "{{ summary }}",
|
||||
"about": "KST4Contest",
|
||||
"url": "https://kst4contest.hamradioonline.de{{ page.url }}",
|
||||
"author": {
|
||||
"@type": "Person",
|
||||
"name": "Praktimarc"
|
||||
},
|
||||
"publisher": {
|
||||
"@type": "Organization",
|
||||
"name": "KST4Contest"
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,15 @@
|
||||
---
|
||||
layout: base.njk
|
||||
---
|
||||
|
||||
<section class="hero">
|
||||
<p class="badge">News · {{ date | readableDate }}</p>
|
||||
<h1>{{ title }}</h1>
|
||||
<p class="lead">{{ summary }}</p>
|
||||
</section>
|
||||
|
||||
<section class="section narrow">
|
||||
<article class="card content-card">
|
||||
{{ content | safe }}
|
||||
</article>
|
||||
</section>
|
||||
@@ -0,0 +1,45 @@
|
||||
---
|
||||
layout: base.njk
|
||||
title: About KST4Contest
|
||||
description: About KST4Contest and its contest-oriented ON4KST workflow.
|
||||
---
|
||||
|
||||
<section class="hero">
|
||||
<p class="badge">About</p>
|
||||
<h1>Built for real contest operation.</h1>
|
||||
<p class="lead">
|
||||
KST4Contest was created to turn ON4KST chat activity into actionable contest workflow.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section class="section narrow">
|
||||
<article class="card content-card">
|
||||
<h2>Why KST4Contest exists</h2>
|
||||
<p>
|
||||
Generic chat clients display messages. KST4Contest goes further:
|
||||
it helps operators identify candidates, manage skeds, use AirScout timing,
|
||||
synchronize with loggers and reduce workload during VHF/UHF/SHF contests.
|
||||
</p>
|
||||
|
||||
<h2>Open source and practical</h2>
|
||||
<p>
|
||||
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>
|
||||
@@ -0,0 +1,807 @@
|
||||
:root {
|
||||
--bg: #050816;
|
||||
--bg2: #0b1224;
|
||||
--panel: rgba(15, 23, 42, 0.78);
|
||||
--panel2: rgba(30, 41, 59, 0.72);
|
||||
--border: rgba(148, 163, 184, 0.22);
|
||||
--text: #f8fafc;
|
||||
--muted: #aab6ca;
|
||||
--soft: #64748b;
|
||||
--accent: #38bdf8;
|
||||
--accent2: #a855f7;
|
||||
--accent3: #22c55e;
|
||||
--shadow: 0 24px 80px rgba(0, 0, 0, 0.45);
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html {
|
||||
scroll-behavior: smooth;
|
||||
background: var(--bg);
|
||||
}
|
||||
|
||||
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%);
|
||||
color: var(--text);
|
||||
font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
||||
line-height: 1.65;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
body::before {
|
||||
content: "";
|
||||
position: fixed;
|
||||
inset: -20%;
|
||||
z-index: -3;
|
||||
background:
|
||||
radial-gradient(circle at 20% 20%, rgba(168, 85, 247, 0.22), transparent 28%),
|
||||
radial-gradient(circle at 78% 18%, rgba(56, 189, 248, 0.18), transparent 30%),
|
||||
radial-gradient(circle at 45% 80%, rgba(34, 197, 94, 0.10), transparent 26%);
|
||||
filter: blur(48px);
|
||||
transform: translateZ(0);
|
||||
}
|
||||
|
||||
body::after {
|
||||
content: "";
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: -2;
|
||||
pointer-events: none;
|
||||
background-image:
|
||||
linear-gradient(rgba(255,255,255,0.038) 1px, transparent 1px),
|
||||
linear-gradient(90deg, rgba(255,255,255,0.038) 1px, transparent 1px);
|
||||
background-size: 54px 54px;
|
||||
-webkit-mask-image: radial-gradient(circle at center, black 0%, black 46%, transparent 78%);
|
||||
mask-image: radial-gradient(circle at center, black 0%, black 46%, transparent 78%);
|
||||
}
|
||||
|
||||
.site-bg {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: -1;
|
||||
pointer-events: none;
|
||||
background:
|
||||
linear-gradient(180deg, rgba(5, 8, 22, 0.10), rgba(5, 8, 22, 0.84)),
|
||||
radial-gradient(circle at 8% 24%, rgba(168, 85, 247, 0.18), transparent 26%),
|
||||
radial-gradient(circle at 82% 20%, rgba(56, 189, 248, 0.16), transparent 30%);
|
||||
}
|
||||
|
||||
.site-bg::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background:
|
||||
radial-gradient(circle at top, transparent 0%, rgba(5, 8, 22, 0.42) 72%),
|
||||
linear-gradient(90deg, rgba(5, 8, 22, 0.25), transparent 50%, rgba(5, 8, 22, 0.25));
|
||||
}
|
||||
|
||||
.site-bg::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background-image:
|
||||
radial-gradient(circle at 1px 1px, rgba(255,255,255,0.10) 1px, transparent 0);
|
||||
background-size: 28px 28px;
|
||||
opacity: 0.11;
|
||||
-webkit-mask-image: radial-gradient(circle at 50% 22%, black 0%, transparent 68%);
|
||||
mask-image: radial-gradient(circle at 50% 22%, black 0%, transparent 68%);
|
||||
}
|
||||
|
||||
a {
|
||||
color: var(--accent);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
color: white;
|
||||
}
|
||||
|
||||
.site-header {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 10;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 24px;
|
||||
align-items: center;
|
||||
padding: 18px 6vw;
|
||||
background: rgba(5, 8, 22, 0.76);
|
||||
-webkit-backdrop-filter: blur(18px);
|
||||
backdrop-filter: blur(18px);
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.brand {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.brand-mark {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 14px;
|
||||
background:
|
||||
radial-gradient(circle at 30% 20%, white, transparent 22%),
|
||||
linear-gradient(135deg, var(--accent), var(--accent2));
|
||||
box-shadow:
|
||||
0 0 26px rgba(168, 85, 247, 0.45),
|
||||
0 0 50px rgba(56, 189, 248, 0.18);
|
||||
}
|
||||
|
||||
.brand strong {
|
||||
display: block;
|
||||
font-size: 1.05rem;
|
||||
letter-spacing: 0.02em;
|
||||
}
|
||||
|
||||
.brand small {
|
||||
display: block;
|
||||
color: var(--muted);
|
||||
font-size: 0.78rem;
|
||||
margin-top: -4px;
|
||||
}
|
||||
|
||||
.site-nav {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.site-nav a {
|
||||
color: var(--muted);
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
.site-nav a:hover {
|
||||
color: white;
|
||||
}
|
||||
|
||||
.nav-cta {
|
||||
padding: 9px 14px;
|
||||
border-radius: 999px;
|
||||
color: white !important;
|
||||
background: linear-gradient(135deg, var(--accent), var(--accent2));
|
||||
box-shadow: 0 0 22px rgba(56, 189, 248, 0.28);
|
||||
}
|
||||
|
||||
.hero {
|
||||
position: relative;
|
||||
padding: 92px 6vw 62px;
|
||||
max-width: 1180px;
|
||||
}
|
||||
|
||||
.hero::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: -160px -12vw auto -12vw;
|
||||
height: 420px;
|
||||
z-index: -1;
|
||||
pointer-events: none;
|
||||
background:
|
||||
radial-gradient(circle at 24% 40%, rgba(168, 85, 247, 0.20), transparent 36%),
|
||||
radial-gradient(circle at 62% 30%, rgba(56, 189, 248, 0.14), transparent 40%);
|
||||
filter: blur(74px);
|
||||
}
|
||||
|
||||
.hero-split {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) minmax(360px, 0.9fr);
|
||||
gap: 42px;
|
||||
align-items: center;
|
||||
max-width: 1500px;
|
||||
}
|
||||
|
||||
.hero h1 {
|
||||
margin: 0 0 22px;
|
||||
font-size: clamp(3rem, 8vw, 6.5rem);
|
||||
line-height: 0.95;
|
||||
letter-spacing: -0.07em;
|
||||
background: linear-gradient(90deg, #ffffff 0%, #7dd3fc 40%, #a855f7 78%);
|
||||
-webkit-background-clip: text;
|
||||
background-clip: text;
|
||||
color: transparent;
|
||||
}
|
||||
|
||||
.hero .lead,
|
||||
.hero > p,
|
||||
.section-heading p,
|
||||
.cta-panel p {
|
||||
color: var(--muted);
|
||||
font-size: 1.18rem;
|
||||
max-width: 850px;
|
||||
}
|
||||
|
||||
.badge,
|
||||
.eyebrow {
|
||||
display: inline-flex;
|
||||
width: fit-content;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
color: #e9d5ff;
|
||||
background: rgba(168, 85, 247, 0.13);
|
||||
border: 1px solid rgba(168, 85, 247, 0.34);
|
||||
padding: 6px 12px;
|
||||
border-radius: 999px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.08em;
|
||||
font-size: 0.74rem;
|
||||
font-weight: 800;
|
||||
box-shadow: 0 0 22px rgba(168, 85, 247, 0.12);
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
gap: 14px;
|
||||
margin-top: 30px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.button {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 46px;
|
||||
padding: 12px 18px;
|
||||
border-radius: 14px;
|
||||
color: #020617;
|
||||
background: linear-gradient(135deg, var(--accent), var(--accent2));
|
||||
font-weight: 800;
|
||||
box-shadow:
|
||||
0 16px 44px rgba(56, 189, 248, 0.25),
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.35);
|
||||
}
|
||||
|
||||
.button:hover {
|
||||
color: white;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.button.secondary {
|
||||
color: white;
|
||||
background: rgba(30, 41, 59, 0.85);
|
||||
border: 1px solid var(--border);
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.button.ghost {
|
||||
color: white;
|
||||
background: transparent;
|
||||
border: 1px solid var(--border);
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.hero-panel,
|
||||
.card,
|
||||
.cta-panel {
|
||||
background:
|
||||
linear-gradient(180deg, rgba(255,255,255,0.045), transparent 42%),
|
||||
var(--panel);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 26px;
|
||||
box-shadow: var(--shadow);
|
||||
-webkit-backdrop-filter: blur(16px);
|
||||
backdrop-filter: blur(16px);
|
||||
}
|
||||
|
||||
.hero-panel {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
transform: perspective(1200px) rotateY(-5deg) rotateX(2deg);
|
||||
}
|
||||
|
||||
.hero-panel::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: -1px;
|
||||
pointer-events: none;
|
||||
background:
|
||||
linear-gradient(135deg, rgba(56, 189, 248, 0.45), transparent 35%, rgba(168, 85, 247, 0.38));
|
||||
opacity: 0.18;
|
||||
}
|
||||
|
||||
.terminal-bar {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
padding: 14px 16px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
background: rgba(2, 6, 23, 0.74);
|
||||
}
|
||||
|
||||
.terminal-bar span {
|
||||
width: 11px;
|
||||
height: 11px;
|
||||
border-radius: 50%;
|
||||
background: var(--accent2);
|
||||
}
|
||||
|
||||
.terminal-bar span:nth-child(2) {
|
||||
background: #f59e0b;
|
||||
}
|
||||
|
||||
.terminal-bar span:nth-child(3) {
|
||||
background: var(--accent3);
|
||||
}
|
||||
|
||||
.terminal-bar strong {
|
||||
margin-left: 8px;
|
||||
color: var(--muted);
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.mock-grid {
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
padding: 18px;
|
||||
}
|
||||
|
||||
.mini-card {
|
||||
padding: 14px;
|
||||
border-radius: 16px;
|
||||
background: rgba(15, 23, 42, 0.88);
|
||||
border: 1px solid rgba(148, 163, 184, 0.16);
|
||||
}
|
||||
|
||||
.mini-card strong,
|
||||
.mini-card small {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.mini-card small {
|
||||
color: var(--muted);
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.section {
|
||||
padding: 52px 6vw;
|
||||
}
|
||||
|
||||
.section.narrow {
|
||||
max-width: 1040px;
|
||||
}
|
||||
|
||||
.section-heading {
|
||||
margin-bottom: 26px;
|
||||
}
|
||||
|
||||
.section h2 {
|
||||
font-size: clamp(2rem, 4vw, 3.4rem);
|
||||
line-height: 1.05;
|
||||
margin: 12px 0 14px;
|
||||
letter-spacing: -0.04em;
|
||||
}
|
||||
|
||||
.grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
|
||||
gap: 18px;
|
||||
}
|
||||
|
||||
.card {
|
||||
padding: 24px;
|
||||
transition: transform 180ms ease, border-color 180ms ease, background 180ms ease, box-shadow 180ms ease;
|
||||
}
|
||||
|
||||
.card:hover {
|
||||
transform: translateY(-4px);
|
||||
border-color: rgba(56, 189, 248, 0.45);
|
||||
background:
|
||||
linear-gradient(180deg, rgba(255,255,255,0.055), transparent 42%),
|
||||
rgba(15, 23, 42, 0.92);
|
||||
box-shadow:
|
||||
0 26px 90px rgba(0, 0, 0, 0.50),
|
||||
0 0 30px rgba(56, 189, 248, 0.08);
|
||||
}
|
||||
|
||||
.card h3,
|
||||
.card h2 {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.card p {
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.roadmap-version + .roadmap-version {
|
||||
margin-top: 24px;
|
||||
}
|
||||
|
||||
.roadmap-version ul {
|
||||
padding-left: 20px;
|
||||
}
|
||||
|
||||
.roadmap-version li {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.roadmap-done a {
|
||||
color: var(--muted);
|
||||
text-decoration: line-through;
|
||||
}
|
||||
|
||||
.feature-icon {
|
||||
font-size: 2rem;
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
.feature-icon.large {
|
||||
font-size: 4rem;
|
||||
}
|
||||
|
||||
.content-card {
|
||||
max-width: 980px;
|
||||
}
|
||||
|
||||
.tag-list {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.tag-list span {
|
||||
padding: 7px 11px;
|
||||
border-radius: 999px;
|
||||
color: var(--muted);
|
||||
background: rgba(15, 23, 42, 0.85);
|
||||
border: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.download-card {
|
||||
min-height: 220px;
|
||||
}
|
||||
|
||||
.screenshot-placeholder {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
min-height: 180px;
|
||||
margin-bottom: 16px;
|
||||
border-radius: 18px;
|
||||
background:
|
||||
linear-gradient(135deg, rgba(56, 189, 248, 0.20), rgba(168, 85, 247, 0.20)),
|
||||
rgba(2, 6, 23, 0.7);
|
||||
border: 1px solid var(--border);
|
||||
color: white;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.cta-panel {
|
||||
padding: clamp(28px, 5vw, 60px);
|
||||
}
|
||||
|
||||
.manual-content {
|
||||
max-width: 980px;
|
||||
}
|
||||
|
||||
.manual-content h1,
|
||||
.manual-content h2,
|
||||
.manual-content h3 {
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.manual-content pre {
|
||||
overflow-x: auto;
|
||||
padding: 16px;
|
||||
border-radius: 14px;
|
||||
background: #020617;
|
||||
}
|
||||
|
||||
.manual-content code {
|
||||
color: #bae6fd;
|
||||
}
|
||||
|
||||
.manual-content table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.manual-content th,
|
||||
.manual-content td {
|
||||
border-bottom: 1px solid rgba(148, 163, 184, 0.22);
|
||||
padding: 10px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.site-footer {
|
||||
padding: 42px 6vw;
|
||||
color: var(--muted);
|
||||
border-top: 1px solid var(--border);
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.hero-split {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.hero-panel {
|
||||
transform: none;
|
||||
}
|
||||
|
||||
.site-header {
|
||||
align-items: flex-start;
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
.button,
|
||||
.card {
|
||||
transition: all 180ms ease;
|
||||
}
|
||||
}
|
||||
|
||||
.footer-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 2fr repeat(3, 1fr);
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
.footer-grid strong {
|
||||
color: white;
|
||||
}
|
||||
|
||||
.footer-grid p {
|
||||
margin: 8px 0;
|
||||
}
|
||||
|
||||
@media (max-width: 800px) {
|
||||
.footer-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
/* ================================================================
|
||||
KST4Contest brand recovery: green operator-console identity
|
||||
and full-screen interactive hero
|
||||
================================================================ */
|
||||
|
||||
:root {
|
||||
--bg: #050806;
|
||||
--bg2: #0b140f;
|
||||
--panel: rgba(15, 28, 20, 0.82);
|
||||
--panel2: rgba(24, 43, 31, 0.84);
|
||||
--border: rgba(125, 178, 134, 0.22);
|
||||
--text: #f4f7f3;
|
||||
--muted: #a9b8ad;
|
||||
--soft: #6e8173;
|
||||
--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);
|
||||
}
|
||||
|
||||
body {
|
||||
background:
|
||||
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%);
|
||||
}
|
||||
|
||||
body::before {
|
||||
background:
|
||||
radial-gradient(circle at 20% 20%, rgba(60, 173, 61, 0.18), transparent 28%),
|
||||
radial-gradient(circle at 78% 18%, rgba(97, 222, 80, 0.13), transparent 30%),
|
||||
radial-gradient(circle at 45% 80%, rgba(34, 122, 53, 0.11), transparent 26%);
|
||||
}
|
||||
|
||||
body::after {
|
||||
background-image:
|
||||
linear-gradient(rgba(160,255,160,0.032) 1px, transparent 1px),
|
||||
linear-gradient(90deg, rgba(160,255,160,0.032) 1px, transparent 1px);
|
||||
}
|
||||
|
||||
.site-bg {
|
||||
background:
|
||||
linear-gradient(180deg, rgba(5, 8, 6, 0.10), rgba(5, 8, 6, 0.84)),
|
||||
radial-gradient(circle at 8% 24%, rgba(52, 165, 57, 0.14), transparent 26%),
|
||||
radial-gradient(circle at 82% 20%, rgba(95, 218, 82, 0.12), transparent 30%);
|
||||
}
|
||||
|
||||
.site-header {
|
||||
background: rgba(5, 8, 7, 0.82);
|
||||
}
|
||||
|
||||
.brand-mark,
|
||||
.nav-cta,
|
||||
.button {
|
||||
background: linear-gradient(135deg, var(--accent), var(--accent2));
|
||||
}
|
||||
|
||||
.brand-mark {
|
||||
box-shadow:
|
||||
0 0 26px rgba(84, 214, 61, 0.42),
|
||||
0 0 50px rgba(49, 181, 74, 0.16);
|
||||
}
|
||||
|
||||
.nav-cta {
|
||||
color: #061008 !important;
|
||||
box-shadow: 0 0 22px rgba(84, 214, 61, 0.28);
|
||||
}
|
||||
|
||||
main {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.hero.hero-split {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
max-width: none;
|
||||
min-height: calc(100vh - 73px);
|
||||
display: grid;
|
||||
grid-template-columns: minmax(420px, 0.9fr) minmax(520px, 1.1fr);
|
||||
gap: clamp(40px, 5vw, 110px);
|
||||
align-items: center;
|
||||
align-content: center;
|
||||
padding: clamp(56px, 7vh, 100px) clamp(28px, 6vw, 110px);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.hero.hero-split > *:not(.hero-radio-fx):not(.hero-fx-vignette) {
|
||||
position: relative;
|
||||
z-index: 3;
|
||||
}
|
||||
|
||||
.hero-copy {
|
||||
max-width: 760px;
|
||||
}
|
||||
|
||||
.hero.hero-split h1 {
|
||||
font-size: clamp(3.4rem, 8vw, 7rem);
|
||||
background: linear-gradient(90deg, #ffffff 0%, #c9ffd0 28%, #77ec70 58%, #45bd4d 100%);
|
||||
-webkit-background-clip: text;
|
||||
background-clip: text;
|
||||
color: transparent;
|
||||
}
|
||||
|
||||
.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);
|
||||
}
|
||||
|
||||
.button {
|
||||
color: #041007;
|
||||
box-shadow:
|
||||
0 16px 44px rgba(84, 214, 61, 0.22),
|
||||
inset 0 1px 0 rgba(255,255,255,0.35);
|
||||
}
|
||||
|
||||
.button.secondary {
|
||||
color: white;
|
||||
background: rgba(28, 43, 32, 0.90);
|
||||
}
|
||||
|
||||
.hero-panel {
|
||||
width: min(100%, 760px);
|
||||
justify-self: center;
|
||||
transform: perspective(1200px) rotateY(-4deg) rotateX(1.5deg);
|
||||
}
|
||||
|
||||
.hero-panel,
|
||||
.card,
|
||||
.cta-panel {
|
||||
background:
|
||||
linear-gradient(180deg, rgba(255,255,255,0.045), transparent 42%),
|
||||
rgba(15, 28, 20, 0.82);
|
||||
}
|
||||
|
||||
.mini-card {
|
||||
background: rgba(12, 25, 17, 0.88);
|
||||
border-color: rgba(148, 184, 155, 0.16);
|
||||
}
|
||||
|
||||
.card:hover {
|
||||
border-color: rgba(84, 214, 61, 0.42);
|
||||
background:
|
||||
linear-gradient(180deg, rgba(255,255,255,0.055), transparent 42%),
|
||||
rgba(12, 25, 17, 0.94);
|
||||
box-shadow:
|
||||
0 26px 90px rgba(0,0,0,0.50),
|
||||
0 0 30px rgba(84,214,61,0.07);
|
||||
}
|
||||
|
||||
.hero-radio-fx {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
pointer-events: none;
|
||||
opacity: 0.78;
|
||||
}
|
||||
|
||||
.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.70) 32%,
|
||||
rgba(7,11,9,0.17) 62%,
|
||||
rgba(7,11,9,0.38) 100%),
|
||||
linear-gradient(180deg,
|
||||
rgba(7,11,9,0.08),
|
||||
rgba(7,11,9,0.58));
|
||||
}
|
||||
|
||||
html[data-theme="light"] {
|
||||
--bg: #f4f4f2;
|
||||
--bg2: #ffffff;
|
||||
--panel: rgba(255,255,255,0.90);
|
||||
--panel2: rgba(242,242,239,0.94);
|
||||
--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);
|
||||
}
|
||||
|
||||
@media (min-width: 1600px) {
|
||||
.hero.hero-split {
|
||||
grid-template-columns: minmax(520px, 0.85fr) minmax(620px, 1.15fr);
|
||||
}
|
||||
|
||||
.hero-panel {
|
||||
width: min(100%, 820px);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 1000px) {
|
||||
.hero.hero-split {
|
||||
min-height: auto;
|
||||
grid-template-columns: 1fr;
|
||||
padding-top: 64px;
|
||||
padding-bottom: 64px;
|
||||
}
|
||||
|
||||
.hero-panel {
|
||||
width: min(100%, 760px);
|
||||
transform: none;
|
||||
}
|
||||
|
||||
.hero-radio-fx {
|
||||
opacity: 0.50;
|
||||
}
|
||||
|
||||
.hero-fx-vignette {
|
||||
background: linear-gradient(180deg, rgba(7,11,9,0.84), rgba(7,11,9,0.34));
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.hero-radio-fx {
|
||||
opacity: 0.28;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,509 @@
|
||||
(() => {
|
||||
"use strict";
|
||||
|
||||
const canvas = document.querySelector("[data-hero-radio-fx]");
|
||||
if (!(canvas instanceof HTMLCanvasElement)) return;
|
||||
|
||||
const hero = canvas.closest(".hero");
|
||||
const ctx = canvas.getContext("2d", { alpha: true, desynchronized: true });
|
||||
if (!(hero instanceof HTMLElement) || !ctx) return;
|
||||
|
||||
const reducedMotion = matchMedia("(prefers-reduced-motion: reduce)");
|
||||
const coarsePointer = matchMedia("(pointer: coarse)");
|
||||
|
||||
const state = {
|
||||
width: 0,
|
||||
height: 0,
|
||||
dpr: 1,
|
||||
running: false,
|
||||
visible: true,
|
||||
lastFrame: 0,
|
||||
spawnTime: 0,
|
||||
beamTime: 0,
|
||||
frameId: 0,
|
||||
pointer: { x: 0, y: 0, active: false },
|
||||
planes: [],
|
||||
beams: [],
|
||||
reflections: []
|
||||
};
|
||||
|
||||
const clamp = (v, min, max) => Math.max(min, Math.min(max, v));
|
||||
const random = (min, max) => min + Math.random() * (max - min);
|
||||
|
||||
function css(name, fallback) {
|
||||
return getComputedStyle(document.documentElement).getPropertyValue(name).trim() || fallback;
|
||||
}
|
||||
|
||||
function palette() {
|
||||
return {
|
||||
plane: css("--fx-plane", "#79ff73"),
|
||||
planeMuted: css("--fx-plane-muted", "rgba(121,255,115,.42)"),
|
||||
beam: css("--fx-beam", "#52ff65"),
|
||||
beamGlow: css("--fx-beam-glow", "rgba(82,255,101,.28)"),
|
||||
reflection: css("--fx-reflection", "#d2ffc8"),
|
||||
trail: css("--fx-trail", "rgba(121,255,115,.17)"),
|
||||
grid: css("--fx-grid", "rgba(121,255,115,.055)")
|
||||
};
|
||||
}
|
||||
|
||||
function roundedRect(x, y, width, height, radius) {
|
||||
const r = Math.min(radius, width / 2, height / 2);
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(x + r, y);
|
||||
ctx.arcTo(x + width, y, x + width, y + height, r);
|
||||
ctx.arcTo(x + width, y + height, x, y + height, r);
|
||||
ctx.arcTo(x, y + height, x, y, r);
|
||||
ctx.arcTo(x, y, x + width, y, r);
|
||||
ctx.closePath();
|
||||
}
|
||||
|
||||
function resize() {
|
||||
const rect = hero.getBoundingClientRect();
|
||||
state.width = Math.max(1, Math.round(rect.width));
|
||||
state.height = Math.max(1, Math.round(rect.height));
|
||||
state.dpr = Math.min(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`;
|
||||
ctx.setTransform(state.dpr, 0, 0, state.dpr, 0, 0);
|
||||
|
||||
if (!state.pointer.active) {
|
||||
state.pointer.x = state.width * 0.55;
|
||||
state.pointer.y = state.height * 0.42;
|
||||
}
|
||||
}
|
||||
|
||||
function stations() {
|
||||
const targetX = state.width * 0.54;
|
||||
const targetY = state.height * 0.43;
|
||||
const left = { x: state.width * 0.075, y: state.height * 0.82, label: "JO51IJ" };
|
||||
const right = { x: state.width * 0.925, y: state.height * 0.80, label: "JN49FL" };
|
||||
left.angle = Math.atan2(targetY - left.y, targetX - left.x);
|
||||
right.angle = Math.atan2(targetY - right.y, targetX - right.x);
|
||||
return [left, right];
|
||||
}
|
||||
|
||||
function planeLimit() {
|
||||
if (coarsePointer.matches) return 5;
|
||||
return state.width < 900 ? 7 : 12;
|
||||
}
|
||||
|
||||
function createPlane() {
|
||||
const margin = 55;
|
||||
const edge = Math.floor(Math.random() * 4);
|
||||
let x;
|
||||
let y;
|
||||
|
||||
if (edge === 0) {
|
||||
x = random(0, state.width);
|
||||
y = -margin;
|
||||
} else if (edge === 1) {
|
||||
x = state.width + margin;
|
||||
y = random(0, state.height);
|
||||
} else if (edge === 2) {
|
||||
x = random(0, state.width);
|
||||
y = state.height + margin;
|
||||
} else {
|
||||
x = -margin;
|
||||
y = random(0, state.height);
|
||||
}
|
||||
|
||||
const tx = state.pointer.active ? state.pointer.x : state.width * random(0.38, 0.72);
|
||||
const ty = state.pointer.active ? state.pointer.y : state.height * random(0.20, 0.72);
|
||||
const angle = Math.atan2(ty - y, tx - x);
|
||||
const speed = random(18, 34);
|
||||
|
||||
return {
|
||||
x, y, angle,
|
||||
vx: Math.cos(angle) * speed,
|
||||
vy: Math.sin(angle) * speed,
|
||||
size: random(5.5, 9),
|
||||
age: 0,
|
||||
maxAge: random(12, 20),
|
||||
steering: random(0.65, 1.2),
|
||||
wobble: random(0, Math.PI * 2),
|
||||
wobbleSpeed: random(0.7, 1.4),
|
||||
hit: 0
|
||||
};
|
||||
}
|
||||
|
||||
function updatePlane(plane, dt) {
|
||||
plane.age += dt;
|
||||
plane.wobble += dt * plane.wobbleSpeed;
|
||||
|
||||
const tx = state.pointer.active ? state.pointer.x : state.width * 0.56;
|
||||
const ty = state.pointer.active ? state.pointer.y : state.height * 0.44;
|
||||
const desired = Math.atan2(ty - plane.y, tx - plane.x);
|
||||
let diff = desired - plane.angle;
|
||||
while (diff > Math.PI) diff -= Math.PI * 2;
|
||||
while (diff < -Math.PI) diff += Math.PI * 2;
|
||||
plane.angle += diff * plane.steering * dt;
|
||||
|
||||
const speed = Math.hypot(plane.vx, plane.vy);
|
||||
const wobble = Math.sin(plane.wobble) * 0.14;
|
||||
plane.vx = Math.cos(plane.angle + wobble) * speed;
|
||||
plane.vy = Math.sin(plane.angle + wobble) * speed;
|
||||
plane.x += plane.vx * dt;
|
||||
plane.y += plane.vy * dt;
|
||||
plane.hit = Math.max(0, plane.hit - dt * 1.8);
|
||||
}
|
||||
|
||||
function expired(plane) {
|
||||
const margin = 180;
|
||||
return plane.age > plane.maxAge || plane.x < -margin || plane.x > state.width + margin || plane.y < -margin || plane.y > state.height + margin;
|
||||
}
|
||||
|
||||
function beamTarget() {
|
||||
if (!state.planes.length) return null;
|
||||
const tx = state.pointer.active ? state.pointer.x : state.width * 0.56;
|
||||
const ty = state.pointer.active ? state.pointer.y : state.height * 0.44;
|
||||
const nearby = state.planes
|
||||
.map(plane => ({ plane, distance: Math.hypot(plane.x - tx, plane.y - ty) }))
|
||||
.filter(item => item.distance < 280)
|
||||
.sort((a, b) => a.distance - b.distance);
|
||||
return nearby[0]?.plane || state.planes[Math.floor(Math.random() * state.planes.length)];
|
||||
}
|
||||
|
||||
function createBeam(target) {
|
||||
const source = stations()
|
||||
.map(station => ({ station, distance: Math.hypot(target.x - station.x, target.y - station.y) }))
|
||||
.sort((a, b) => a.distance - b.distance)[0].station;
|
||||
|
||||
const angle = Math.atan2(target.y - source.y, target.x - source.x);
|
||||
const tip = 45;
|
||||
state.beams.push({
|
||||
startX: source.x + Math.cos(angle) * tip,
|
||||
startY: source.y + Math.sin(angle) * tip,
|
||||
endX: target.x,
|
||||
endY: target.y,
|
||||
age: 0,
|
||||
maxAge: 0.72
|
||||
});
|
||||
|
||||
target.hit = 1;
|
||||
const values = [50, 75, 100];
|
||||
state.reflections.push({
|
||||
x: target.x,
|
||||
y: target.y,
|
||||
age: 0,
|
||||
maxAge: 1.25,
|
||||
radius: random(8, 13),
|
||||
probability: values[Math.floor(Math.random() * values.length)]
|
||||
});
|
||||
}
|
||||
|
||||
function updateEffects(dt) {
|
||||
state.beams.forEach(beam => beam.age += dt);
|
||||
state.beams = state.beams.filter(beam => beam.age < beam.maxAge);
|
||||
state.reflections.forEach(r => {
|
||||
r.age += dt;
|
||||
r.radius += dt * 30;
|
||||
});
|
||||
state.reflections = state.reflections.filter(r => r.age < r.maxAge);
|
||||
}
|
||||
|
||||
function drawGrid(p) {
|
||||
ctx.save();
|
||||
ctx.strokeStyle = p.grid;
|
||||
ctx.lineWidth = 1;
|
||||
const spacing = 54;
|
||||
for (let x = spacing; x < state.width; x += spacing) {
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(x, 0);
|
||||
ctx.lineTo(x, state.height);
|
||||
ctx.stroke();
|
||||
}
|
||||
for (let y = spacing; y < state.height; y += spacing) {
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(0, y);
|
||||
ctx.lineTo(state.width, y);
|
||||
ctx.stroke();
|
||||
}
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
function drawRadar(p) {
|
||||
const x = state.pointer.active ? state.pointer.x : state.width * 0.56;
|
||||
const y = state.pointer.active ? state.pointer.y : state.height * 0.44;
|
||||
ctx.save();
|
||||
ctx.strokeStyle = p.planeMuted;
|
||||
ctx.lineWidth = 1;
|
||||
for (const radius of [24, 48, 82]) {
|
||||
ctx.globalAlpha = 0.24 - radius / 500;
|
||||
ctx.beginPath();
|
||||
ctx.arc(x, y, radius, 0, Math.PI * 2);
|
||||
ctx.stroke();
|
||||
}
|
||||
ctx.globalAlpha = 0.2;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(x - 100, y);
|
||||
ctx.lineTo(x + 100, y);
|
||||
ctx.moveTo(x, y - 100);
|
||||
ctx.lineTo(x, y + 100);
|
||||
ctx.stroke();
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
function drawYagi(station, p) {
|
||||
ctx.save();
|
||||
ctx.translate(station.x, station.y);
|
||||
ctx.rotate(station.angle);
|
||||
ctx.strokeStyle = p.plane;
|
||||
ctx.fillStyle = p.plane;
|
||||
ctx.lineWidth = 1.5;
|
||||
ctx.lineCap = "round";
|
||||
ctx.shadowColor = p.beamGlow;
|
||||
ctx.shadowBlur = 9;
|
||||
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(-30, 26);
|
||||
ctx.lineTo(-30, -5);
|
||||
ctx.stroke();
|
||||
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(-32, 0);
|
||||
ctx.lineTo(43, 0);
|
||||
ctx.stroke();
|
||||
|
||||
for (const element of [
|
||||
{ x: -27, length: 34 },
|
||||
{ x: -12, length: 28 },
|
||||
{ x: 2, length: 22 },
|
||||
{ x: 14, length: 19 },
|
||||
{ x: 25, length: 16 },
|
||||
{ x: 35, length: 13 }
|
||||
]) {
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(element.x, -element.length / 2);
|
||||
ctx.lineTo(element.x, element.length / 2);
|
||||
ctx.stroke();
|
||||
}
|
||||
|
||||
ctx.beginPath();
|
||||
ctx.arc(-12, 0, 3.2, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
ctx.restore();
|
||||
|
||||
ctx.save();
|
||||
ctx.font = "700 11px Inter, system-ui, sans-serif";
|
||||
const width = ctx.measureText(station.label).width + 14;
|
||||
const x = station.x - width / 2;
|
||||
const y = station.y + 32;
|
||||
ctx.fillStyle = "rgba(6,18,10,.86)";
|
||||
ctx.strokeStyle = p.planeMuted;
|
||||
roundedRect(x, y, width, 23, 6);
|
||||
ctx.fill();
|
||||
ctx.stroke();
|
||||
ctx.fillStyle = p.plane;
|
||||
ctx.textAlign = "center";
|
||||
ctx.textBaseline = "middle";
|
||||
ctx.fillText(station.label, station.x, y + 11.5);
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
function drawPlane(plane, p) {
|
||||
ctx.save();
|
||||
ctx.translate(plane.x, plane.y);
|
||||
ctx.rotate(plane.angle);
|
||||
ctx.globalAlpha = clamp(Math.min(plane.age * 1.8, (plane.maxAge - plane.age) * 1.8), 0, 1);
|
||||
ctx.strokeStyle = p.trail;
|
||||
ctx.lineWidth = 1;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(-plane.size * 4.5, 0);
|
||||
ctx.lineTo(-plane.size * 1.4, 0);
|
||||
ctx.stroke();
|
||||
|
||||
ctx.fillStyle = plane.hit > 0 ? p.reflection : p.plane;
|
||||
ctx.shadowColor = plane.hit > 0 ? p.reflection : p.beamGlow;
|
||||
ctx.shadowBlur = plane.hit > 0 ? 16 : 5;
|
||||
const s = plane.size;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(s * 1.65, 0);
|
||||
ctx.lineTo(-s * 0.7, s * 0.34);
|
||||
ctx.lineTo(-s * 0.15, s * 1.05);
|
||||
ctx.lineTo(-s * 0.65, s * 1.08);
|
||||
ctx.lineTo(-s * 1.35, s * 0.3);
|
||||
ctx.lineTo(-s * 1.7, s * 0.28);
|
||||
ctx.lineTo(-s * 1.12, 0);
|
||||
ctx.lineTo(-s * 1.7, -s * 0.28);
|
||||
ctx.lineTo(-s * 1.35, -s * 0.3);
|
||||
ctx.lineTo(-s * 0.65, -s * 1.08);
|
||||
ctx.lineTo(-s * 0.15, -s * 1.05);
|
||||
ctx.lineTo(-s * 0.7, -s * 0.34);
|
||||
ctx.closePath();
|
||||
ctx.fill();
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
function drawBeam(beam, p) {
|
||||
const progress = beam.age / beam.maxAge;
|
||||
const alpha = Math.sin(progress * Math.PI);
|
||||
ctx.save();
|
||||
ctx.globalAlpha = alpha;
|
||||
ctx.strokeStyle = p.beamGlow;
|
||||
ctx.lineWidth = 8;
|
||||
ctx.shadowColor = p.beam;
|
||||
ctx.shadowBlur = 15;
|
||||
ctx.setLineDash([10, 6]);
|
||||
ctx.lineDashOffset = -progress * 36;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(beam.startX, beam.startY);
|
||||
ctx.lineTo(beam.endX, beam.endY);
|
||||
ctx.stroke();
|
||||
ctx.setLineDash([]);
|
||||
ctx.strokeStyle = p.beam;
|
||||
ctx.lineWidth = 1.8;
|
||||
ctx.shadowBlur = 5;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(beam.startX, beam.startY);
|
||||
ctx.lineTo(beam.endX, beam.endY);
|
||||
ctx.stroke();
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
function drawReflection(r, p) {
|
||||
const progress = r.age / r.maxAge;
|
||||
const alpha = 1 - progress;
|
||||
ctx.save();
|
||||
ctx.translate(r.x, r.y);
|
||||
ctx.globalAlpha = alpha;
|
||||
ctx.strokeStyle = p.reflection;
|
||||
ctx.lineWidth = 1.6;
|
||||
ctx.shadowColor = p.beam;
|
||||
ctx.shadowBlur = 18;
|
||||
for (const scale of [1, 1.65]) {
|
||||
ctx.globalAlpha = alpha * (scale === 1 ? 1 : 0.55);
|
||||
ctx.beginPath();
|
||||
ctx.arc(0, 0, r.radius * scale, 0, Math.PI * 2);
|
||||
ctx.stroke();
|
||||
}
|
||||
ctx.globalAlpha = alpha * 0.8;
|
||||
const cross = r.radius * 1.25;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(-cross, 0);
|
||||
ctx.lineTo(cross, 0);
|
||||
ctx.moveTo(0, -cross);
|
||||
ctx.lineTo(0, cross);
|
||||
ctx.stroke();
|
||||
ctx.globalAlpha = alpha;
|
||||
ctx.fillStyle = p.reflection;
|
||||
ctx.beginPath();
|
||||
ctx.arc(0, 0, 3.8, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
ctx.restore();
|
||||
|
||||
ctx.save();
|
||||
const label = `${r.probability}%`;
|
||||
ctx.font = "800 12px Inter, system-ui, sans-serif";
|
||||
const width = ctx.measureText(label).width + 16;
|
||||
const height = 25;
|
||||
const x = r.x + r.radius + 12;
|
||||
const y = r.y - r.radius - 17;
|
||||
ctx.globalAlpha = clamp(alpha * 1.25, 0, 1);
|
||||
ctx.fillStyle = "rgba(6,18,10,.91)";
|
||||
ctx.strokeStyle = p.plane;
|
||||
ctx.shadowColor = p.beamGlow;
|
||||
ctx.shadowBlur = 10;
|
||||
roundedRect(x, y, width, height, 6);
|
||||
ctx.fill();
|
||||
ctx.stroke();
|
||||
ctx.shadowBlur = 0;
|
||||
ctx.fillStyle = p.reflection;
|
||||
ctx.textAlign = "center";
|
||||
ctx.textBaseline = "middle";
|
||||
ctx.fillText(label, x + width / 2, y + height / 2);
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
function draw(staticOnly = false) {
|
||||
const p = palette();
|
||||
ctx.clearRect(0, 0, state.width, state.height);
|
||||
drawGrid(p);
|
||||
drawRadar(p);
|
||||
stations().forEach(station => drawYagi(station, p));
|
||||
if (staticOnly) return;
|
||||
state.planes.forEach(plane => drawPlane(plane, p));
|
||||
state.beams.forEach(beam => drawBeam(beam, p));
|
||||
state.reflections.forEach(r => drawReflection(r, p));
|
||||
}
|
||||
|
||||
function animate(timestamp) {
|
||||
if (!state.running) return;
|
||||
const dt = clamp((timestamp - state.lastFrame) / 1000 || 0, 0, 0.05);
|
||||
state.lastFrame = timestamp;
|
||||
|
||||
if (!state.visible || document.hidden) {
|
||||
state.frameId = requestAnimationFrame(animate);
|
||||
return;
|
||||
}
|
||||
|
||||
state.spawnTime += dt;
|
||||
state.beamTime += dt;
|
||||
const spawnInterval = coarsePointer.matches ? 2.2 : 1.15;
|
||||
const beamInterval = coarsePointer.matches ? 3.3 : 1.8;
|
||||
|
||||
if (state.spawnTime >= spawnInterval && state.planes.length < planeLimit()) {
|
||||
state.spawnTime = 0;
|
||||
state.planes.push(createPlane());
|
||||
}
|
||||
|
||||
if (state.beamTime >= beamInterval && state.planes.length) {
|
||||
state.beamTime = 0;
|
||||
const target = beamTarget();
|
||||
if (target) createBeam(target);
|
||||
}
|
||||
|
||||
state.planes.forEach(plane => updatePlane(plane, dt));
|
||||
state.planes = state.planes.filter(plane => !expired(plane));
|
||||
updateEffects(dt);
|
||||
draw(false);
|
||||
state.frameId = requestAnimationFrame(animate);
|
||||
}
|
||||
|
||||
function start() {
|
||||
cancelAnimationFrame(state.frameId);
|
||||
resize();
|
||||
if (reducedMotion.matches) {
|
||||
state.running = false;
|
||||
draw(true);
|
||||
return;
|
||||
}
|
||||
|
||||
state.running = true;
|
||||
state.lastFrame = performance.now();
|
||||
if (!state.planes.length) {
|
||||
const count = coarsePointer.matches ? 2 : 5;
|
||||
for (let i = 0; i < count; i++) {
|
||||
const plane = createPlane();
|
||||
plane.age = Math.random() * 2;
|
||||
state.planes.push(plane);
|
||||
}
|
||||
}
|
||||
state.frameId = requestAnimationFrame(animate);
|
||||
}
|
||||
|
||||
hero.addEventListener("pointermove", event => {
|
||||
const rect = hero.getBoundingClientRect();
|
||||
state.pointer.x = clamp(event.clientX - rect.left, 0, rect.width);
|
||||
state.pointer.y = clamp(event.clientY - rect.top, 0, rect.height);
|
||||
state.pointer.active = true;
|
||||
}, { passive: true });
|
||||
|
||||
hero.addEventListener("pointerleave", () => state.pointer.active = false);
|
||||
addEventListener("resize", resize, { passive: true });
|
||||
document.addEventListener("visibilitychange", () => state.lastFrame = performance.now());
|
||||
reducedMotion.addEventListener("change", start);
|
||||
|
||||
new ResizeObserver(() => {
|
||||
resize();
|
||||
if (reducedMotion.matches) draw(true);
|
||||
}).observe(hero);
|
||||
|
||||
new IntersectionObserver(entries => {
|
||||
state.visible = entries[0]?.isIntersecting ?? true;
|
||||
state.lastFrame = performance.now();
|
||||
}, { threshold: 0.05 }).observe(hero);
|
||||
|
||||
start();
|
||||
})();
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 2.3 MiB |
@@ -0,0 +1,33 @@
|
||||
---
|
||||
layout: base.njk
|
||||
title: Contact | KST4Contest
|
||||
description: Contact options for KST4Contest.
|
||||
---
|
||||
|
||||
<section class="hero">
|
||||
<p class="badge">Contact</p>
|
||||
<h1>Contact</h1>
|
||||
<p class="lead">Get in touch, report issues or contribute to KST4Contest.</p>
|
||||
</section>
|
||||
|
||||
<section class="section">
|
||||
<div class="grid">
|
||||
<article class="card">
|
||||
<h3>GitHub Issues</h3>
|
||||
<p>Report bugs, request features or discuss technical problems.</p>
|
||||
<a href="https://github.com/praktimarc/kst4contest/issues">Open issues →</a>
|
||||
</article>
|
||||
|
||||
<article class="card">
|
||||
<h3>GitHub Repository</h3>
|
||||
<p>Browse source code, releases and documentation.</p>
|
||||
<a href="https://github.com/praktimarc/kst4contest">Open repository →</a>
|
||||
</article>
|
||||
|
||||
<article class="card">
|
||||
<h3>Email</h3>
|
||||
<p>For legal or website-related contact.</p>
|
||||
<a href="https://groups.google.com/g/kst4contest">Join the mailing list →</a>
|
||||
</article>
|
||||
</div>
|
||||
</section>
|
||||
@@ -0,0 +1,80 @@
|
||||
---
|
||||
layout: base.njk
|
||||
title: Download KST4Contest
|
||||
description: Download the latest stable KST4Contest packages for Windows, Linux and macOS. The required Java runtime is included.
|
||||
---
|
||||
|
||||
<section class="hero">
|
||||
<p class="badge">Stable release</p>
|
||||
<h1>Download KST4Contest</h1>
|
||||
<p class="lead">
|
||||
Choose the package for your operating system. Official builds are published through GitHub Releases,
|
||||
and the required Java runtime is already included.
|
||||
</p>
|
||||
|
||||
<div class="actions">
|
||||
<a class="button" href="https://github.com/praktimarc/kst4contest/releases/latest">Latest release on GitHub</a>
|
||||
<a class="button secondary" href="https://github.com/praktimarc/kst4contest/releases">All releases</a>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="section">
|
||||
<div class="section-heading">
|
||||
<p class="eyebrow">Packages by platform</p>
|
||||
<h2>Choose the format that fits your system</h2>
|
||||
</div>
|
||||
|
||||
<div class="grid">
|
||||
{% for item in downloads %}
|
||||
<article class="card download-card">
|
||||
<div class="feature-icon">{{ item.icon }}</div>
|
||||
|
||||
{% if item.recommended %}
|
||||
<p class="badge">Default choice</p>
|
||||
{% endif %}
|
||||
|
||||
<h3>{{ item.os }}</h3>
|
||||
<p><strong>{{ item.format }}</strong></p>
|
||||
<p>{{ item.note }}</p>
|
||||
|
||||
<a class="button secondary" href="{{ item.url }}">Download →</a>
|
||||
</article>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="section">
|
||||
<div class="cta-panel">
|
||||
<p class="eyebrow">Choosing a package</p>
|
||||
<h2>Which file should I use?</h2>
|
||||
<p>
|
||||
Windows users normally need the x64 ZIP package. On Linux, Flatpak is the practical choice when
|
||||
updates should be managed through the desktop software centre or the Flatpak command line. The
|
||||
AppImage is useful when a portable file is preferred, while DEB, RPM and Arch packages integrate
|
||||
with their respective package managers.
|
||||
</p>
|
||||
<p>
|
||||
macOS packages are provided for Apple Silicon and Intel systems on a best-effort basis. They are
|
||||
not currently notarized by Apple. The installation guide covers the first launch, updates, the
|
||||
Stable, Beta and Nightly channels, and the location of the settings directory.
|
||||
</p>
|
||||
<div class="actions">
|
||||
<a class="button" href="/manual/en/installation/">Read the installation guide</a>
|
||||
<a class="button secondary" href="/faq/">Open the FAQ</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="section">
|
||||
<div class="card content-card">
|
||||
<h2>Development and infrastructure</h2>
|
||||
<p>
|
||||
KST4Contest is developed independently. If it is useful in your contest workflow and you want to
|
||||
help cover continued development, testing and infrastructure costs, the support page explains
|
||||
what project contributions are used for.
|
||||
</p>
|
||||
<div class="actions">
|
||||
<a class="button" href="/support/">Support the project</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
@@ -0,0 +1,76 @@
|
||||
---
|
||||
layout: base.njk
|
||||
title: KST4Contest FAQ
|
||||
description: Frequently asked questions about KST4Contest, ON4KST contest operation, AirScout integration and VHF/UHF/SHF contest workflow.
|
||||
---
|
||||
|
||||
<section class="hero">
|
||||
<h1>Frequently asked questions</h1>
|
||||
<p>
|
||||
Answers about KST4Contest, ON4KST contest operation, downloads,
|
||||
AirScout integration and supported platforms.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section class="section">
|
||||
<div class="card">
|
||||
<h2>What is KST4Contest?</h2>
|
||||
<p>
|
||||
KST4Contest is a contest-optimized ON4KST chat client for VHF, UHF and SHF ham radio contest operation.
|
||||
</p>
|
||||
|
||||
<h2>Is KST4Contest a replacement for wtKST, KST2Me or KSTChat?</h2>
|
||||
<p>
|
||||
KST4Contest follows a different approach. It is focused on contest workflow, priority candidates,
|
||||
sked handling, AirScout support, log synchronization and operator decision support.
|
||||
</p>
|
||||
|
||||
<h2>Which platforms are supported?</h2>
|
||||
<p>
|
||||
KST4Contest provides release artifacts for Windows, Linux and macOS through GitHub Releases.
|
||||
</p>
|
||||
|
||||
<h2>Does KST4Contest support AirScout?</h2>
|
||||
<p>
|
||||
Yes. KST4Contest is designed to integrate AirScout-related workflow information into contest operation.
|
||||
</p>
|
||||
|
||||
<h2>Where can I download KST4Contest?</h2>
|
||||
<p>
|
||||
Official builds are available on the GitHub Releases page.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<script type="application/ld+json">
|
||||
{
|
||||
"@context": "https://schema.org",
|
||||
"@type": "FAQPage",
|
||||
"mainEntity": [
|
||||
{
|
||||
"@type": "Question",
|
||||
"name": "What is KST4Contest?",
|
||||
"acceptedAnswer": {
|
||||
"@type": "Answer",
|
||||
"text": "KST4Contest is a contest-optimized ON4KST chat client for VHF, UHF and SHF ham radio contest operation."
|
||||
}
|
||||
},
|
||||
{
|
||||
"@type": "Question",
|
||||
"name": "Is KST4Contest a replacement for wtKST, KST2Me or KSTChat?",
|
||||
"acceptedAnswer": {
|
||||
"@type": "Answer",
|
||||
"text": "KST4Contest follows a different approach and focuses on contest workflow, priority candidates, sked handling, AirScout support, log synchronization and operator decision support."
|
||||
}
|
||||
},
|
||||
{
|
||||
"@type": "Question",
|
||||
"name": "Which platforms are supported?",
|
||||
"acceptedAnswer": {
|
||||
"@type": "Answer",
|
||||
"text": "KST4Contest provides release artifacts for Windows, Linux and macOS through GitHub Releases."
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,27 @@
|
||||
---
|
||||
title: AirScout Integration
|
||||
icon: ✈️
|
||||
category: Aircraft Scatter
|
||||
since: "1.26"
|
||||
summary: Use AirScout AP predictions in candidate evaluation, the timeline and sked planning.
|
||||
description: KST4Contest receives aircraft scatter information from AirScout and relates AP timing to active ON4KST stations.
|
||||
tagsList:
|
||||
- AirScout
|
||||
- airplane scatter
|
||||
- aircraft scatter
|
||||
- VHF contest
|
||||
related:
|
||||
- timeline
|
||||
- priority-score
|
||||
- sked-reminder
|
||||
---
|
||||
|
||||
## Airplane scatter in the operator workflow
|
||||
|
||||
Aircraft reflections can create short but valuable contact opportunities.
|
||||
|
||||
KST4Contest helps combine ON4KST chat activity with AirScout-based propagation awareness.
|
||||
|
||||
## From candidate to sked decision
|
||||
|
||||
AirScout information can support the decision whether a candidate station should be called immediately, scheduled later or monitored for a better window.
|
||||
@@ -0,0 +1,25 @@
|
||||
---
|
||||
title: Dual Chat Categories
|
||||
icon: 💬
|
||||
category: ON4KST Chat
|
||||
since: "1.26"
|
||||
summary: Log in to two ON4KST categories and handle both message streams in one station list and interface.
|
||||
description: KST4Contest combines two ON4KST chat categories while retaining the category required for messages and station context.
|
||||
tagsList:
|
||||
- ON4KST
|
||||
- dual chat
|
||||
- microwave contest
|
||||
related:
|
||||
- priority-score
|
||||
- log-sync
|
||||
---
|
||||
|
||||
## Two categories, one workflow
|
||||
|
||||
KST4Contest can operate with two ON4KST chat categories at once.
|
||||
|
||||
This is useful when contest activity spans VHF/UHF and microwave operation.
|
||||
|
||||
## Less window switching
|
||||
|
||||
The operator can keep more information visible without constantly changing tools.
|
||||
@@ -0,0 +1,24 @@
|
||||
---
|
||||
title: DXCluster Server
|
||||
icon: 📡
|
||||
category: Radio Workflow
|
||||
since: "1.23"
|
||||
summary: Send direction warnings as DX Cluster spots to compatible logging software when a usable QRG is known.
|
||||
description: The built-in DX Cluster server forwards KST4Contest direction warnings and known frequency information to compatible contest loggers.
|
||||
tagsList:
|
||||
- DXCluster
|
||||
- ham radio
|
||||
- contest
|
||||
related:
|
||||
- log-sync
|
||||
- priority-score
|
||||
---
|
||||
|
||||
|
||||
## More situational awareness
|
||||
|
||||
DXCluster information can support contest operators by adding another source of activity information.
|
||||
|
||||
## Integrated workflow
|
||||
|
||||
The goal is not to open another isolated tool, but to connect activity information with the wider KST4Contest workflow.
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"layout": "feature.njk",
|
||||
"tags": "features"
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
---
|
||||
layout: base.njk
|
||||
title: KST4Contest Features
|
||||
description: Contest-optimized ON4KST features for VHF, UHF and SHF operation.
|
||||
---
|
||||
|
||||
<section class="hero">
|
||||
<p class="badge">Features</p>
|
||||
<h1>Contest tools, not just chat windows.</h1>
|
||||
<p class="lead">
|
||||
KST4Contest combines ON4KST chat, candidate scoring, AirScout workflow,
|
||||
sked handling, log synchronization and operator decision support.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section class="section">
|
||||
<div class="grid">
|
||||
{% for feature in collections.features %}
|
||||
<article class="card feature-card">
|
||||
<div class="feature-icon">{{ feature.data.icon }}</div>
|
||||
<p class="eyebrow">{{ feature.data.category }}</p>
|
||||
<h3><a href="{{ feature.url }}">{{ feature.data.title }}</a></h3>
|
||||
<p>{{ feature.data.summary }}</p>
|
||||
<a href="{{ feature.url }}">Learn more →</a>
|
||||
</article>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</section>
|
||||
@@ -0,0 +1,25 @@
|
||||
---
|
||||
title: Log Synchronization
|
||||
icon: 🔄
|
||||
category: Logger Integration
|
||||
since: "1.31"
|
||||
summary: Import worked stations and current frequencies from supported contest loggers so filters and band information follow the log.
|
||||
description: KST4Contest receives worked-station and, where supported, frequency data from UCXLog, N1MM+, QARTest, DXLog.net and Win-Test through file and UDP interfaces.
|
||||
tagsList:
|
||||
- Win-Test
|
||||
- UCXLog
|
||||
- contest logger
|
||||
related:
|
||||
- priority-score
|
||||
- dual-chat
|
||||
---
|
||||
|
||||
## Connect chat and logging
|
||||
|
||||
Contest operation is faster when chat information and log state are connected.
|
||||
|
||||
KST4Contest can use logger information to improve workflow awareness.
|
||||
|
||||
## Worked and band awareness
|
||||
|
||||
Log synchronization helps the client understand which stations and bands are relevant.
|
||||
@@ -0,0 +1,23 @@
|
||||
---
|
||||
title: Macros and Variables
|
||||
icon: ⚡
|
||||
category: Operator Speed
|
||||
since: "1.0"
|
||||
summary: Build recurring chat messages from configurable text blocks and variables such as the current QRG or locator.
|
||||
description: KST4Contest macros combine reusable message text with values taken from the current station, frequency and operating context.
|
||||
tagsList:
|
||||
- macros
|
||||
- ON4KST
|
||||
- operator workflow
|
||||
related:
|
||||
- sked-reminder
|
||||
- dual-chat
|
||||
---
|
||||
|
||||
## Faster messages
|
||||
|
||||
Macros reduce repetitive typing during active contest operation.
|
||||
|
||||
## More consistent operation
|
||||
|
||||
Variables help keep messages structured and reduce manual errors.
|
||||
@@ -0,0 +1,29 @@
|
||||
---
|
||||
title: Priority Score System
|
||||
icon: 🎯
|
||||
category: Contest Workflow
|
||||
since: "1.40"
|
||||
summary: Rank active stations from known context such as direction, activity, band, QRG, AP and sked information.
|
||||
description: The Priority Score System combines available contest information to populate a separate list of currently relevant candidates.
|
||||
tagsList:
|
||||
- ON4KST
|
||||
- VHF
|
||||
- UHF
|
||||
- SHF
|
||||
- contest
|
||||
- priority candidates
|
||||
related:
|
||||
- timeline
|
||||
- airscout
|
||||
- sked-reminder
|
||||
---
|
||||
|
||||
## Why it matters
|
||||
|
||||
During active VHF, UHF and microwave contests, relevant stations can disappear quickly in busy ON4KST chat traffic.
|
||||
|
||||
The Priority Score System helps operators focus on stations that are more likely to be useful during contest operation.
|
||||
|
||||
## Built for contest decisions
|
||||
|
||||
KST4Contest is not just a chat viewer. It evaluates contest-relevant context and helps turn chat activity into better operating decisions.
|
||||
@@ -0,0 +1,26 @@
|
||||
---
|
||||
title: Sked Reminder
|
||||
icon: 🔔
|
||||
category: Sked Management
|
||||
since: "1.40"
|
||||
summary: Store planned contacts and issue configurable chat messages plus local alerts before the agreed time.
|
||||
description: Sked Reminder keeps scheduled contacts visible and provides automatic advance messages together with acoustic and visual operator alerts.
|
||||
tagsList:
|
||||
- sked
|
||||
- ON4KST
|
||||
- contest reminder
|
||||
related:
|
||||
- priority-score
|
||||
- airscout
|
||||
- timeline
|
||||
---
|
||||
|
||||
## Never lose important skeds
|
||||
|
||||
During active contests, it is easy to miss a planned contact while handling chat traffic, logging and band changes.
|
||||
|
||||
Sked Reminder keeps scheduled contacts visible.
|
||||
|
||||
## Built for real contest pressure
|
||||
|
||||
The workflow supports time-critical operation where missing a few minutes can mean missing a QSO.
|
||||
@@ -0,0 +1,25 @@
|
||||
---
|
||||
title: Timeline View
|
||||
icon: ⏱️
|
||||
category: Contest Awareness
|
||||
since: "1.40"
|
||||
summary: Show AP-based priority candidates and scheduled contacts on a shared timeline.
|
||||
description: The Timeline View places upcoming aircraft scatter candidates and planned skeds into a common time-based overview.
|
||||
tagsList:
|
||||
- timeline
|
||||
- AP windows
|
||||
- airplane scatter
|
||||
related:
|
||||
- airscout
|
||||
- priority-score
|
||||
---
|
||||
|
||||
## Timing matters
|
||||
|
||||
Many VHF/UHF/SHF opportunities are short-lived.
|
||||
|
||||
The Timeline View helps operators see upcoming timing windows instead of keeping everything in mind manually.
|
||||
|
||||
## Designed for fast awareness
|
||||
|
||||
The timeline supports quick decisions during busy contest operation.
|
||||
@@ -0,0 +1,23 @@
|
||||
---
|
||||
permalink: /feed.xml
|
||||
eleventyExcludeFromCollections: true
|
||||
---
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<rss version="2.0">
|
||||
<channel>
|
||||
<title>KST4Contest News</title>
|
||||
<link>https://kst4contest.hamradioonline.de/</link>
|
||||
<description>News and release updates for KST4Contest.</description>
|
||||
<language>en</language>
|
||||
|
||||
{% for post in collections.news | reverse %}
|
||||
<item>
|
||||
<title>{{ post.data.title }}</title>
|
||||
<link>https://kst4contest.hamradioonline.de{{ post.url }}</link>
|
||||
<guid>https://kst4contest.hamradioonline.de{{ post.url }}</guid>
|
||||
<pubDate>{{ post.date | dateToRfc822 }}</pubDate>
|
||||
<description>{{ post.data.summary }}</description>
|
||||
</item>
|
||||
{% endfor %}
|
||||
</channel>
|
||||
</rss>
|
||||
@@ -0,0 +1,168 @@
|
||||
---
|
||||
layout: base.njk
|
||||
title: KST4Contest – ON4KST Contest Client for VHF, UHF and SHF
|
||||
description: KST4Contest combines ON4KST chat, candidate prioritisation, sked planning, AirScout data and logger integration in one desktop client.
|
||||
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 class="hero-copy">
|
||||
<p class="badge">ON4KST contest workflow for 144 MHz and above</p>
|
||||
<h1>KST4Contest</h1>
|
||||
|
||||
<p class="lead">
|
||||
ON4KST shows you the traffic. KST4Contest helps turn it into an operating
|
||||
decision by combining chat, candidate prioritisation, sked planning,
|
||||
AirScout data and logger integration in one desktop client.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
The available information is filtered, related to active stations and
|
||||
presented in a contest-oriented workflow. The final decision still
|
||||
belongs to the operator.
|
||||
</p>
|
||||
|
||||
<div class="actions">
|
||||
<a class="button" href="/download/">Download KST4Contest</a>
|
||||
<a class="button secondary" href="/manual/">Read the manual</a>
|
||||
<a class="button ghost" href="https://github.com/praktimarc/kst4contest">View source code</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="hero-panel">
|
||||
<div class="terminal-bar">
|
||||
<span></span><span></span><span></span>
|
||||
<strong>Functions sharing the same station context</strong>
|
||||
</div>
|
||||
|
||||
<div class="mock-grid">
|
||||
{% for feature in collections.sortedFeatures %}
|
||||
{% if loop.index <= 6 %}
|
||||
<div class="mini-card">
|
||||
<strong>{{ feature.data.icon }} {{ feature.data.title }}</strong>
|
||||
<small>{{ feature.data.summary }}</small>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="section">
|
||||
<div class="section-heading">
|
||||
<p class="eyebrow">The operating problem</p>
|
||||
<h2>The useful station is rarely the only station in the chat</h2>
|
||||
<p>
|
||||
During an active contest, messages, sked requests, frequency information
|
||||
and band changes arrive continuously. The task is to identify which part
|
||||
of that traffic matters now, which candidate should be monitored and
|
||||
which contact is better scheduled for later.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="grid">
|
||||
<article class="card">
|
||||
<h3>Observe</h3>
|
||||
<p>
|
||||
Messages, locators, detected frequencies and known band activity are
|
||||
assigned to stations across as many as two ON4KST chat categories.
|
||||
</p>
|
||||
</article>
|
||||
|
||||
<article class="card">
|
||||
<h3>Evaluate</h3>
|
||||
<p>
|
||||
Direction, distance, worked status, NOT-QRV information, chat activity,
|
||||
sked context and aircraft scatter data feed filters and candidate scores.
|
||||
</p>
|
||||
</article>
|
||||
|
||||
<article class="card">
|
||||
<h3>Act</h3>
|
||||
<p>
|
||||
Candidates can be contacted, scheduled or monitored. Skeds remain
|
||||
visible, while supported loggers and station interfaces receive the
|
||||
information required for the next operating step.
|
||||
</p>
|
||||
</article>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="section">
|
||||
<div class="section-heading">
|
||||
<p class="eyebrow">Functions</p>
|
||||
<h2>One workflow, shared context</h2>
|
||||
<p>
|
||||
The individual functions are not isolated tools. They use the same
|
||||
station, message, band and timing information, so that a change in one
|
||||
part of the workflow can also affect filters, priorities and reminders.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="grid">
|
||||
{% for feature in collections.sortedFeatures %}
|
||||
<article class="card feature-card">
|
||||
<div class="feature-icon">{{ feature.data.icon }}</div>
|
||||
<p class="eyebrow">{{ feature.data.category }}</p>
|
||||
<h3><a href="{{ feature.url }}">{{ feature.data.title }}</a></h3>
|
||||
<p>{{ feature.data.summary }}</p>
|
||||
<a href="{{ feature.url }}">Read how it works →</a>
|
||||
</article>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="section">
|
||||
<div class="cta-panel">
|
||||
<p class="eyebrow">Limits</p>
|
||||
<h2>A priority score is not a propagation forecast</h2>
|
||||
<p>
|
||||
KST4Contest can only evaluate the information it knows. Scores, AP
|
||||
windows, filters and path profiles support the operator's decision; they
|
||||
do not guarantee a contact. If the input data is incomplete or outdated,
|
||||
the result can be incomplete or outdated as well.
|
||||
</p>
|
||||
|
||||
<div class="actions">
|
||||
<a class="button" href="/features/">Explore the functions</a>
|
||||
<a class="button secondary" href="/manual/">Read the technical details</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="section">
|
||||
<div class="cta-panel">
|
||||
<p class="eyebrow">Project status</p>
|
||||
<h2>Stable for operation, Nightly for testing</h2>
|
||||
<p>
|
||||
KST4Contest is open-source software. The current stable release is the
|
||||
normal choice for contest operation. Nightly builds follow ongoing
|
||||
development and are useful when a particular fix or feature needs
|
||||
testing. A few minutes before a contest is not the ideal time to discover
|
||||
what changed.
|
||||
</p>
|
||||
|
||||
<div class="actions">
|
||||
<a class="button" href="/download/">Download the stable release</a>
|
||||
<a class="button secondary" href="/roadmap/">Development status</a>
|
||||
<a class="button ghost" href="/support/">Support development</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<script type="application/ld+json">
|
||||
{
|
||||
"@context": "https://schema.org",
|
||||
"@type": "SoftwareApplication",
|
||||
"name": "KST4Contest",
|
||||
"applicationCategory": "CommunicationApplication",
|
||||
"operatingSystem": "Windows, Linux, macOS",
|
||||
"url": "https://kst4contest.hamradioonline.de/",
|
||||
"downloadUrl": "https://github.com/praktimarc/kst4contest/releases/latest",
|
||||
"description": "KST4Contest combines ON4KST chat, candidate prioritisation, sked planning, AirScout data and logger integration for VHF, UHF and SHF contest operation.",
|
||||
"license": "https://github.com/praktimarc/kst4contest/blob/main/LICENSE"
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,43 @@
|
||||
---
|
||||
layout: base.njk
|
||||
title: Legal Notice | KST4Contest
|
||||
description: Legal notice for the KST4Contest website.
|
||||
---
|
||||
|
||||
<section class="hero">
|
||||
<p class="badge">Legal Notice</p>
|
||||
<h1>Legal Notice</h1>
|
||||
<p class="lead">Information according to § 5 DDG.</p>
|
||||
</section>
|
||||
|
||||
<section class="section narrow">
|
||||
<article class="card content-card">
|
||||
<h2>Provider</h2>
|
||||
<p>
|
||||
Marc Fröhlich<br>
|
||||
Rheingoldstr. 6<br>
|
||||
68199 Mannheim<br>
|
||||
Germany
|
||||
</p>
|
||||
|
||||
<h2>Contact</h2>
|
||||
<p>
|
||||
Email: <a href="mailto:praktimarc+kst4contest@gmail.com">praktimarc+kst4contest@gmail.com</a>
|
||||
</p>
|
||||
|
||||
<h2>Responsible for content</h2>
|
||||
<p>
|
||||
Marc Fröhlich<br>
|
||||
Rheingoldstr. 6<br>
|
||||
68199 Mannheim<br>
|
||||
Germany
|
||||
</p>
|
||||
|
||||
<h2>Project</h2>
|
||||
<p>
|
||||
KST4Contest is an open-source ON4KST contest client.
|
||||
Source code and releases are available on
|
||||
<a href="https://github.com/praktimarc/kst4contest">GitHub</a>.
|
||||
</p>
|
||||
</article>
|
||||
</section>
|
||||
@@ -0,0 +1,18 @@
|
||||
---
|
||||
pagination:
|
||||
data: collections.manualPages
|
||||
size: 1
|
||||
alias: manual
|
||||
permalink: "/manual/{{ manual.lang }}/{{ manual.slug }}/"
|
||||
layout: base.njk
|
||||
title: "{{ manual.title }} | KST4Contest Manual"
|
||||
description: "KST4Contest manual page: {{ manual.title }}"
|
||||
---
|
||||
|
||||
<section class="section">
|
||||
<div class="card manual-content">
|
||||
<p><a href="/manual/{{ manual.lang }}/">← Back to manual overview</a></p>
|
||||
<h1>{{ manual.title }}</h1>
|
||||
{{ manual.content | markdown | safe }}
|
||||
</div>
|
||||
</section>
|
||||
@@ -0,0 +1,25 @@
|
||||
---
|
||||
layout: base.njk
|
||||
title: KST4Contest Deutsches Handbuch
|
||||
description: Deutsches Benutzerhandbuch für KST4Contest.
|
||||
---
|
||||
|
||||
<section class="hero">
|
||||
<h1>Deutsches Handbuch</h1>
|
||||
<p>Das Handbuch wird zentral in GitHub gepflegt und als Wiki, PDF und Online-Dokumentation veröffentlicht.</p>
|
||||
</section>
|
||||
|
||||
<section class="section">
|
||||
<div class="grid">
|
||||
{% for manual in collections.manualPages %}
|
||||
{% if manual.lang == "de" %}
|
||||
<div class="card">
|
||||
<h3><a href="/manual/de/{{ manual.slug }}/">{{ manual.title }}</a></h3>
|
||||
{% if manual.summary %}
|
||||
<p>{{ manual.summary }}</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
</section>
|
||||
@@ -0,0 +1,25 @@
|
||||
---
|
||||
layout: base.njk
|
||||
title: KST4Contest English Manual
|
||||
description: English user manual for KST4Contest.
|
||||
---
|
||||
|
||||
<section class="hero">
|
||||
<h1>English Manual</h1>
|
||||
<p>The manual is maintained centrally in GitHub and published as Wiki, PDF and online documentation.</p>
|
||||
</section>
|
||||
|
||||
<section class="section">
|
||||
<div class="grid">
|
||||
{% for manual in collections.manualPages %}
|
||||
{% if manual.lang == "en" %}
|
||||
<div class="card">
|
||||
<h3><a href="/manual/en/{{ manual.slug }}/">{{ manual.title }}</a></h3>
|
||||
{% if manual.summary %}
|
||||
<p>{{ manual.summary }}</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
</section>
|
||||
@@ -0,0 +1,18 @@
|
||||
---
|
||||
layout: base.njk
|
||||
title: KST4Contest Manual
|
||||
description: Online manual for KST4Contest, the contest-optimized ON4KST chat client.
|
||||
---
|
||||
|
||||
<section class="hero">
|
||||
<h1>KST4Contest Manual</h1>
|
||||
<p>
|
||||
User documentation for installation, configuration, features,
|
||||
AirScout integration, log synchronization and contest workflow.
|
||||
</p>
|
||||
|
||||
<div class="actions">
|
||||
<a class="button" href="/manual/en/">English Manual</a>
|
||||
<a class="button secondary" href="/manual/de/">Deutsches Handbuch</a>
|
||||
</div>
|
||||
</section>
|
||||
@@ -0,0 +1,9 @@
|
||||
---
|
||||
title: Hotfix Version 1.41.1
|
||||
summary: Hotfix of a bug in Version 1.41
|
||||
date: 2026-07-08
|
||||
---
|
||||
|
||||
## Version 1.41.1
|
||||
We had a bug, where KST4Contest would clear out the text field, where you would send your messages after a period. It also happend to set your Cursor back to the Sending Field, when you were filtering for a station. We fixed this bug today. The new release is [here](https://github.com/praktimarc/kst4contest/releases/tag/v1.41.1). You can also download the artifacts as always via the [Downloads-Page](/download/).
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
---
|
||||
title: AUR Packages now live of kst4contest
|
||||
summary: We now uploaded the packages of kst4contest to the AUR
|
||||
date: 2026-07-15
|
||||
---
|
||||
## AUR Packages now live of KST4Contest
|
||||
|
||||
We now have uploaded kst4contest as multiple Arch User Repository Packages. This allows yet another integration into Package Managers for easier updating toall Archlinux Users of our software. This one of many more repositories for our linux users to come. It is an continous effort. You can see the progress of making updating KST4Contest for our users easier [here](https://github.com/praktimarc/kst4contest/issues/61).
|
||||
|
||||
### AUR DISCLAIMER
|
||||
|
||||
Before you use the AUR (if you did not use the AUR before) please read [this entry in the Arch wiki and the disclaimer](https://wiki.archlinux.org/title/Arch_User_Repository) about security in the AUR.
|
||||
|
||||
### Packages provided to the AUR:
|
||||
- [kst4contest](https://aur.archlinux.org/packages/kst4contest): This is the latest Release Build. If you install this package, you will compile the source code on your own machine. We recommend an JDK21 Enviroment for doing this
|
||||
- [kst4contest-bin](https://aur.archlinux.org/packages/kst4contest-bin): This is the precompiled Binary, which is the same as the binary on the Github release page.
|
||||
- [kstcontest-git](https://aur.archlinux.org/packages/kst4contest-git): This is the latest devolopment state of kst4contest. This is recommend for testers only, as things might break.
|
||||
|
||||
Have fun with these packages. 73
|
||||
@@ -0,0 +1,17 @@
|
||||
---
|
||||
title: KST4Contest Website Launch
|
||||
summary: The new KST4Contest website is being built as a static, SEO-friendly product portal with GitHub-based documentation.
|
||||
date: 2026-07-07
|
||||
---
|
||||
|
||||
## A new home for KST4Contest
|
||||
|
||||
KST4Contest now has its own website at `kst4contest.hamradioonline.de`.
|
||||
|
||||
The site is built with Eleventy, generated from version-controlled Markdown and deployed as a fast static website.
|
||||
|
||||
## Why this matters
|
||||
|
||||
The goal is to make KST4Contest easier to discover, easier to document and easier to maintain.
|
||||
|
||||
Documentation, release information, feature pages and future updates can now grow from a shared content model.
|
||||
@@ -0,0 +1,27 @@
|
||||
---
|
||||
layout: base.njk
|
||||
title: KST4Contest News
|
||||
description: News, release updates and development notes for KST4Contest.
|
||||
eleventyExcludeFromCollections: true
|
||||
---
|
||||
|
||||
<section class="hero">
|
||||
<p class="badge">News</p>
|
||||
<h1>Development updates and release notes.</h1>
|
||||
<p class="lead">
|
||||
Updates about KST4Contest releases, website improvements and contest workflow development.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section class="section">
|
||||
<div class="grid">
|
||||
{% for post in collections.news | reverse %}
|
||||
<article class="card">
|
||||
<p class="eyebrow">{{ post.date | readableDate }}</p>
|
||||
<h3><a href="{{ post.url }}">{{ post.data.title }}</a></h3>
|
||||
<p>{{ post.data.summary }}</p>
|
||||
<a href="{{ post.url }}">Read more →</a>
|
||||
</article>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</section>
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"layout": "news-post.njk",
|
||||
"tags": "news"
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
---
|
||||
layout: base.njk
|
||||
title: Privacy Policy | KST4Contest
|
||||
description: Privacy policy for the KST4Contest website.
|
||||
---
|
||||
|
||||
<section class="hero">
|
||||
<p class="badge">Privacy</p>
|
||||
<h1>Privacy Policy</h1>
|
||||
<p class="lead">Information about data processing on this static website.</p>
|
||||
</section>
|
||||
|
||||
<section class="section narrow">
|
||||
<article class="card content-card">
|
||||
<h2>Controller</h2>
|
||||
<p>
|
||||
Marc Fröhlich<br>
|
||||
Rheingoldstr. 6<br>
|
||||
68199 Mannheim<br>
|
||||
Germany
|
||||
</p>
|
||||
|
||||
<h2>Access logs</h2>
|
||||
<p>
|
||||
When this website is accessed, the web server may process technical access data such as
|
||||
IP address, requested URL, date and time, user agent and referrer.
|
||||
This processing is used to provide the website, maintain security and investigate technical issues.
|
||||
</p>
|
||||
|
||||
<h2>Legal basis</h2>
|
||||
<p>
|
||||
The legal basis is Art. 6(1)(f) GDPR: legitimate interest in secure and reliable operation
|
||||
of the website.
|
||||
</p>
|
||||
|
||||
<h2>Cookies and tracking</h2>
|
||||
<p>
|
||||
This website currently does not use cookies, analytics tracking, advertising pixels or user profiling.
|
||||
</p>
|
||||
|
||||
<h2>External links</h2>
|
||||
<p>
|
||||
This website links to GitHub for source code, releases and downloads. When you follow these links,
|
||||
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,
|
||||
data portability and objection under the GDPR.
|
||||
</p>
|
||||
|
||||
<h2>Supervisory authority</h2>
|
||||
<p>
|
||||
You have the right to lodge a complaint with a competent data protection supervisory authority.
|
||||
</p>
|
||||
</article>
|
||||
</section>
|
||||
@@ -0,0 +1,93 @@
|
||||
---
|
||||
layout: base.njk
|
||||
title: KST4Contest Roadmap
|
||||
description: Planned enhancements for upcoming KST4Contest versions, generated from GitHub milestones and issues.
|
||||
---
|
||||
|
||||
<section class="hero">
|
||||
<p class="badge">Roadmap</p>
|
||||
<h1>What's planned next.</h1>
|
||||
<p class="lead">
|
||||
Generated automatically from the "enhancement" issues in our
|
||||
<a href="https://github.com/praktimarc/kst4contest">GitHub repository</a>,
|
||||
grouped by their target version milestone.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section class="section narrow">
|
||||
{% if roadmap.length == 0 %}
|
||||
<article class="card content-card roadmap-version">
|
||||
<p>No planned enhancements are currently assigned to a version milestone.</p>
|
||||
<p>
|
||||
See all open feature requests on
|
||||
<a href="https://github.com/praktimarc/kst4contest/issues?q=is%3Aissue+is%3Aopen+label%3Aenhancement">GitHub</a>.
|
||||
</p>
|
||||
</article>
|
||||
{% endif %}
|
||||
|
||||
{% for version in roadmap %}
|
||||
<article class="card content-card roadmap-version">
|
||||
{% if version.version %}
|
||||
<h2><a href="{{ version.url }}">{{ version.version }}</a></h2>
|
||||
{% else %}
|
||||
<h2>Planned, no version yet</h2>
|
||||
{% endif %}
|
||||
{% if version.description %}<p>{{ version.description }}</p>{% endif %}
|
||||
{% if version.released %}
|
||||
<p class="eyebrow">Released{% if version.releasedOn %} {{ version.releasedOn | readableDate }}{% endif %}</p>
|
||||
{% elif version.dueOn %}
|
||||
<p class="eyebrow">Due {{ version.dueOn | readableDate }}</p>
|
||||
{% endif %}
|
||||
<ul>
|
||||
{% for item in version.enhancements %}
|
||||
<li{% if item.done or item.notPlanned %} class="roadmap-done"{% endif %}>
|
||||
<a href="{{ item.url }}">{{ item.title }}</a>{% if item.done %} ✓{% endif %}{% if item.notPlanned %} (not planned){% endif %}
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</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,4 @@
|
||||
User-agent: *
|
||||
Allow: /
|
||||
|
||||
Sitemap: https://kst4contest.hamradioonline.de/sitemap.xml
|
||||
@@ -0,0 +1,28 @@
|
||||
---
|
||||
layout: base.njk
|
||||
title: KST4Contest Screenshots
|
||||
description: Screenshots of KST4Contest, the contest-optimized ON4KST chat client for VHF, UHF and SHF operators.
|
||||
---
|
||||
|
||||
<section class="hero">
|
||||
<p class="badge">Screenshots</p>
|
||||
<h1>See the contest workflow.</h1>
|
||||
<p class="lead">
|
||||
Visual impressions of KST4Contest: ON4KST chat workflow, priority candidates,
|
||||
sked handling, AirScout integration and contest-focused operator tools.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section class="section">
|
||||
<div class="grid">
|
||||
{% for shot in screenshots %}
|
||||
<article class="card screenshot-card">
|
||||
<div class="screenshot-placeholder">
|
||||
<span>{{ shot.title }}</span>
|
||||
</div>
|
||||
<h3>{{ shot.title }}</h3>
|
||||
<p>{{ shot.caption }}</p>
|
||||
</article>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</section>
|
||||
@@ -0,0 +1,15 @@
|
||||
---
|
||||
permalink: /sitemap.xml
|
||||
eleventyExcludeFromCollections: true
|
||||
---
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
||||
{%- for page in collections.all %}
|
||||
{%- if page.url and not page.data.eleventyExcludeFromCollections %}
|
||||
<url>
|
||||
<loc>https://kst4contest.hamradioonline.de{{ page.url }}</loc>
|
||||
<lastmod>{{ page.date | dateToIso }}</lastmod>
|
||||
</url>
|
||||
{%- endif %}
|
||||
{%- endfor %}
|
||||
</urlset>
|
||||
@@ -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>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user