From 6afcee8df59115f4fa995401beea0607f5f53498 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc=20Fr=C3=B6hlich?= Date: Tue, 30 Jun 2026 14:30:31 +0200 Subject: [PATCH] Fix #39 and JDK 21 Upgrade an Grid Square Coloring * Fixed Map: leaflet rendering in Java 21 Webview (disabling 3D in leaflet) * Added grid square coloring function and fixed bug of message categorizing after entering the chat and not selected a chatmember --------- Authored-by: Marc Froehlich --- .github/workflows/nightly-artifacts.yml | 188 ++------- .github/workflows/tagged-release.yml | 153 ++----- .../controller/ChatController.java | 37 +- .../kst4contest/controller/DBController.java | 44 +- .../controller/WorkedGrossFieldCache.java | 25 ++ .../view/Kst4ContestApplication.java | 385 ++++++++++++++++-- .../view/map/MapHtmlResources.java | 82 +++- .../kst4contest/view/map/StationMapView.java | 126 ++++-- .../kst4contest/view/map/TileProxyServer.java | 9 +- src/main/java/module-info.java | 2 +- 10 files changed, 691 insertions(+), 360 deletions(-) diff --git a/.github/workflows/nightly-artifacts.yml b/.github/workflows/nightly-artifacts.yml index 51b7b2e..a3a24db 100644 --- a/.github/workflows/nightly-artifacts.yml +++ b/.github/workflows/nightly-artifacts.yml @@ -12,7 +12,7 @@ env: FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true permissions: - contents: write + contents: read packages: write jobs: @@ -34,11 +34,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 21 + - name: Set up Java 17 uses: actions/setup-java@v4.1.0 with: distribution: temurin - java-version: "21" + java-version: "17" - name: Install WiX Toolset shell: pwsh @@ -89,11 +89,11 @@ jobs: SHORT_SHA="${GITHUB_SHA::7}" echo "ASSET_BASENAME=KST4Contest-${VERSION}-${SHORT_SHA}" >> "$GITHUB_ENV" - - name: Set up Java 21 + - name: Set up Java 17 uses: actions/setup-java@v4.1.0 with: distribution: temurin - java-version: "21" + java-version: "17" - name: Ensure mvnw is executable run: chmod +x mvnw @@ -169,11 +169,11 @@ jobs: SHORT_SHA="${GITHUB_SHA::7}" echo "ASSET_BASENAME=KST4Contest-${VERSION}-${SHORT_SHA}" >> "$GITHUB_ENV" - - name: Set up Java 21 + - name: Set up Java 17 uses: actions/setup-java@v4.1.0 with: distribution: temurin - java-version: "21" + java-version: "17" - name: Install packaging dependencies run: | @@ -199,7 +199,6 @@ 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 @@ -228,11 +227,11 @@ jobs: SHORT_SHA="${GITHUB_SHA::7}" echo "ASSET_BASENAME=KST4Contest-${VERSION}-${SHORT_SHA}" >> "$GITHUB_ENV" - - name: Set up Java 21 + - name: Set up Java 17 uses: actions/setup-java@v4.1.0 with: distribution: temurin - java-version: "21" + java-version: "17" - name: Install packaging dependencies run: | @@ -258,7 +257,6 @@ 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 @@ -289,11 +287,11 @@ jobs: echo "SHORT_SHA=$SHORT_SHA" >> "$GITHUB_ENV" echo "ASSET_BASENAME=KST4Contest-${VERSION}-${SHORT_SHA}" >> "$GITHUB_ENV" - - name: Set up Java 21 + - name: Set up Java 17 uses: actions/setup-java@v4.1.0 with: distribution: temurin - java-version: "21" + java-version: "17" - name: Install packaging dependencies run: | @@ -348,42 +346,21 @@ 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 -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" . + 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" . - name: Upload Arch Linux artifact uses: actions/upload-artifact@v4.3.4 @@ -408,11 +385,11 @@ jobs: echo "SHORT_SHA=$SHORT_SHA" >> "$GITHUB_ENV" echo "ASSET_BASENAME=KST4Contest-${VERSION}-${SHORT_SHA}" >> "$GITHUB_ENV" - - name: Set up Java 21 + - name: Set up Java 17 uses: actions/setup-java@v4.1.0 with: distribution: temurin - java-version: "21" + java-version: "17" - name: Ensure mvnw is executable run: chmod +x mvnw @@ -451,18 +428,14 @@ 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 /app/share/alsa - - printf '@include /usr/share/alsa/alsa.conf\npcm.!default { type pulse }\nctl.!default { type pulse }\n' > /app/share/alsa/asound.conf + - install -d /app/lib/KST4Contest /app/bin /app/share/applications - cp -a . /app/lib/KST4Contest/ - printf '#!/bin/sh\nexec /app/lib/KST4Contest/bin/KST4Contest "$@"\n' > /app/bin/KST4Contest - chmod 755 /app/bin/KST4Contest @@ -480,103 +453,18 @@ jobs: path: flatpak-src/KST4Contest EOF - - name: Import Flatpak signing key - run: | - echo "${{ secrets.FLATPAK_GPG_PRIVATE_KEY }}" | gpg --batch --import - 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: Build Flatpak repo (nightly) + - name: Build Flatpak bundle run: | flatpak-builder --force-clean target/flatpak-build target/de.x08.KST4Contest.yml - flatpak build-export --gpg-sign="$FLATPAK_GPG_KEY_ID" target/flatpak-repo target/flatpak-build nightly - flatpak build-update-repo --gpg-sign="$FLATPAK_GPG_KEY_ID" target/flatpak-repo + flatpak build-export target/flatpak-repo target/flatpak-build + flatpak build-bundle target/flatpak-repo "dist/${ASSET_BASENAME}-linux-x86_64.flatpak" de.x08.KST4Contest - - name: Create flatpakref (nightly) - run: | - 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.nightly.flatpakref" << EOF - [Flatpak Ref] - Name=de.x08.KST4Contest - Branch=nightly - Title=KST4Contest (Nightly) – ON4KST Chat Client - Url=${PAGES_URL} - RuntimeRepo=https://flathub.org/repo/flathub.flatpakrepo - GPGKey=${GPG_KEY_B64} - IsRuntime=false - EOF - - - name: Upload flatpakref + - name: Upload Flatpak artifact uses: actions/upload-artifact@v4.3.4 with: - name: flatpakref - path: dist/de.x08.KST4Contest.nightly.flatpakref - - - name: Upload Flatpak OSTree repo - uses: actions/upload-artifact@v4.3.4 - with: - name: flatpak-ostree-repo - path: target/flatpak-repo/ - - publish-flatpak-repo: - name: Publish Flatpak OSTree Repo (nightly) - runs-on: ubuntu-latest - needs: build-flatpak - - steps: - - name: Install Flatpak tooling - run: | - sudo apt-get update -qq - sudo apt-get install -y --no-install-recommends flatpak - - - name: Import Flatpak signing key - run: | - echo "${{ secrets.FLATPAK_GPG_PRIVATE_KEY }}" | gpg --batch --import - echo "FLATPAK_GPG_KEY_ID=$(gpg --list-secret-keys --with-colons | awk -F: '/^fpr/{print $10; exit}')" >> "$GITHUB_ENV" - - - name: Download OSTree repo artifact - uses: actions/download-artifact@v4.1.3 - with: - name: flatpak-ostree-repo - path: flatpak-ostree-repo/ - - - name: Checkout existing flatpak-repo branch - uses: actions/checkout@v4.1.7 - with: - ref: flatpak-repo - path: existing-flatpak-repo - - - name: Merge nightly build into flatpak-repo - run: | - cd existing-flatpak-repo - git config user.email "41898282+github-actions[bot]@users.noreply.github.com" - git config user.name "github-actions[bot]" - # Copy the new OSTree build into the repo - rsync -a ../flatpak-ostree-repo/ ./ - # Regenerate summary with all branches - flatpak build-update-repo --gpg-sign="$FLATPAK_GPG_KEY_ID" . - # Generate .flatpakrepo with embedded GPG key so users can do remote-add without errors - 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 > kst4contest.flatpakrepo << EOF - [Flatpak Repo] - Title=KST4Contest - Url=${PAGES_URL} - Homepage=https://github.com/${GITHUB_REPOSITORY} - Comment=KST4Contest – ON4KST Chat Client for VHF/UHF contests - GPGKey=${GPG_KEY_B64} - EOF - # Stage all changes (new/updated refs, summary, objects in OSTree) - git add -A - if git diff --cached --quiet; then - echo "No changes to commit" - else - git commit -m "Nightly flatpak: $(echo ${{ github.sha }} | cut -c1-7)" - git push https://x-access-token:${{ secrets.GITHUB_TOKEN }}@github.com/${{ github.repository }}.git HEAD:flatpak-repo - fi + name: linux-flatpak + path: dist/KST4Contest-*-linux-x86_64.flatpak + retention-days: 14 build-macos-dmg: name: Build macOS DMG (${{ matrix.os }}) @@ -599,11 +487,11 @@ jobs: echo "ASSET_BASENAME=KST4Contest-${VERSION}-${SHORT_SHA}" >> "$GITHUB_ENV" echo "ARCH=$ARCH" >> "$GITHUB_ENV" - - name: Set up Java 21 + - name: Set up Java 17 uses: actions/setup-java@v4.1.0 with: distribution: temurin - java-version: "21" + java-version: "17" - name: Ensure mvnw is executable run: chmod +x mvnw diff --git a/.github/workflows/tagged-release.yml b/.github/workflows/tagged-release.yml index 929d4d2..7bf4859 100644 --- a/.github/workflows/tagged-release.yml +++ b/.github/workflows/tagged-release.yml @@ -22,11 +22,11 @@ jobs: - name: Checkout uses: actions/checkout@v4.1.7 - - name: Set up Java 21 + - name: Set up Java 17 uses: actions/setup-java@v4.1.0 with: distribution: temurin - java-version: "21" + java-version: "17" - name: Install WiX Toolset shell: pwsh @@ -70,11 +70,11 @@ jobs: - name: Checkout uses: actions/checkout@v4.1.7 - - name: Set up Java 21 + - name: Set up Java 17 uses: actions/setup-java@v4.1.0 with: distribution: temurin - java-version: "21" + java-version: "17" - 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 21 + - name: Set up Java 17 uses: actions/setup-java@v4.1.0 with: distribution: temurin - java-version: "21" + java-version: "17" - name: Install packaging dependencies run: | @@ -173,7 +173,6 @@ 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 @@ -195,11 +194,11 @@ jobs: - name: Checkout uses: actions/checkout@v4.1.7 - - name: Set up Java 21 + - name: Set up Java 17 uses: actions/setup-java@v4.1.0 with: distribution: temurin - java-version: "21" + java-version: "17" - name: Install packaging dependencies run: | @@ -225,7 +224,6 @@ 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 @@ -247,11 +245,11 @@ jobs: - name: Checkout uses: actions/checkout@v4.1.7 - - name: Set up Java 21 + - name: Set up Java 17 uses: actions/setup-java@v4.1.0 with: distribution: temurin - java-version: "21" + java-version: "17" - name: Install packaging dependencies run: | @@ -306,42 +304,21 @@ 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 -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" . + 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" . - name: Upload Arch Linux artifact uses: actions/upload-artifact@v4.3.4 @@ -357,11 +334,11 @@ jobs: - name: Checkout uses: actions/checkout@v4.1.7 - - name: Set up Java 21 + - name: Set up Java 17 uses: actions/setup-java@v4.1.0 with: distribution: temurin - java-version: "21" + java-version: "17" - name: Ensure mvnw is executable run: chmod +x mvnw @@ -400,18 +377,14 @@ 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 /app/share/alsa - - printf '@include /usr/share/alsa/alsa.conf\npcm.!default { type pulse }\nctl.!default { type pulse }\n' > /app/share/alsa/asound.conf + - install -d /app/lib/KST4Contest /app/bin /app/share/applications - cp -a . /app/lib/KST4Contest/ - printf '#!/bin/sh\nexec /app/lib/KST4Contest/bin/KST4Contest "$@"\n' > /app/bin/KST4Contest - chmod 755 /app/bin/KST4Contest @@ -435,18 +408,10 @@ 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 - run: | - if [[ "${{ github.ref_name }}" == beta-* ]]; then - echo "OSTREE_BRANCH=beta" >> "$GITHUB_ENV" - else - echo "OSTREE_BRANCH=stable" >> "$GITHUB_ENV" - fi - - name: Build Flatpak repo run: | flatpak-builder --force-clean target/flatpak-build target/de.x08.KST4Contest.yml - flatpak build-export --gpg-sign="$FLATPAK_GPG_KEY_ID" target/flatpak-repo target/flatpak-build "$OSTREE_BRANCH" + flatpak build-export --gpg-sign="$FLATPAK_GPG_KEY_ID" target/flatpak-repo target/flatpak-build stable flatpak build-update-repo --gpg-sign="$FLATPAK_GPG_KEY_ID" target/flatpak-repo - name: Create flatpakref @@ -454,11 +419,11 @@ 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/de.x08.KST4Contest.flatpakref" << EOF [Flatpak Ref] Name=de.x08.KST4Contest - Branch=${OSTREE_BRANCH} - Title=KST4Contest (${{ startsWith(github.ref_name, 'beta-') && 'Beta' || 'Release' }}) – ON4KST Chat Client + Branch=stable + Title=KST4Contest – ON4KST Chat Client Url=${PAGES_URL} RuntimeRepo=https://flathub.org/repo/flathub.flatpakrepo GPGKey=${GPG_KEY_B64} @@ -469,7 +434,7 @@ jobs: uses: actions/upload-artifact@v4.3.4 with: name: flatpakref - path: dist/de.x08.KST4Contest.*.flatpakref + path: dist/de.x08.KST4Contest.flatpakref - name: Upload Flatpak OSTree repo uses: actions/upload-artifact@v4.3.4 @@ -488,11 +453,11 @@ jobs: - name: Checkout uses: actions/checkout@v4.1.7 - - name: Set up Java 21 + - name: Set up Java 17 uses: actions/setup-java@v4.1.0 with: distribution: temurin - java-version: "21" + java-version: "17" - name: Ensure mvnw is executable run: chmod +x mvnw @@ -611,60 +576,32 @@ jobs: path: dist/KST4Contest-${{ github.ref_name }}-manual-*.pdf publish-flatpak-repo: - name: Publish Flatpak OSTree Repo (${{ github.ref_name }}) + name: Publish Flatpak OSTree Repo to GitHub Pages runs-on: ubuntu-latest needs: build-flatpak steps: - - name: Install Flatpak tooling - run: | - sudo apt-get update -qq - sudo apt-get install -y --no-install-recommends flatpak - - - name: Import Flatpak signing key - run: | - echo "${{ secrets.FLATPAK_GPG_PRIVATE_KEY }}" | gpg --batch --import - echo "FLATPAK_GPG_KEY_ID=$(gpg --list-secret-keys --with-colons | awk -F: '/^fpr/{print $10; exit}')" >> "$GITHUB_ENV" - - name: Download OSTree repo artifact uses: actions/download-artifact@v4.1.3 with: name: flatpak-ostree-repo path: flatpak-ostree-repo/ - - name: Checkout existing flatpak-repo branch - uses: actions/checkout@v4.1.7 + - name: Download flatpakref + uses: actions/download-artifact@v4.1.3 with: - ref: flatpak-repo - path: existing-flatpak-repo + name: flatpakref + path: flatpak-ostree-repo/ - - name: Merge build into flatpak-repo + - name: Push to flatpak-repo branch run: | - cd existing-flatpak-repo + cd flatpak-ostree-repo + git init git config user.email "41898282+github-actions[bot]@users.noreply.github.com" git config user.name "github-actions[bot]" - rsync -a ../flatpak-ostree-repo/ ./ - # Regenerate summary with all branches (not just the one just built) - flatpak build-update-repo --gpg-sign="$FLATPAK_GPG_KEY_ID" . - # Generate .flatpakrepo with embedded GPG key so users can do remote-add without errors - 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 > kst4contest.flatpakrepo << EOF - [Flatpak Repo] - Title=KST4Contest - Url=${PAGES_URL} - Homepage=https://github.com/${GITHUB_REPOSITORY} - Comment=KST4Contest – ON4KST Chat Client for VHF/UHF contests - GPGKey=${GPG_KEY_B64} - EOF git add -A - if git diff --cached --quiet; then - echo "No changes to commit" - else - git commit -m "Flatpak repo: ${{ github.ref_name }}" - git push https://x-access-token:${{ secrets.GITHUB_TOKEN }}@github.com/${{ github.repository }}.git HEAD:flatpak-repo - fi + git commit -m "Flatpak repo: ${{ github.ref_name }}" + git push --force https://x-access-token:${{ secrets.GITHUB_TOKEN }}@github.com/${{ github.repository }}.git HEAD:flatpak-repo release-tag: name: Publish Tagged Release diff --git a/src/main/java/kst4contest/controller/ChatController.java b/src/main/java/kst4contest/controller/ChatController.java index 20de7ba..f7053a0 100644 --- a/src/main/java/kst4contest/controller/ChatController.java +++ b/src/main/java/kst4contest/controller/ChatController.java @@ -2179,32 +2179,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. + * + *

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.

* * @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 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. + * + *

This is the predicate behind the optional "Only new grids" filter. It no + * longer depends on active station band settings.

+ * + * @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() { diff --git a/src/main/java/kst4contest/controller/DBController.java b/src/main/java/kst4contest/controller/DBController.java index 3ec64e4..583d588 100644 --- a/src/main/java/kst4contest/controller/DBController.java +++ b/src/main/java/kst4contest/controller/DBController.java @@ -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. + * + *

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.

+ * + * @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 diff --git a/src/main/java/kst4contest/controller/WorkedGrossFieldCache.java b/src/main/java/kst4contest/controller/WorkedGrossFieldCache.java index 2bfa2ba..fd7feeb 100644 --- a/src/main/java/kst4contest/controller/WorkedGrossFieldCache.java +++ b/src/main/java/kst4contest/controller/WorkedGrossFieldCache.java @@ -119,6 +119,31 @@ public final class WorkedGrossFieldCache { .contains(grossField); } + /** + * Checks whether a locator gross field has already been worked on any band. + * + *

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.

+ * + * @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 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". diff --git a/src/main/java/kst4contest/view/Kst4ContestApplication.java b/src/main/java/kst4contest/view/Kst4ContestApplication.java index 85fb58b..02755bc 100644 --- a/src/main/java/kst4contest/view/Kst4ContestApplication.java +++ b/src/main/java/kst4contest/view/Kst4ContestApplication.java @@ -67,12 +67,17 @@ import kst4contest.utils.ApplicationFileUtils; import kst4contest.view.map.StationMapBridge; import kst4contest.view.map.StationMapView; import kst4contest.view.map.OfflineDemImportService; +import kst4contest.controller.WorkedGrossFieldCache; public class Kst4ContestApplication extends Application implements StatusUpdateListener { // private static final Kst4ContestApplication dbcontroller = new DBController(); // Null means Auto: use the lowest session band, or category fallback. + // Enables optional color highlighting of the QRA/grid cell. +// The status text itself remains visible independently of this flag. + private boolean gridSquareHighlightEnabled = false; + private PauseTransition userListRefreshCoalescer; private String pendingUserListUpdateReason = ""; @@ -1260,6 +1265,75 @@ public class Kst4ContestApplication extends Application implements StatusUpdateL } }); + /** + * Optionally highlights already worked grid squares in the QRA column. + * + *

The default table design is preserved as much as possible: + *

    + *
  • If grid coloring is disabled, the cell uses the standard JavaFX style.
  • + *
  • If the grid is not worked yet, the cell also uses the standard style.
  • + *
  • Only already worked grids get a subtle background color.
  • + *
  • Selected rows keep the normal table selection style.
  • + *
+ * + *

No font weight is changed here. The compact worked/grid status such as + * {@code xo} is emphasized only in the worked-any column.

+ */ + qraCol.setCellFactory(column -> new TableCell() { + @Override + protected void updateItem(String item, boolean empty) { + super.updateItem(item, empty); + + if (empty || item == null) { + setText(null); + setTooltip(null); + setStyle(""); + return; + } + + ChatMember member = getTableRow() == null ? null : getTableRow().getItem(); + boolean gridWorked = member != null + && chatcontroller != null + && chatcontroller.isGridSquareWorkedAny(member); + + String grossField = WorkedGrossFieldCache.extractGrossField(item); + + setText(item); + setTooltip(new Tooltip( + "Grid status: " + + (grossField == null ? "unknown" : grossField) + + "\nGrid worked any: " + + (gridWorked ? "yes" : "no") + )); + + /* + * Important: + * Do not style the cell unless the Grid color button is active AND the + * grid has already been worked. This keeps normal/new grids visually + * identical to the standard table design. + */ + if (!gridSquareHighlightEnabled || !gridWorked) { + setStyle(""); + return; + } + + /* + * Preserve normal selection appearance. A selected row should look like a + * selected row, not like a custom-colored QRA cell. + */ + if (isSelected() || (getTableRow() != null && getTableRow().isSelected())) { + setStyle(""); + return; + } + + /* + * Already worked grid: + * Use a subtle background derived from the current theme. No bold text. + */ + setStyle("-fx-background-color: derive(-fx-control-inner-background, -18%);"); + } + }); + TableColumn qrBCol = new TableColumn("QRB"); qrBCol.setCellValueFactory(new Callback, ObservableValue>() { @@ -1484,19 +1558,60 @@ public class Kst4ContestApplication extends Application implements StatusUpdateL @Override public ObservableValue call(CellDataFeatures cellDataFeatures) { - SimpleStringProperty wkd = new SimpleStringProperty(); - - if (cellDataFeatures.getValue().isWorked()) { - wkd.setValue("X"); - } else { - wkd.setValue(""); - } - - return wkd; + return new SimpleStringProperty(formatWorkedAnyGridStatus(cellDataFeatures.getValue())); } }); wkdAny_subcol.prefWidthProperty().bind(tbl_chatMemberTable.widthProperty().divide(14)); + /** + * Shows the compact worked/grid status and explains it by tooltip. + */ + wkdAny_subcol.setCellFactory(column -> new TableCell() { + @Override + protected void updateItem(String item, boolean empty) { + super.updateItem(item, empty); + + if (empty) { + setText(null); + setTooltip(null); + setStyle(""); + return; + } + + ChatMember member = getTableRow() == null ? null : getTableRow().getItem(); + + setText(item); + setTooltip(new Tooltip(buildWorkedAnyGridStatusTooltip(member))); + setAlignment(Pos.CENTER); + setStyle("-fx-font-weight: bold;"); + } + }); + + /** + * Shows the compact worked/grid status and explains it by tooltip. + */ + wkdAny_subcol.setCellFactory(column -> new TableCell() { + @Override + protected void updateItem(String item, boolean empty) { + super.updateItem(item, empty); + + if (empty) { + setText(null); + setTooltip(null); + setStyle(""); + return; + } + + ChatMember member = getTableRow() == null ? null : getTableRow().getItem(); + + setText(item); + setTooltip(new Tooltip(buildWorkedAnyGridStatusTooltip(member))); + setAlignment(Pos.CENTER); + setStyle("-fx-font-weight: bold;"); + } + }); + + TableColumn vhfCol_subcol = new TableColumn("144"); vhfCol_subcol .setCellValueFactory(new Callback, ObservableValue>() { @@ -5487,29 +5602,79 @@ public class Kst4ContestApplication extends Application implements StatusUpdateL ChatMessage sendMe = new ChatMessage(); - /** - * testing bugfix + /* + * Resolve the selected station safely. + * + * Normal case: + * - selectedCallSignInfoStageChatMember is set by clicking/selecting a station. + * + * Fallbacks: + * - current table selection + * - ScoreService selected member + * + * If no station is selected at all, the message is sent to the main category. */ + ChatMember effectiveSelectedMember = selectedCallSignInfoStageChatMember; - ChatCategory sendMeInThisCat; - String categoryNumber = selectedCallSignInfoStageChatMember.getChatCategory().getCategoryNumber() +""; - - if (categoryNumber.equals(chatcontroller.getChatCategoryMain().getCategoryNumber() + "")) { - sendMeInThisCat = chatcontroller.getChatCategoryMain(); - - } else if (categoryNumber.equals(chatcontroller.getChatCategorySecondChat().getCategoryNumber() + "")) { - sendMeInThisCat = chatcontroller.getChatCategorySecondChat(); - - } else { - sendMeInThisCat = chatcontroller.getChatCategoryMain(); //Chatcategory default decision + if (effectiveSelectedMember == null + && tbl_chatMember != null + && tbl_chatMember.getSelectionModel() != null) { + effectiveSelectedMember = tbl_chatMember.getSelectionModel().getSelectedItem(); } - System.out.println("<<<<<<<<<<<<<<<<<<<<< detected Category for sending message is " + sendMeInThisCat + " // selected member: " + selectedCallSignInfoStageChatMember.getChatCategory() + " evt " + event.isConsumed() ); - /** - * end testing bugfix - */ + if (effectiveSelectedMember == null + && chatcontroller != null + && chatcontroller.getScoreService() != null) { + effectiveSelectedMember = chatcontroller.getScoreService().getSelectedChatMember(); + } - sendMe.setChatCategory(sendMeInThisCat); //new in 1.26, answer in channel of the selected member + /* + * Default decision: + * If no station is selected, use the main category. + */ + ChatCategory sendMeInThisCat = chatcontroller.getChatCategoryMain(); + + /* + * If a station is selected and its category matches one of our active chat + * categories, send in that category. This keeps the old behaviour, but avoids + * NullPointerExceptions when no station has been selected yet. + */ + if (effectiveSelectedMember != null && effectiveSelectedMember.getChatCategory() != null) { + + String categoryNumber = effectiveSelectedMember.getChatCategory().getCategoryNumber() + ""; + + if (chatcontroller.getChatCategoryMain() != null + && categoryNumber.equals(chatcontroller.getChatCategoryMain().getCategoryNumber() + "")) { + + sendMeInThisCat = chatcontroller.getChatCategoryMain(); + + } else if (chatcontroller.getChatCategorySecondChat() != null + && categoryNumber.equals(chatcontroller.getChatCategorySecondChat().getCategoryNumber() + "")) { + + sendMeInThisCat = chatcontroller.getChatCategorySecondChat(); + + } else { + sendMeInThisCat = chatcontroller.getChatCategoryMain(); // Chatcategory default decision + } + } + + String selectedMemberDebugText; + if (effectiveSelectedMember == null) { + selectedMemberDebugText = "none, using main category"; + } else { + selectedMemberDebugText = effectiveSelectedMember.getCallSignRaw() + + " / " + + effectiveSelectedMember.getChatCategory(); + } + + System.out.println("<<<<<<<<<<<<<<<<<<<<< detected Category for sending message is " + + sendMeInThisCat + + " // selected member: " + + selectedMemberDebugText + + " evt " + + event.isConsumed()); + + sendMe.setChatCategory(sendMeInThisCat); // new in 1.26, answer in channel of the selected member if available sendMe.setMessageText(txt_chatMessageUserInput.getText()); // If operator sends "/cq CALL ..." => arm pending ping metrics for reply-time / no-reply tracking @@ -5518,11 +5683,9 @@ public class Kst4ContestApplication extends Application implements StatusUpdateL sendMe.setMessageDirectedToServer(false); - chatcontroller.getMessageTXBus().add(sendMe); //move the message to the tx queue + chatcontroller.getMessageTXBus().add(sendMe); // move the message to the tx queue txt_chatMessageUserInput.clear(); - - } }); @@ -6120,13 +6283,14 @@ public class Kst4ContestApplication extends Application implements StatusUpdateL chatMemberTableFilterMaxQrbTF.setFocusTraversable(false); - ToggleButton btnTglNewLocator = new ToggleButton("New locator"); + ToggleButton btnTglNewLocator = new ToggleButton("Only new grids"); Predicate newLocatorPredicate = new Predicate() { @Override public boolean test(ChatMember chatMember) { - return chatcontroller.isNewLocatorOnAnyEnabledBand(chatMember); + return chatcontroller.isNewGridSquare(chatMember); } }; + btnTglNewLocator.setOnAction(new EventHandler() { @Override public void handle(ActionEvent actionEvent) { @@ -6137,7 +6301,26 @@ public class Kst4ContestApplication extends Application implements StatusUpdateL } } }); - btnTglNewLocator.setTooltip(new Tooltip("Show only stations whose gross locator is still new on at least one active own band")); + btnTglNewLocator.setTooltip(new Tooltip("Show only stations whose 4-character grid square has not been worked on any band yet")); + + + ToggleButton btnTglGridColor = new ToggleButton("Grid color"); + + /** + * Enables optional QRA-cell coloring for grid status. + * + *

This does not filter the list. It only colors the locator field: + * worked grid = darker, new grid = brighter.

+ */ + btnTglGridColor.setOnAction(new EventHandler() { + @Override + public void handle(ActionEvent actionEvent) { + gridSquareHighlightEnabled = btnTglGridColor.isSelected(); + tbl_chatMember.refresh(); + } + }); + + btnTglGridColor.setTooltip(new Tooltip("Color the QRA field by grid status without filtering the station list")); ToggleButton btnTglReachableTropo = new ToggleButton("Tropo >=0dB"); Predicate reachableTropoPredicate = new Predicate() { @@ -6807,6 +6990,7 @@ public class Kst4ContestApplication extends Application implements StatusUpdateL chatMemberTableFilterWorkedBandFiltersHbx.getChildren().addAll( btnTglNewLocator, + btnTglGridColor, btnTglReachableTropo, btnTglNewBands, btnTglAsNext5Min @@ -9426,6 +9610,75 @@ public class Kst4ContestApplication extends Application implements StatusUpdateL return sb.toString(); } + + /** + * Formats the global worked/grid status for the worked-any column. + * + *

Status characters follow the user feedback: + *

    + *
  • empty = call not worked, grid not worked
  • + *
  • x = call worked
  • + *
  • o = grid worked, call not worked
  • + *
  • xo = call and grid worked
  • + *
+ * + *

The call status is based on {@link ChatMember#isWorked()}, i.e. worked-any. + * The grid status is based on the four-character grid square worked on any band.

+ * + * @param member station row + * @return compact status text + */ + private String formatWorkedAnyGridStatus(ChatMember member) { + if (member == null) { + return ""; + } + + boolean callWorked = member.isWorked(); + boolean gridWorked = chatcontroller != null && chatcontroller.isGridSquareWorkedAny(member); + + if (callWorked && gridWorked) { + return "xo"; + } + + if (callWorked) { + return "x"; + } + + if (gridWorked) { + return "o"; + } + + return ""; + } + + /** + * Builds a tooltip for the worked-any/grid-status cell. + * + * @param member station row + * @return tooltip text + */ + private String buildWorkedAnyGridStatusTooltip(ChatMember member) { + if (member == null) { + return "empty = call not worked, grid not worked\n" + + "x = call worked\n" + + "o = grid worked\n" + + "xo = call and grid worked"; + } + + String grossField = WorkedGrossFieldCache.extractGrossField(member.getQra()); + String gridText = grossField == null ? "unknown grid" : "grid " + grossField; + + return "Worked status:\n" + + "empty = call not worked, grid not worked\n" + + "x = call worked\n" + + "o = grid worked\n" + + "xo = call and grid worked\n\n" + + "This station:\n" + + "Call worked: " + (member.isWorked() ? "yes" : "no") + "\n" + + "Grid worked: " + (chatcontroller != null && chatcontroller.isGridSquareWorkedAny(member) ? "yes" : "no") + + " (" + gridText + ")"; + } + // /** // * // * resets the style of the not selected direction buttons @@ -9706,6 +9959,72 @@ public class Kst4ContestApplication extends Application implements StatusUpdateL // } + /** + * Returns the best currently selected ChatMember for actions that depend on a + * selected station. + * + *

The normal source is selectedCallSignInfoStageChatMember. As a fallback, + * the current table selection and the ScoreService selection are checked. If + * nothing is selected yet, null is returned. This is valid and must be handled + * by the caller.

+ * + * @return selected ChatMember or null if no station is selected + */ + private ChatMember getEffectiveSelectedChatMember() { + if (selectedCallSignInfoStageChatMember != null) { + return selectedCallSignInfoStageChatMember; + } + + if (tbl_chatMember != null + && tbl_chatMember.getSelectionModel() != null + && tbl_chatMember.getSelectionModel().getSelectedItem() != null) { + return tbl_chatMember.getSelectionModel().getSelectedItem(); + } + + if (chatcontroller != null + && chatcontroller.getScoreService() != null + && chatcontroller.getScoreService().getSelectedChatMember() != null) { + return chatcontroller.getScoreService().getSelectedChatMember(); + } + + return null; + } + + /** + * Resolves the chat category for an outgoing operator message. + * + *

If a station is selected, the message is sent in the category of that + * station. If no station is selected yet, the message is sent in the main chat + * category. This prevents NullPointerExceptions when the operator writes to the + * chat immediately after joining.

+ * + * @param selectedMember selected station, may be null + * @return category for the outgoing message, normally main or second category + */ + private ChatCategory resolveOutgoingChatCategory(ChatMember selectedMember) { + ChatCategory mainCategory = chatcontroller.getChatCategoryMain(); + ChatCategory secondCategory = chatcontroller.getChatCategorySecondChat(); + + if (selectedMember == null || selectedMember.getChatCategory() == null) { + return mainCategory; + } + + String categoryNumber = selectedMember.getChatCategory().getCategoryNumber() + ""; + + if (mainCategory != null + && categoryNumber.equals(mainCategory.getCategoryNumber() + "")) { + return mainCategory; + } + + if (secondCategory != null + && categoryNumber.equals(secondCategory.getCategoryNumber() + "")) { + return secondCategory; + } + + return mainCategory; + } + + /** * helper method, in cases of QRG ending qith 0 as double values, it will fill the ending 0 to the qrg string * @param raw diff --git a/src/main/java/kst4contest/view/map/MapHtmlResources.java b/src/main/java/kst4contest/view/map/MapHtmlResources.java index 8b78af3..ab6223b 100644 --- a/src/main/java/kst4contest/view/map/MapHtmlResources.java +++ b/src/main/java/kst4contest/view/map/MapHtmlResources.java @@ -214,6 +214,11 @@ public final class MapHtmlResources {
+ @@ -366,18 +371,31 @@ public final class MapHtmlResources { applyThemeClass(); map = L.map('map', { - zoomControl: true + zoomControl: true, + 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, + updateWhenZooming: false, + keepBuffer: 4, attribution: '© OpenStreetMap' } - ).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; @@ -416,22 +434,47 @@ 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); - } + if (!map) { + return; + } + + 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(); + } function zoomIn() { if (!map) { @@ -693,6 +736,7 @@ public final class MapHtmlResources { return { init: init, invalidateSize: invalidateSize, + resize: resize, zoomIn: zoomIn, zoomOut: zoomOut, inspectPoint: inspectPoint, diff --git a/src/main/java/kst4contest/view/map/StationMapView.java b/src/main/java/kst4contest/view/map/StationMapView.java index 82250a7..066b8a1 100644 --- a/src/main/java/kst4contest/view/map/StationMapView.java +++ b/src/main/java/kst4contest/view/map/StationMapView.java @@ -49,6 +49,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 +96,22 @@ public final class StationMapView { private List 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 +219,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) + ");" ); } @@ -292,6 +322,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 +655,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 +675,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); @@ -910,9 +944,6 @@ public final class StationMapView { detailPathProfileChart.setObstructionSummary(result.obstructionSummary()); } - - - private void renderAll() { if (!mapReady) { return; @@ -930,49 +961,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 sectorPoints = buildBeamPolygon(homeLatitudeDeg, homeLongitudeDeg, antennaAzimuthDeg, beamWidthDeg, maxQrbKm); + beamJson = toPointArrayJson(sectorPoints); } - List 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 points = List.of( + new double[]{homeLatitudeDeg, homeLongitudeDeg}, + new double[]{lastSelectedSnapshot.latitudeDeg(), lastSelectedSnapshot.longitudeDeg()} + ); + connectionJson = toPointArrayJson(points); } - List 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 +1071,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 buildBeamPolygon(double startLatDeg, double startLonDeg, double centerAzimuthDeg, diff --git a/src/main/java/kst4contest/view/map/TileProxyServer.java b/src/main/java/kst4contest/view/map/TileProxyServer.java index 4d71fbc..8311440 100644 --- a/src/main/java/kst4contest/view/map/TileProxyServer.java +++ b/src/main/java/kst4contest/view/map/TileProxyServer.java @@ -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()); diff --git a/src/main/java/module-info.java b/src/main/java/module-info.java index 3a614f6..1aa283a 100644 --- a/src/main/java/module-info.java +++ b/src/main/java/module-info.java @@ -18,4 +18,4 @@ module praktiKST { opens kst4contest.view.map to javafx.web; -} \ No newline at end of file +}