diff --git a/.clang-format-ignore b/.clang-format-ignore
new file mode 100644
index 00000000..5e71f5bf
--- /dev/null
+++ b/.clang-format-ignore
@@ -0,0 +1 @@
+CMakePresets.json
diff --git a/.clang-tidy b/.clang-tidy
new file mode 100644
index 00000000..0ca21696
--- /dev/null
+++ b/.clang-tidy
@@ -0,0 +1,19 @@
+Checks:
+  - '-*'
+  - 'bugprone-*'
+  - 'clang-analyzer-*'
+  - 'cppcoreguidelines-*'
+  - 'misc-*'
+  - 'modernize-*'
+  - 'performance-*'
+  - '-bugprone-easily-swappable-parameters'
+  - '-cppcoreguidelines-avoid-do-while'
+  - '-cppcoreguidelines-avoid-non-const-global-variables'
+  - '-cppcoreguidelines-pro-type-reinterpret-cast'
+  - '-cppcoreguidelines-pro-type-union-access'
+  - '-misc-include-cleaner'
+  - '-misc-non-private-member-variables-in-classes'
+  - '-misc-use-anonymous-namespace'
+  - '-modernize-return-braced-init-list'
+  - '-modernize-use-trailing-return-type'
+FormatStyle: 'file'
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index ba82d35a..e3033807 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -11,7 +11,7 @@ on:
 
 concurrency:
   # Cancel in-progress jobs for the same pull request
-  group: ${{ github.head_ref || github.run_id }}
+  group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
   cancel-in-progress: true
 
 jobs:
@@ -20,61 +20,130 @@ jobs:
       fail-fast: false
       matrix:
         include:
-          - name: win64_msvc2022
+          - name: windows_msvc2022_x64
             os: windows-2022
             build_type: Release
             env_cc: ''
             env_cxx: ''
             compiler: msvc
+            cppflags: ''
+            ldflags: ''
             msvc_arch: x64
             msvc_version: 2022
-            qt_version: 6.7.2
-            qt_arch_aqt: win64_msvc2019_64
-            qt_arch_dir: msvc2019_64
+            qt_version: 6.9.2
+            qt_arch_aqt: win64_msvc2022_64
+            qt_arch_dir: msvc2022_64
             qt_modules: qtimageformats qtmultimedia qtpositioning qtserialport
             qt_tools: ''
-            conan_arch: x86_64
-            conan_compiler: Visual Studio
-            conan_compiler_version: 17
-            conan_compiler_runtime: --settings compiler.runtime=MD
             conan_package_manager: ''
+            conan_profile: scwx-windows_msvc2022_x64
+            appimage_arch: ''
             artifact_suffix: windows-x64
-          - name: linux64_gcc
+          - name: linux_gcc_x64
             os: ubuntu-22.04
             build_type: Release
             env_cc: gcc-11
             env_cxx: g++-11
             compiler: gcc
-            qt_version: 6.7.2
+            cppflags: ''
+            ldflags: ''
+            qt_version: 6.9.2
             qt_arch_aqt: linux_gcc_64
             qt_arch_dir: gcc_64
             qt_modules: qtimageformats qtmultimedia qtpositioning qtserialport
             qt_tools: ''
-            conan_arch: x86_64
-            conan_compiler: gcc
-            conan_compiler_version: 11
-            conan_compiler_runtime: ''
             conan_package_manager: --conf tools.system.package_manager:mode=install --conf tools.system.package_manager:sudo=True
+            conan_profile: scwx-linux_gcc-11
+            appimage_arch: x86_64
             artifact_suffix: linux-x64
+            compiler_packages: ''
+          - name: linux_clang_x64
+            os: ubuntu-24.04
+            build_type: Release
+            env_cc: clang-17
+            env_cxx: clang++-17
+            compiler: clang
+            cppflags: ''
+            ldflags: ''
+            qt_version: 6.9.2
+            qt_arch_aqt: linux_gcc_64
+            qt_arch_dir: gcc_64
+            qt_modules: qtimageformats qtmultimedia qtpositioning qtserialport
+            qt_tools: ''
+            conan_package_manager: --conf tools.system.package_manager:mode=install --conf tools.system.package_manager:sudo=True
+            conan_profile: scwx-linux_clang-17
+            appimage_arch: x86_64
+            artifact_suffix: linux-clang-x64
+            compiler_packages: clang-17
+          - name: linux_gcc_arm64
+            os: ubuntu-24.04-arm
+            build_type: Release
+            env_cc: gcc-11
+            env_cxx: g++-11
+            compiler: gcc
+            cppflags: ''
+            ldflags: ''
+            qt_version: 6.9.2
+            qt_arch_aqt: linux_gcc_arm64
+            qt_arch_dir: gcc_arm64
+            qt_modules: qtimageformats qtmultimedia qtpositioning qtserialport
+            qt_tools: ''
+            conan_package_manager: --conf tools.system.package_manager:mode=install --conf tools.system.package_manager:sudo=True
+            conan_profile: scwx-linux_gcc-11_armv8
+            appimage_arch: aarch64
+            artifact_suffix: linux-arm64
+            compiler_packages: g++-11
+          - name: macos_clang18_x64
+            os: macos-13
+            build_type: Release
+            env_cc: clang
+            env_cxx: clang++
+            compiler: clang
+            qt_version: 6.9.2
+            qt_arch_aqt: clang_64
+            qt_arch_dir: macos
+            qt_modules: qtimageformats qtmultimedia qtpositioning qtserialport
+            qt_tools: ''
+            conan_package_manager: ''
+            conan_profile: scwx-macos_clang-18
+            appimage_arch: ''
+            artifact_suffix: macos-x64
+          - name: macos_clang18_arm64
+            os: macos-14
+            build_type: Release
+            env_cc: clang
+            env_cxx: clang++
+            compiler: clang
+            qt_version: 6.9.2
+            qt_arch_aqt: clang_64
+            qt_arch_dir: macos
+            qt_modules: qtimageformats qtmultimedia qtpositioning qtserialport
+            qt_tools: ''
+            conan_package_manager: ''
+            conan_profile: scwx-macos_clang-18_armv8
+            appimage_arch: ''
+            artifact_suffix: macos-arm64
     name: ${{ matrix.name }}
     env:
       CC: ${{ matrix.env_cc }}
       CXX: ${{ matrix.env_cxx }}
-      SCWX_VERSION: v0.4.5
+      SCWX_VERSION: v0.5.1
     runs-on: ${{ matrix.os }}
 
     steps:
     - name: Setup
       run: git config --global core.longpaths true
-      
+
     - name: Checkout
-      uses: actions/checkout@v4
+      uses: actions/checkout@v5
       with:
         path: source
         submodules: recursive
 
     - name: Install Qt
-      uses: jurplel/install-qt-action@v3
+      uses: jdpurcell/install-qt-action@v5
+      env:
+        AQT_CONFIG: ${{ github.workspace }}/source/tools/aqt-settings.ini
       with:
         version: ${{ matrix.qt_version }}
         arch: ${{ matrix.qt_arch_aqt }}
@@ -89,50 +158,79 @@ jobs:
         vsversion: ${{ matrix.msvc_version }}
 
     - name: Setup Ubuntu Environment
-      if: matrix.os == 'ubuntu-22.04'
+      if: ${{ startsWith(matrix.os, 'ubuntu') }}
       shell: bash
       run: |
         sudo apt-get install doxygen \
                              libfuse2 \
-                             ninja-build
+                             ninja-build \
+                             wayland-protocols \
+                             libwayland-dev \
+                             libwayland-egl-backend-dev \
+                             flatpak \
+                             flatpak-builder \
+                             ${{ matrix.compiler_packages }}
+
+    - name: Setup macOS Environment
+      if: ${{ startsWith(matrix.os, 'macos') }}
+      shell: bash
+      run: |
+        brew install llvm@18
+        LLVM_PATH=$(brew --prefix llvm@18)
+        echo "CC=${LLVM_PATH}/bin/clang" >> $GITHUB_ENV
+        echo "CXX=${LLVM_PATH}/bin/clang++" >> $GITHUB_ENV
+        echo "CPPFLAGS=-I${LLVM_PATH}/include" >> $GITHUB_ENV
+        echo "LDFLAGS=-L${LLVM_PATH}/lib -L${LLVM_PATH}/lib/c++" >> $GITHUB_ENV
 
     - name: Setup Python Environment
       shell: pwsh
       run: |
         pip install geopandas `
-                    GitPython
+                    GitPython `
+                    conan
+
+    - name: Cache Conan Packages
+      uses: actions/cache@v4
+      with:
+        path: ~/.conan2
+        key: build-${{ matrix.conan_profile }}-${{ hashFiles('./source/conanfile.py', './source/tools/conan/profiles/*') }}
 
     - name: Install Conan Packages
       shell: pwsh
       run: |
-        pip install "conan<2.0"
-        conan profile new default --detect
-        conan install ./source/ `
+        conan config install `
+          ./source/tools/conan/profiles/${{ matrix.conan_profile }} `
+          -tf profiles
+        mkdir build
+        cd build
+        mkdir conan
+        conan install ../source/ `
           --remote conancenter `
           --build missing `
-          --settings arch=${{ matrix.conan_arch }} `
-          --settings build_type=${{ matrix.build_type }} `
-          --settings compiler="${{ matrix.conan_compiler }}" `
-          --settings compiler.version=${{ matrix.conan_compiler_version }} `
-          ${{ matrix.conan_compiler_runtime }} `
+          --profile:all ${{ matrix.conan_profile }} `
+          --settings:all build_type=${{ matrix.build_type }} `
+          --output-folder ./conan/ `
           ${{ matrix.conan_package_manager }}
 
     - name: Build Supercell Wx
       shell: pwsh
       run: |
-        mkdir build
         cd build
         cmake ../source/ `
           -G Ninja `
           -DCMAKE_BUILD_TYPE="${{ matrix.build_type }}" `
-          -DCMAKE_INSTALL_PREFIX="${{ github.workspace }}/supercell-wx"
+          -DCMAKE_PROJECT_TOP_LEVEL_INCLUDES="${{ github.workspace }}/source/external/cmake-conan/conan_provider.cmake" `
+          -DCMAKE_INSTALL_PREFIX="${{ github.workspace }}/supercell-wx" `
+          -DCONAN_HOST_PROFILE="${{ matrix.conan_profile }}" `
+          -DCONAN_BUILD_PROFILE="${{ matrix.conan_profile }}"
         ninja supercell-wx wxtest
 
     - name: Separate Debug Symbols (Linux)
-      if: matrix.os == 'ubuntu-22.04'
+      if: ${{ startsWith(matrix.os, 'ubuntu') }}
       shell: bash
       run: |
         cd build/
+        cd Release/
         cd bin/
         objcopy --only-keep-debug supercell-wx supercell-wx.debug
         objcopy --strip-debug --strip-unneeded supercell-wx
@@ -148,7 +246,7 @@ jobs:
         cmake --install . --component supercell-wx
 
     - name: Collect Artifacts
-      if: matrix.os == 'ubuntu-22.04'
+      if: ${{ startsWith(matrix.os, 'ubuntu') }}
       shell: bash
       run: |
         pushd supercell-wx/
@@ -161,6 +259,8 @@ jobs:
         cd plugins/
         mkdir -p sqldrivers/
         cp "${RUNNER_WORKSPACE}/Qt/${{ matrix.qt_version }}/${{ matrix.qt_arch_dir }}/plugins/sqldrivers/libqsqlite.so" sqldrivers/
+        mkdir -p platforms/
+        cp ${RUNNER_WORKSPACE}/Qt/${{ matrix.qt_version }}/${{ matrix.qt_arch_dir }}/plugins/platforms/libqwayland* platforms/
         cd ..
         popd
         tar -czf supercell-wx-${{ matrix.artifact_suffix }}.tar.gz supercell-wx/
@@ -177,23 +277,23 @@ jobs:
       uses: actions/upload-artifact@v4
       with:
         name: supercell-wx-debug-${{ matrix.artifact_suffix }}
-        path: ${{ github.workspace }}/build/bin/*.pdb
+        path: ${{ github.workspace }}/build/Release/bin/*.pdb
 
     - name: Upload Artifacts (Linux)
-      if: matrix.os == 'ubuntu-22.04'
+      if: ${{ startsWith(matrix.os, 'ubuntu') }}
       uses: actions/upload-artifact@v4
       with:
         name: supercell-wx-${{ matrix.artifact_suffix }}
         path: ${{ github.workspace }}/supercell-wx-${{ matrix.artifact_suffix }}.tar.gz
 
     - name: Upload Debug Artifacts (Linux)
-      if: matrix.os == 'ubuntu-22.04'
+      if: ${{ startsWith(matrix.os, 'ubuntu') }}
       uses: actions/upload-artifact@v4
       with:
         name: supercell-wx-debug-${{ matrix.artifact_suffix }}
         path: |
-          ${{ github.workspace }}/build/bin/*.debug
-          ${{ github.workspace }}/build/lib/*.debug
+          ${{ github.workspace }}/build/Release/bin/*.debug
+          ${{ github.workspace }}/build/Release/lib/*.debug
 
     - name: Build Installer (Windows)
       if: matrix.os == 'windows-2022'
@@ -210,42 +310,90 @@ jobs:
         path: ${{ github.workspace }}/build/supercell-wx-*.msi*
 
     - name: Build AppImage (Linux)
-      if: matrix.os == 'ubuntu-22.04'
+      if: ${{ startsWith(matrix.os, 'ubuntu') }}
       env:
-        APPIMAGE_DIR: ${{ github.workspace }}/supercell-wx/
-        LDAI_UPDATE_INFORMATION: gh-releases-zsync|dpaulat|supercell-wx|latest|*x86_64.AppImage.zsync
-        LDAI_OUTPUT: supercell-wx-${{ env.SCWX_VERSION }}-x86_64.AppImage
+        INSTALL_DIR: ${{ github.workspace }}/supercell-wx/
+        APPIMAGE_DIR: ${{ github.workspace }}/supercell-wx-appimage/
+        LDAI_UPDATE_INFORMATION: gh-releases-zsync|dpaulat|supercell-wx|latest|*${{ matrix.appimage_arch }}.AppImage.zsync
+        LDAI_OUTPUT: supercell-wx-${{ env.SCWX_VERSION }}-${{ matrix.appimage_arch }}.AppImage
         LINUXDEPLOY_OUTPUT_APP_NAME: supercell-wx
         LINUXDEPLOY_OUTPUT_VERSION: ${{ env.SCWX_VERSION }}
       shell: bash
       run: |
-        wget https://github.com/linuxdeploy/linuxdeploy/releases/download/continuous/linuxdeploy-x86_64.AppImage
-        chmod +x linuxdeploy-x86_64.AppImage
+        wget https://github.com/linuxdeploy/linuxdeploy/releases/download/continuous/linuxdeploy-${{ matrix.appimage_arch }}.AppImage
+        chmod +x linuxdeploy-${{ matrix.appimage_arch }}.AppImage
         cp "${{ github.workspace }}/source/scwx-qt/res/icons/scwx-256.png" supercell-wx.png
         cp "${{ github.workspace }}/source/scwx-qt/res/linux/supercell-wx.desktop" .
+        cp -r "${{ env.INSTALL_DIR }}" "${{ env.APPIMAGE_DIR }}"
         pushd "${{ env.APPIMAGE_DIR }}"
         mkdir -p usr/
         mv bin/ usr/
         mv lib/ usr/
         mv plugins/ usr/
         popd
-        ./linuxdeploy-x86_64.AppImage --appdir ${{ env.APPIMAGE_DIR }} -i supercell-wx.png -d supercell-wx.desktop
-        ./linuxdeploy-x86_64.AppImage --appdir ${{ env.APPIMAGE_DIR }} --output appimage
-        rm -f linuxdeploy-x86_64.AppImage
+        ./linuxdeploy-${{ matrix.appimage_arch }}.AppImage --appdir ${{ env.APPIMAGE_DIR }} -i supercell-wx.png -d supercell-wx.desktop
+        ./linuxdeploy-${{ matrix.appimage_arch }}.AppImage --appdir ${{ env.APPIMAGE_DIR }} --output appimage
+        rm -f linuxdeploy-${{ matrix.appimage_arch }}.AppImage
 
     - name: Upload AppImage (Linux)
-      if: matrix.os == 'ubuntu-22.04'
+      if: ${{ startsWith(matrix.os, 'ubuntu') }}
       uses: actions/upload-artifact@v4
       with:
-        name: supercell-wx-appimage-x64
-        path: ${{ github.workspace }}/*-x86_64.AppImage*
+        name: supercell-wx-appimage-${{ matrix.artifact_suffix }}
+        path: ${{ github.workspace }}/*-${{ matrix.appimage_arch }}.AppImage*
+
+    - name: Build FlatPak (Linux)
+      if: ${{ startsWith(matrix.os, 'ubuntu') }}
+      env:
+        INSTALL_DIR: ${{ github.workspace }}/supercell-wx/
+        FLATPAK_DIR: ${{ github.workspace }}/supercell-wx-flatpak/
+      shell: bash
+      run: |
+        cp -r ${{ env.INSTALL_DIR }} ${{ env.FLATPAK_DIR }}
+        # Copy krb5 libraries to flatpak
+        cp /usr/lib/*/libkrb5.so* \
+           /usr/lib/*/libkrb5support.so* \
+           /usr/lib/*/libgssapi_krb5.so* \
+           /usr/lib/*/libk5crypto.so* \
+           /usr/lib/*/libkeyutils.so* \
+           ${{ env.FLATPAK_DIR }}/lib
+
+        flatpak remote-add --if-not-exists --user flathub https://dl.flathub.org/repo/flathub.flatpakrepo
+        flatpak-builder --force-clean \
+            --user \
+            --install-deps-from=flathub \
+            --repo=flatpak-repo \
+            --install flatpak-build \
+            ${{ github.workspace }}/source/tools/net.supercellwx.app.yml
+        flatpak build-bundle flatpak-repo supercell-wx.flatpak net.supercellwx.app
+
+    - name: Upload FlatPak (Linux)
+      if: ${{ startsWith(matrix.os, 'ubuntu') }}
+      uses: actions/upload-artifact@v4
+      with:
+        name: supercell-wx-flatpak-${{ matrix.artifact_suffix }}
+        path: ${{ github.workspace }}/supercell-wx.flatpak
+
+    - name: Build Disk Image (macOS)
+      if: ${{ startsWith(matrix.os, 'macos') }}
+      shell: pwsh
+      run: |
+        cd build
+        cpack
+
+    - name: Upload Disk Image (macOS)
+      if: ${{ startsWith(matrix.os, 'macos') }}
+      uses: actions/upload-artifact@v4
+      with:
+        name: supercell-wx-${{ matrix.artifact_suffix }}
+        path: ${{ github.workspace }}/build/supercell-wx-*.dmg*
 
     - name: Test Supercell Wx
       working-directory: ${{ github.workspace }}/build
       env:
         MAPBOX_API_KEY:   ${{ secrets.MAPBOX_API_KEY }}
         MAPTILER_API_KEY: ${{ secrets.MAPTILER_API_KEY }}
-      run: ctest -C ${{ matrix.build_type }} --exclude-regex test_mln.*
+      run: ctest -C ${{ matrix.build_type }} --exclude-regex "test_mln.*|NtpClient.*|UpdateManager.*"
 
     - name: Upload Test Logs
       if: ${{ !cancelled() }}
diff --git a/.github/workflows/clang-format-check.yml b/.github/workflows/clang-format-check.yml
new file mode 100644
index 00000000..7eaa1e95
--- /dev/null
+++ b/.github/workflows/clang-format-check.yml
@@ -0,0 +1,42 @@
+name: clang-format-check
+
+on:
+  workflow_dispatch:
+  pull_request:
+    branches:
+      - 'develop'
+
+concurrency:
+  # Cancel in-progress jobs for the same pull request
+  group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
+  cancel-in-progress: true
+
+jobs:
+  format:
+    runs-on: ubuntu-24.04
+    steps:
+
+    - name: Checkout
+      uses: actions/checkout@v5
+      with:
+        fetch-depth: 0
+        submodules: false
+
+    - name: Update References
+      shell: bash
+      run: |
+        git fetch origin develop
+
+    - name: Setup Ubuntu Environment
+      shell: bash
+      run: |
+        sudo apt-get install clang-format-19
+        sudo rm -f /usr/bin/clang-format
+        sudo ln -s /usr/bin/clang-format-19 /usr/bin/clang-format
+
+    - name: Check Formatting
+      shell: bash
+      run: |
+        MERGE_BASE=$(git merge-base origin/develop ${{ github.event.pull_request.head.sha || github.ref }})
+        echo "Comparing against ${MERGE_BASE}"
+        git clang-format-19 --diff --style=file -v ${MERGE_BASE}
diff --git a/.github/workflows/clang-tidy-comments.yml b/.github/workflows/clang-tidy-comments.yml
new file mode 100644
index 00000000..9a6df52d
--- /dev/null
+++ b/.github/workflows/clang-tidy-comments.yml
@@ -0,0 +1,21 @@
+name: Post clang-tidy Review Comments
+
+on:
+  workflow_run:
+    workflows: ["clang-tidy-review"]
+    types:
+      - completed
+
+jobs:
+  build:
+    runs-on: ubuntu-latest
+    if: ${{ github.event.workflow_run.conclusion != 'cancelled' }}
+
+    steps:
+      - name: Post Comments
+        uses: ZedThree/clang-tidy-review/post@v0.21.0
+        with:
+          lgtm_comment_body: ''
+          annotations: false
+          max_comments: 25
+          num_comments_as_exitcode: false
diff --git a/.github/workflows/clang-tidy-review.yml b/.github/workflows/clang-tidy-review.yml
new file mode 100644
index 00000000..b3de8edf
--- /dev/null
+++ b/.github/workflows/clang-tidy-review.yml
@@ -0,0 +1,152 @@
+name: clang-tidy-review
+
+on:
+  pull_request:
+    branches:
+      - 'develop'
+
+concurrency:
+  # Cancel in-progress jobs for the same pull request
+  group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
+  cancel-in-progress: true
+
+jobs:
+  build:
+    strategy:
+      matrix:
+        include:
+          - name: linux_clang-tidy_x64
+            os: ubuntu-24.04
+            build_type: Release
+            env_cc: clang-18
+            env_cxx: clang++-18
+            qt_version: 6.9.2
+            qt_arch_aqt: linux_gcc_64
+            qt_modules: qtimageformats qtmultimedia qtpositioning qtserialport
+            qt_tools: ''
+            conan_package_manager: --conf tools.system.package_manager:mode=install --conf tools.system.package_manager:sudo=True
+            conan_profile: scwx-linux_clang-18
+            compiler_packages: clang-18 clang-tidy-18
+            clang_tidy_binary: clang-tidy-18
+    name: ${{ matrix.name }}
+    runs-on: ${{ matrix.os }}
+    env:
+      CC: ${{ matrix.env_cc }}
+      CXX: ${{ matrix.env_cxx }}
+    steps:
+
+    - name: Checkout
+      uses: actions/checkout@v5
+      with:
+        path: source
+        submodules: recursive
+
+    - name: Checkout clang-tidy-review Repository
+      uses: actions/checkout@v5
+      with:
+        repository: ZedThree/clang-tidy-review
+        ref: v0.20.1
+        path: clang-tidy-review
+
+    - name: Install Qt
+      uses: jdpurcell/install-qt-action@v5
+      env:
+        AQT_CONFIG: ${{ github.workspace }}/source/tools/aqt-settings.ini
+      with:
+        version: ${{ matrix.qt_version }}
+        arch: ${{ matrix.qt_arch_aqt }}
+        modules: ${{ matrix.qt_modules }}
+        tools: ${{ matrix.qt_tools }}
+
+    - name: Setup Ubuntu Environment
+      if: ${{ startsWith(matrix.os, 'ubuntu') }}
+      shell: bash
+      run: |
+        sudo apt-get install doxygen \
+                             libfuse2 \
+                             ninja-build \
+                             wayland-protocols \
+                             libwayland-dev \
+                             libwayland-egl-backend-dev \
+                             ${{ matrix.compiler_packages }}
+
+    - name: Setup Python Environment
+      shell: pwsh
+      run: |
+        pip install geopandas `
+                    GitPython `
+                    conan
+        pip install --break-system-packages clang-tidy-review/post/clang_tidy_review
+
+    - name: Cache Conan Packages
+      uses: actions/cache@v4
+      with:
+        path: ~/.conan2
+        key: build-${{ matrix.conan_profile }}-${{ hashFiles('./source/conanfile.py', './source/tools/conan/profiles/*') }}
+
+    - name: Install Conan Packages
+      shell: pwsh
+      run: |
+        conan config install `
+          ./source/tools/conan/profiles/${{ matrix.conan_profile }} `
+          -tf profiles
+        mkdir build
+        cd build
+        mkdir conan
+        conan install ../source/ `
+          --remote conancenter `
+          --build missing `
+          --profile:all ${{ matrix.conan_profile }} `
+          --settings:all build_type=${{ matrix.build_type }} `
+          --output-folder ./conan/ `
+          ${{ matrix.conan_package_manager }}
+
+    - name: Autogenerate
+      shell: pwsh
+      run: |
+        cd build
+        cmake ../source/ `
+          -G Ninja `
+          -DCMAKE_BUILD_TYPE="${{ matrix.build_type }}" `
+          -DCMAKE_PROJECT_TOP_LEVEL_INCLUDES="${{ github.workspace }}/source/external/cmake-conan/conan_provider.cmake" `
+          -DCMAKE_INSTALL_PREFIX="${{ github.workspace }}/supercell-wx" `
+          -DCONAN_HOST_PROFILE="${{ matrix.conan_profile }}" `
+          -DCONAN_BUILD_PROFILE="${{ matrix.conan_profile }}" `
+          -DCMAKE_EXPORT_COMPILE_COMMANDS=on
+        ninja glad_gl_core_33 `
+              scwx-qt_generate_counties_db `
+              scwx-qt_generate_versions `
+              scwx-qt_autogen
+
+    - name: Code Review
+      id: review
+      shell: bash
+      run: |
+        cd source
+        review --clang_tidy_binary=${{ matrix.clang_tidy_binary }} \
+               --token=${{ github.token }} \
+               --repo='${{ github.repository }}' \
+               --pr='${{ github.event.pull_request.number }}' \
+               --build_dir='../build' \
+               --base_dir='${{ github.workspace }}/source' \
+               --clang_tidy_checks='' \
+               --config_file='' \
+               --include='*.[ch],*.[ch]xx,*.[chi]pp,*.[ch]++,*.cc,*.hh' \
+               --exclude='' \
+               --apt-packages='' \
+               --cmake-command='' \
+               --max-comments=25 \
+               --lgtm-comment-body='' \
+               --split_workflow=true \
+               --annotations=false \
+               --parallel=0
+        rsync -avzh --ignore-missing-args clang-tidy-review-output.json ../
+        rsync -avzh --ignore-missing-args clang-tidy-review-metadata.json ../
+        rsync -avzh --ignore-missing-args clang_fixes.json ../
+
+    - name: Upload Review
+      uses: ZedThree/clang-tidy-review/upload@v0.21.0
+
+    - name: Status Check
+      if: steps.review.outputs.total_comments > 0
+      run: exit 1
diff --git a/.gitignore b/.gitignore
index 9e6da487..cb46cec7 100644
--- a/.gitignore
+++ b/.gitignore
@@ -5,9 +5,20 @@ CMakeLists.txt.user
 CMakeCache.txt
 CMakeFiles
 CMakeScripts
+CMakeUserPresets.json
 Testing
 cmake_install.cmake
 install_manifest.txt
 compile_commands.json
 CTestTestfile.cmake
 _deps
+
+# Editor directories
+.idea/
+.vs/
+
+# Python Virtual Environment
+.venv/
+
+# Specific excludes for Supercell Wx
+tools/lib/user-setup.sh
diff --git a/.gitmodules b/.gitmodules
index 52ede30b..5bf3b307 100644
--- a/.gitmodules
+++ b/.gitmodules
@@ -1,39 +1,45 @@
-[submodule "external/cmake-conan"]
-	path = external/cmake-conan
-	url = https://github.com/conan-io/cmake-conan.git
-[submodule "test/data"]
-	path = test/data
-	url = https://github.com/dpaulat/supercell-wx-test-data
-[submodule "external/hsluv-c"]
-	path = external/hsluv-c
-	url = https://github.com/hsluv/hsluv-c.git
-[submodule "external/stb"]
-	path = external/stb
-	url = https://github.com/nothings/stb.git
 [submodule "data"]
 	path = data
 	url = https://github.com/dpaulat/supercell-wx-data
+[submodule "external/aws-sdk-cpp"]
+	path = external/aws-sdk-cpp
+	url = https://github.com/aws/aws-sdk-cpp.git
+[submodule "external/cmake-conan"]
+	path = external/cmake-conan
+	url = https://github.com/conan-io/cmake-conan.git
+[submodule "external/date"]
+	path = external/date
+	url = https://github.com/HowardHinnant/date.git
+[submodule "external/glad"]
+	path = external/glad
+	url = https://github.com/Dav1dde/glad.git
+[submodule "external/hsluv-c"]
+	path = external/hsluv-c
+	url = https://github.com/hsluv/hsluv-c.git
 [submodule "external/imgui"]
 	path = external/imgui
 	url = https://github.com/ocornut/imgui.git
 [submodule "external/imgui-backend-qt"]
 	path = external/imgui-backend-qt
 	url = https://github.com/dpaulat/imgui-backend-qt
-[submodule "external/aws-sdk-cpp"]
-	path = external/aws-sdk-cpp
-	url = https://github.com/aws/aws-sdk-cpp.git
-[submodule "external/date"]
-	path = external/date
-	url = https://github.com/HowardHinnant/date.git
-[submodule "external/units"]
-	path = external/units
-	url = https://github.com/nholthaus/units.git
-[submodule "external/textflowcpp"]
-	path = external/textflowcpp
-	url = https://github.com/catchorg/textflowcpp.git
-[submodule "external/maplibre-native-qt"]
-	path = external/maplibre-native-qt
-	url = https://github.com/dpaulat/maplibre-native-qt.git
 [submodule "external/maplibre-native"]
 	path = external/maplibre-native
 	url = https://github.com/dpaulat/maplibre-gl-native.git
+[submodule "external/maplibre-native-qt"]
+	path = external/maplibre-native-qt
+	url = https://github.com/dpaulat/maplibre-native-qt.git
+[submodule "external/qt6ct"]
+	path = external/qt6ct
+	url = https://github.com/AdenKoperczak/qt6ct.git
+[submodule "external/stb"]
+	path = external/stb
+	url = https://github.com/nothings/stb.git
+[submodule "external/textflowcpp"]
+	path = external/textflowcpp
+	url = https://github.com/catchorg/textflowcpp.git
+[submodule "external/units"]
+	path = external/units
+	url = https://github.com/nholthaus/units.git
+[submodule "test/data"]
+	path = test/data
+	url = https://github.com/dpaulat/supercell-wx-test-data
diff --git a/ACKNOWLEDGEMENTS.md b/ACKNOWLEDGEMENTS.md
index 154d0f6c..1608b2ae 100644
--- a/ACKNOWLEDGEMENTS.md
+++ b/ACKNOWLEDGEMENTS.md
@@ -23,7 +23,7 @@ Supercell Wx uses code from the following dependencies:
 | [FreeType GL](https://github.com/rougier/freetype-gl) | [BSD 2-Clause with views sentence](https://spdx.org/licenses/BSD-2-Clause-Views.html) |
 | [GeographicLib](https://geographiclib.sourceforge.io/) | [MIT License](https://spdx.org/licenses/MIT.html) |
 | [geos](https://libgeos.org/) | [GNU Lesser General Public License v2.1 or later](https://spdx.org/licenses/LGPL-2.1-or-later.html) |
-| [GLEW](https://www.opengl.org/sdk/libs/GLEW/) | [MIT License](https://spdx.org/licenses/MIT.html) |
+| [GLAD](https://github.com/Dav1dde/glad) | [MIT License](https://spdx.org/licenses/MIT.html) |
 | [GLM](https://github.com/g-truc/glm) | [MIT License](https://spdx.org/licenses/MIT.html) |
 | [GoogleTest](https://google.github.io/googletest/) | [BSD 3-Clause "New" or "Revised" License](https://spdx.org/licenses/BSD-3-Clause.html) |
 | [HSLuv](https://www.hsluv.org/) | [MIT License](https://spdx.org/licenses/MIT.html) |
@@ -32,9 +32,12 @@ Supercell Wx uses code from the following dependencies:
 | [libpng](http://libpng.org/pub/png/libpng.html) | [PNG Reference Library version 2](https://spdx.org/licenses/libpng-2.0.html) |
 | [libxml2](http://xmlsoft.org/) | [MIT License](https://spdx.org/licenses/MIT.html) |
 | [MapLibre Native](https://maplibre.org/projects/maplibre-native/) | [BSD 2-Clause "Simplified" License](https://spdx.org/licenses/BSD-2-Clause.html) |
+| [Mesa 3D](https://mesa3d.org/) | [MIT License](https://spdx.org/licenses/MIT.html) |
 | [nunicode](https://bitbucket.org/alekseyt/nunicode/src/master/) | [MIT License](https://spdx.org/licenses/MIT.html) | Modified for MapLibre Native |
 | [OpenSSL](https://www.openssl.org/) | [OpenSSL License](https://spdx.org/licenses/OpenSSL.html) |
 | [Qt](https://www.qt.io/) | [GNU Lesser General Public License v3.0 only](https://spdx.org/licenses/LGPL-3.0-only.html) | Qt Core, Qt GUI, Qt Multimedia, Qt Network, Qt OpenGL, Qt Positioning, Qt Serial Port, Qt SQL, Qt SVG, Qt Widgets
Additional Licenses: https://doc.qt.io/qt-6/licenses-used-in-qt.html |
+| [qt6ct](https://github.com/trialuser02/qt6ct) | [BSD 2-Clause "Simplified" License](https://spdx.org/licenses/BSD-2-Clause.html) |
+| [range-v3](https://github.com/ericniebler/range-v3) | [Boost Software License 1.0](https://spdx.org/licenses/BSL-1.0.html)
[MIT License](https://spdx.org/licenses/MIT.html)
[Stepanov and McJones, "Elements of Programming" license](https://github.com/ericniebler/range-v3/tree/0.12.0?tab=License-1-ov-file)
[SGI C++ Standard Template Library license](https://github.com/ericniebler/range-v3/tree/0.12.0?tab=License-1-ov-file) |
 | [re2](https://github.com/google/re2) | [BSD 3-Clause "New" or "Revised" License](https://spdx.org/licenses/BSD-3-Clause.html) |
 | [spdlog](https://github.com/gabime/spdlog) | [MIT License](https://spdx.org/licenses/MIT.html) |
 | [SQLite](https://www.sqlite.org/) | Public Domain |
@@ -67,6 +70,7 @@ Supercell Wx uses assets from the following sources:
 | [Font Awesome Free](https://fontawesome.com/) | CC BY 4.0 License |
 | [Inconsolata](https://fonts.google.com/specimen/Inconsolata) | SIL Open Font License |
 | [NOAA's Weather and Climate Toolkit](https://www.ncdc.noaa.gov/wct/) | Public Domain | Default Color Tables |
+| [qt6ct](https://github.com/trialuser02/qt6ct) | [BSD 2-Clause "Simplified" License](https://spdx.org/licenses/BSD-2-Clause.html) |
 | [Roboto Flex](https://fonts.google.com/specimen/Roboto+Flex) | SIL Open Font License |
 | [Supercell thunderstorm with dramatic clouds](https://www.shutterstock.com/image-photo/supercell-thunderstorm-dramatic-clouds-1354353521) | Shutterstock Standard License | Photo by John Sirlin
 
diff --git a/CMakeLists.txt b/CMakeLists.txt
index c06a46e6..e76cb8d1 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -1,7 +1,14 @@
-cmake_minimum_required(VERSION 3.21)
+cmake_minimum_required(VERSION 3.24)
 set(PROJECT_NAME supercell-wx)
+
+include(tools/scwx_config.cmake)
+
+set(CMAKE_OSX_DEPLOYMENT_TARGET 12.0)
+
+scwx_python_setup()
+
 project(${PROJECT_NAME}
-        VERSION      0.4.5
+        VERSION      0.5.1
         DESCRIPTION  "Supercell Wx is a free, open source advanced weather radar viewer."
         HOMEPAGE_URL "https://github.com/dpaulat/supercell-wx"
         LANGUAGES    C CXX)
@@ -11,40 +18,21 @@ set(CMAKE_POLICY_DEFAULT_CMP0077 NEW)
 set(CMAKE_POLICY_DEFAULT_CMP0079 NEW)
 set(CMAKE_POLICY_DEFAULT_CMP0148 OLD) # aws-sdk-cpp uses FindPythonInterp
 
+scwx_output_dirs_setup()
+
 enable_testing()
 
 set(CMAKE_MODULE_PATH "${PROJECT_SOURCE_DIR}/cmake" ${CMAKE_MODULE_PATH})
 
-include(${PROJECT_SOURCE_DIR}/external/cmake-conan/conan.cmake)
-
 set_property(DIRECTORY
              APPEND
              PROPERTY CMAKE_CONFIGURE_DEPENDS
              conanfile.py)
 
-# Don't use RelWithDebInfo Conan packages
-if (${CMAKE_BUILD_TYPE} STREQUAL "RelWithDebInfo")
-    set(conan_build_type "Release")
-else()
-    set(conan_build_type ${CMAKE_BUILD_TYPE})
-endif()
-
-conan_cmake_autodetect(settings
-                       BUILD_TYPE ${conan_build_type})
-
-conan_cmake_install(PATH_OR_REFERENCE ${PROJECT_SOURCE_DIR}
-                    BUILD missing
-                    REMOTE conancenter
-                    SETTINGS ${settings})
-
-include(${CMAKE_BINARY_DIR}/conanbuildinfo.cmake)
-include(${CMAKE_BINARY_DIR}/conan_paths.cmake)
-conan_basic_setup(TARGETS)
-
 set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DBOOST_ALL_NO_LIB")
 
 set(SCWX_DIR ${PROJECT_SOURCE_DIR})
-set(SCWX_VERSION "0.4.5")
+set(SCWX_VERSION "0.5.1")
 
 option(SCWX_ADDRESS_SANITIZER "Build with Address Sanitizer" OFF)
 
diff --git a/CMakePresets.json b/CMakePresets.json
new file mode 100644
index 00000000..ec997016
--- /dev/null
+++ b/CMakePresets.json
@@ -0,0 +1,383 @@
+{
+  "version": 5,
+  "cmakeMinimumRequired": {
+    "major": 3,
+    "minor": 24,
+    "patch": 0
+  },
+  "configurePresets": [
+    {
+      "name": "base",
+      "hidden": true,
+      "generator": "Ninja",
+      "binaryDir": "${sourceDir}/build/${presetName}",
+      "cacheVariables": {
+        "CMAKE_PROJECT_TOP_LEVEL_INCLUDES": "${sourceDir}/external/cmake-conan/conan_provider.cmake",
+        "SCWX_VIRTUAL_ENV": "${sourceDir}/.venv"
+      }
+    },
+    {
+      "name": "windows-base",
+      "inherits": "base",
+      "hidden": true,
+      "generator": "Visual Studio 17 2022",
+      "condition": {
+        "type": "equals",
+        "lhs": "${hostSystemName}",
+        "rhs": "Windows"
+      },
+      "vendor": {
+        "microsoft.com/VisualStudioSettings/CMake/1.0": {
+          "hostOS": [
+            "Windows"
+          ]
+        }
+      }
+    },
+    {
+      "name": "windows-x64-base",
+      "inherits": "windows-base",
+      "hidden": true
+    },
+    {
+      "name": "linux-base",
+      "inherits": "base",
+      "hidden": true,
+      "condition": {
+        "type": "equals",
+        "lhs": "${hostSystemName}",
+        "rhs": "Linux"
+      }
+    },
+    {
+      "name": "windows-msvc2022-x64-base",
+      "inherits": "windows-x64-base",
+      "hidden": true,
+      "cacheVariables": {
+        "CMAKE_PREFIX_PATH": "C:/Qt/6.9.2/msvc2022_64"
+      }
+    },
+    {
+      "name": "windows-msvc2022-x64-ninja-base",
+      "inherits": "windows-msvc2022-x64-base",
+      "hidden": true,
+      "generator": "Ninja",
+      "cacheVariables": {
+        "CMAKE_PREFIX_PATH": "C:/Qt/6.9.2/msvc2022_64"
+      }
+    },
+    {
+      "name": "linux-gcc-base",
+      "inherits": "linux-base",
+      "hidden": true,
+      "cacheVariables": {
+        "CMAKE_PREFIX_PATH": "/opt/Qt/6.9.2/gcc_64"
+      },
+      "environment": {
+        "CC": "gcc-11",
+        "CXX": "g++-11"
+      }
+    },
+    {
+      "name": "windows-msvc2022-x64-debug",
+      "inherits": "windows-msvc2022-x64-base",
+      "displayName": "Windows MSVC 2022 x64 Debug",
+      "architecture": {
+        "value": "x64",
+        "strategy": "external"
+      },
+      "cacheVariables": {
+        "CMAKE_BUILD_TYPE": "Debug",
+        "CONAN_HOST_PROFILE": "scwx-windows_msvc2022_x64-debug",
+        "CONAN_BUILD_PROFILE": "scwx-windows_msvc2022_x64-debug"
+      }
+    },
+    {
+      "name": "windows-msvc2022-x64-release",
+      "inherits": "windows-msvc2022-x64-base",
+      "displayName": "Windows MSVC 2022 x64 Release",
+      "architecture": {
+        "value": "x64",
+        "strategy": "external"
+      },
+      "cacheVariables": {
+        "CMAKE_BUILD_TYPE": "Release",
+        "CONAN_HOST_PROFILE": "scwx-windows_msvc2022_x64",
+        "CONAN_BUILD_PROFILE": "scwx-windows_msvc2022_x64"
+      }
+    },
+    {
+      "name": "windows-msvc2022-x64-ninja-debug",
+      "inherits": "windows-msvc2022-x64-ninja-base",
+      "displayName": "Windows MSVC 2022 x64 Ninja Debug",
+      "cacheVariables": {
+        "CMAKE_BUILD_TYPE": "Debug",
+        "CONAN_HOST_PROFILE": "scwx-windows_msvc2022_x64-debug",
+        "CONAN_BUILD_PROFILE": "scwx-windows_msvc2022_x64-debug"
+      }
+    },
+    {
+      "name": "windows-msvc2022-x64-ninja-release",
+      "inherits": "windows-msvc2022-x64-ninja-base",
+      "displayName": "Windows MSVC 2022 x64 Ninja Release",
+      "cacheVariables": {
+        "CMAKE_BUILD_TYPE": "Release",
+        "CONAN_HOST_PROFILE": "scwx-windows_msvc2022_x64",
+        "CONAN_BUILD_PROFILE": "scwx-windows_msvc2022_x64"
+      }
+    },
+    {
+      "name": "linux-gcc-debug",
+      "inherits": "linux-gcc-base",
+      "displayName": "Linux GCC Debug",
+      "cacheVariables": {
+        "CMAKE_BUILD_TYPE": "Debug",
+        "CMAKE_INSTALL_PREFIX": "${sourceDir}/build/${presetName}/Debug/supercell-wx",
+        "CONAN_HOST_PROFILE": "scwx-linux_gcc-11-debug",
+        "CONAN_BUILD_PROFILE": "scwx-linux_gcc-11-debug"
+      }
+    },
+    {
+      "name": "linux-gcc-release",
+      "inherits": "linux-gcc-base",
+      "displayName": "Linux GCC Release",
+      "cacheVariables": {
+        "CMAKE_BUILD_TYPE": "Release",
+        "CMAKE_INSTALL_PREFIX": "${sourceDir}/build/${presetName}/Release/supercell-wx",
+        "CONAN_HOST_PROFILE": "scwx-linux_gcc-11",
+        "CONAN_BUILD_PROFILE": "scwx-linux_gcc-11"
+      }
+    },
+    {
+      "name": "linux-gcc-debug-asan",
+      "inherits": "linux-gcc-base",
+      "displayName": "Linux GCC Debug Address Sanitizer",
+      "cacheVariables": {
+        "CMAKE_BUILD_TYPE": "Debug",
+        "CMAKE_INSTALL_PREFIX": "${sourceDir}/build/${presetName}/Debug/supercell-wx",
+        "CONAN_HOST_PROFILE": "scwx-linux_gcc-11-debug",
+        "CONAN_BUILD_PROFILE": "scwx-linux_gcc-11-debug",
+        "SCWX_ADDRESS_SANITIZER": {
+          "type": "BOOL",
+          "value": "ON"
+        }
+      }
+    },
+    {
+      "name": "linux-gcc-release-asan",
+      "inherits": "linux-gcc-base",
+      "displayName": "Linux GCC Release Address Sanitizer",
+      "cacheVariables": {
+        "CMAKE_BUILD_TYPE": "Release",
+        "CMAKE_INSTALL_PREFIX": "${sourceDir}/build/${presetName}/Release/supercell-wx",
+        "CONAN_HOST_PROFILE": "scwx-linux_gcc-11",
+        "CONAN_BUILD_PROFILE": "scwx-linux_gcc-11",
+        "SCWX_ADDRESS_SANITIZER": {
+          "type": "BOOL",
+          "value": "ON"
+        }
+      }
+    },
+    {
+      "name": "ci-linux-gcc14",
+      "inherits": "linux-gcc-base",
+      "displayName": "CI Linux GCC 14",
+      "cacheVariables": {
+        "CMAKE_BUILD_TYPE": "Release",
+        "CONAN_HOST_PROFILE": "scwx-linux_gcc-14",
+        "CONAN_BUILD_PROFILE": "scwx-linux_gcc-14"
+      },
+      "environment": {
+        "CC": "gcc-14",
+        "CXX": "g++-14"
+      }
+    },
+    {
+      "name": "ci-linux-clang17",
+      "inherits": "linux-gcc-base",
+      "displayName": "CI Linux Clang 17",
+      "cacheVariables": {
+        "CMAKE_BUILD_TYPE": "Release",
+        "CONAN_HOST_PROFILE": "scwx-linux_clang-17",
+        "CONAN_BUILD_PROFILE": "scwx-linux_clang-17"
+      },
+      "environment": {
+        "CC": "clang-17",
+        "CXX": "clang++-17"
+      }
+    },
+    {
+      "name": "ci-linux-gcc-arm64",
+      "inherits": "linux-gcc-base",
+      "displayName": "CI Linux GCC ARM64",
+      "cacheVariables": {
+        "CMAKE_BUILD_TYPE": "Release",
+        "CMAKE_PREFIX_PATH": "/opt/Qt/6.9.2/gcc_arm64",
+        "CONAN_HOST_PROFILE": "scwx-linux_gcc-11_armv8",
+        "CONAN_BUILD_PROFILE": "scwx-linux_gcc-11_armv8"
+      },
+      "environment": {
+        "CC": "gcc-11",
+        "CXX": "g++-11"
+      }
+    },
+    {
+      "name": "macos-base",
+      "inherits": "base",
+      "hidden": true,
+      "cacheVariables": {
+        "CMAKE_PREFIX_PATH": "$env{HOME}/Qt/6.9.2/macos"
+      },
+      "condition": {
+        "type": "equals",
+        "lhs": "${hostSystemName}",
+        "rhs": "Darwin"
+      }
+    },
+    {
+      "name": "macos-clang18-base",
+      "inherits": "macos-base",
+      "hidden": true
+    },
+    {
+      "name": "macos-clang18-x64-base",
+      "inherits": "macos-clang18-base",
+      "hidden": true,
+      "environment": {
+        "CC": "/usr/local/opt/llvm@18/bin/clang",
+        "CXX": "/usr/local/opt/llvm@18/bin/clang++",
+        "PATH": "/usr/local/opt/llvm@18/bin:$penv{PATH}",
+        "CPPFLAGS": "-I/usr/local/opt/llvm@18/include",
+        "LDFLAGS": "-L/usr/local/opt/llvm@18/lib -L/usr/local/opt/llvm@18/lib/c++"
+      }
+    },
+    {
+      "name": "macos-clang18-arm64-base",
+      "inherits": "macos-clang18-base",
+      "hidden": true,
+      "environment": {
+        "CC": "/opt/homebrew/opt/llvm@18/bin/clang",
+        "CXX": "/opt/homebrew/opt/llvm@18/bin/clang++",
+        "PATH": "/opt/homebrew/opt/llvm@18/bin:$penv{PATH}",
+        "CPPFLAGS": "-I/opt/homebrew/opt/llvm@18/include",
+        "LDFLAGS": "-L/opt/homebrew/opt/llvm@18/lib -L/opt/homebrew/opt/llvm@18/lib/c++"
+      }
+    },
+    {
+      "name": "macos-clang18-x64-debug",
+      "inherits": "macos-clang18-x64-base",
+      "displayName": "macOS Clang 18 x64 Debug",
+      "cacheVariables": {
+        "CMAKE_BUILD_TYPE": "Debug",
+        "CONAN_HOST_PROFILE": "scwx-macos_clang-18-debug",
+        "CONAN_BUILD_PROFILE": "scwx-macos_clang-18-debug"
+      }
+    },
+    {
+      "name": "macos-clang18-x64-release",
+      "inherits": "macos-clang18-x64-base",
+      "displayName": "macOS Clang 18 x64 Release",
+      "cacheVariables": {
+        "CMAKE_BUILD_TYPE": "Release",
+        "CONAN_HOST_PROFILE": "scwx-macos_clang-18",
+        "CONAN_BUILD_PROFILE": "scwx-macos_clang-18"
+      }
+    },
+    {
+      "name": "macos-clang18-arm64-debug",
+      "inherits": "macos-clang18-arm64-base",
+      "displayName": "macOS Clang 18 Arm64 Debug",
+      "cacheVariables": {
+        "CMAKE_BUILD_TYPE": "Debug",
+        "CONAN_HOST_PROFILE": "scwx-macos_clang-18_armv8-debug",
+        "CONAN_BUILD_PROFILE": "scwx-macos_clang-18_armv8-debug"
+      }
+    },
+    {
+      "name": "macos-clang18-arm64-release",
+      "inherits": "macos-clang18-arm64-base",
+      "displayName": "macOS Clang 18 Arm64 Release",
+      "cacheVariables": {
+        "CMAKE_BUILD_TYPE": "Release",
+        "CONAN_HOST_PROFILE": "scwx-macos_clang-18_armv8",
+        "CONAN_BUILD_PROFILE": "scwx-macos_clang-18_armv8"
+      }
+    }
+  ],
+  "buildPresets": [
+    {
+      "name": "windows-msvc2022-x64-debug",
+      "configurePreset": "windows-msvc2022-x64-debug",
+      "displayName": "Windows MSVC 2022 x64 Debug",
+      "configuration": "Debug"
+    },
+    {
+      "name": "windows-msvc2022-x64-release",
+      "configurePreset": "windows-msvc2022-x64-release",
+      "displayName": "Windows MSVC 2022 x64 Release",
+      "configuration": "Release"
+    },
+    {
+      "name": "linux-gcc-debug",
+      "configurePreset": "linux-gcc-debug",
+      "displayName": "Linux GCC Debug",
+      "configuration": "Debug"
+    },
+    {
+      "name": "linux-gcc-release",
+      "configurePreset": "linux-gcc-release",
+      "displayName": "Linux GCC Release",
+      "configuration": "Release"
+    },
+    {
+      "name": "macos-clang18-x64-debug",
+      "configurePreset": "macos-clang18-x64-debug",
+      "displayName": "macOS Clang 18 x64 Debug",
+      "configuration": "Debug"
+    },
+    {
+      "name": "macos-clang18-x64-release",
+      "configurePreset": "macos-clang18-x64-release",
+      "displayName": "macOS Clang 18 x64 Release",
+      "configuration": "Release"
+    },
+    {
+      "name": "macos-clang18-arm64-debug",
+      "configurePreset": "macos-clang18-arm64-debug",
+      "displayName": "macOS Clang 18 Arm64 Debug",
+      "configuration": "Debug"
+    },
+    {
+      "name": "macos-clang18-arm64-release",
+      "configurePreset": "macos-clang18-arm64-release",
+      "displayName": "macOS Clang 18 Arm64 Release",
+      "configuration": "Release"
+    }
+  ],
+  "testPresets": [
+    {
+      "name": "windows-msvc2022-x64-debug",
+      "configurePreset": "windows-msvc2022-x64-debug",
+      "displayName": "Windows MSVC 2022 x64 Debug",
+      "configuration": "Debug"
+    },
+    {
+      "name": "windows-msvc2022-x64-release",
+      "configurePreset": "windows-msvc2022-x64-release",
+      "displayName": "Windows MSVC 2022 x64 Release",
+      "configuration": "Release"
+    },
+    {
+      "name": "linux-gcc-debug",
+      "configurePreset": "linux-gcc-debug",
+      "displayName": "Linux GCC Debug",
+      "configuration": "Debug"
+    },
+    {
+      "name": "linux-gcc-release",
+      "configurePreset": "linux-gcc-release",
+      "displayName": "Linux GCC Release",
+      "configuration": "Release"
+    }
+  ]
+}
diff --git a/LICENSE.txt b/LICENSE.txt
index 8c9c5fbd..799086a0 100644
--- a/LICENSE.txt
+++ b/LICENSE.txt
@@ -1,6 +1,6 @@
 The MIT License (MIT)
 
-Copyright (c) 2021-2024 Dan Paulat
+Copyright (c) 2021-2025 Dan Paulat
 
 Permission is hereby granted, free of charge, to any person obtaining a copy
 of this software and associated documentation files (the "Software"), to deal
diff --git a/README.md b/README.md
index 321c33f2..81b0e698 100644
--- a/README.md
+++ b/README.md
@@ -26,32 +26,36 @@ Supercell Wx supports the following 64-bit operating systems:
   - Fedora Linux 34+
   - openSUSE Tumbleweed
   - Ubuntu 22.04+
+  - NixOS 25.05+
   - Most distributions supporting the GCC Standard C++ Library 11+
- 
+- macOS
+  - 13.6+ for Intel-based Macs
+  - 14.0+ for Apple silicon-based Macs
+
 ## Linux Dependencies
 
 Supercell Wx requires the following Linux dependencies:
 
-- Linux/X11 (Wayland works too) with support for GCC 11 and OpenGL 3.3
+- Linux/X11 (Wayland works too) with support for GCC 11, OpenGL 3.3 and OpenGL ES 3.0
 - X11/XCB libraries including xcb-cursor
- 
+
 ## FAQ
 
 Frequently asked questions:
 
 - Q: Why is the map black when loading for the first time?
-  
+
   - A. You must obtain a free API key from either (or both) [MapTiler](https://cloud.maptiler.com/auth/widget?next=https://cloud.maptiler.com/maps/) which currently does not require a credit/debit card, or [Mapbox](https://account.mapbox.com/) which ***does*** require a credit/debit card, but as of writing, you will receive 200K free requests per month, which should be sufficient for an individual user.
 
 - Q: Why is it that when I change my color table, API key, grid width/height settings, nothing happens after hitting apply?
 
   - A. As of right now, you must restart Supercell Wx in order to apply these changes. In future iterations, this will no longer be an issue.
-   
+
 - Q. Is it possible to get dark mode?
- 
+
   - A. In Windows, make sure to set the flag `-style fusion` at the end of the target path of the .exe
     - Example: `C:\Users\Administrator\Desktop\Supercell-Wx\bin\supercell-wx.exe -style fusion`
   - A. In Linux, if you're using KDE, Supercell Wx should automatically follow your theme settings.
-    
+
 - Q: How can I contribute?
   - A. Head to [Developer Setup](https://supercell-wx.readthedocs.io/en/stable/development/developer-setup.html) and [Contributing](CONTRIBUTING.md) to configure the Supercell Wx development environment for your IDE. Currently Visual Studio and Visual Studio Code are recommended, with other IDEs remaining untested at this time.
diff --git a/conanfile.py b/conanfile.py
index b18f2d31..6803a56c 100644
--- a/conanfile.py
+++ b/conanfile.py
@@ -1,37 +1,66 @@
-from conans import ConanFile
+from conan import ConanFile
+from conan.tools.cmake import CMake
+from conan.tools.files import copy
+import os
 
 class SupercellWxConan(ConanFile):
     settings   = ("os", "compiler", "build_type", "arch")
-    requires   = ("boost/1.85.0",
-                  "cpr/1.10.5",
+    requires   = ("boost/1.88.0",
+                  "cpr/1.12.0",
                   "fontconfig/2.15.0",
                   "freetype/2.13.2",
-                  "geographiclib/2.3",
-                  "geos/3.12.2",
-                  "glew/2.2.0",
-                  "glm/cci.20230113",
-                  "gtest/1.15.0",
-                  "libcurl/8.9.1",
-                  "libxml2/2.12.7",
-                  "openssl/3.3.1",
-                  "re2/20240702",
-                  "spdlog/1.14.1",
-                  "sqlite3/3.46.0",
-                  "vulkan-loader/1.3.243.0",
+                  "geographiclib/2.4",
+                  "geos/3.13.0",
+                  "glm/1.0.1",
+                  "gtest/1.17.0",
+                  "libcurl/8.12.1",
+                  "libpng/1.6.50",
+                  "libxml2/2.14.5",
+                  "openssl/3.5.0",
+                  "range-v3/0.12.0",
+                  "re2/20250722",
+                  "spdlog/1.15.1",
+                  "sqlite3/3.49.1",
+                  "vulkan-loader/1.3.290.0",
                   "zlib/1.3.1")
-    generators = ("cmake",
-                  "cmake_find_package",
-                  "cmake_paths")
-    default_options = {"geos:shared"      : True,
-                       "libiconv:shared"  : True,
-                       "openssl:no_module": True,
-                       "openssl:shared"   : True}
+    generators = ("CMakeDeps")
+    default_options = {"geos/*:shared"     : True,
+                       "libiconv/*:shared" : True}
+
+    def configure(self):
+        if self.settings.os == "Windows":
+            self.options["libcurl"].with_ssl = "schannel"
+        elif self.settings.os == "Linux":
+            self.options["openssl"].shared    = True
+            self.options["libcurl"].ca_bundle = "none"
+            self.options["libcurl"].ca_path   = "none"
+        elif self.settings.os == "Macos":
+            self.options["openssl"].shared    = True
+            self.options["libcurl"].ca_bundle = "none"
+            self.options["libcurl"].ca_path   = "none"
 
     def requirements(self):
         if self.settings.os == "Linux":
-            self.requires("onetbb/2021.12.0")
+            self.requires("mesa-glu/9.0.3")
+            self.requires("onetbb/2022.2.0")
 
-    def imports(self):
-        self.copy("*.dll", dst="bin", src="bin")
-        self.copy("*.dylib", dst="bin", src="lib")
-        self.copy("license*", dst="licenses", src=".", folder=True, ignore_case=True)
+    def generate(self):
+        build_folder = os.path.join(self.build_folder,
+                                    "..",
+                                    str(self.settings.build_type),
+                                    self.cpp_info.bindirs[0])
+
+        for dep in self.dependencies.values():
+            if dep.cpp_info.bindirs:
+                copy(self, "*.dll", dep.cpp_info.bindirs[0], build_folder)
+            if dep.cpp_info.libdirs:
+                copy(self, "*.dylib", dep.cpp_info.libdirs[0], build_folder)
+
+    def build(self):
+        cmake = CMake(self)
+        cmake.configure()
+        cmake.build()
+
+    def package(self):
+        cmake = CMake(self)
+        cmake.install()
diff --git a/data b/data
index 8eb89b19..fd72b32c 160000
--- a/data
+++ b/data
@@ -1 +1 @@
-Subproject commit 8eb89b19fdd1c78e896cc6cb47e07425bb473699
+Subproject commit fd72b32cc12419b4a9c9a72487e58ffa04fb2a70
diff --git a/external/CMakeLists.txt b/external/CMakeLists.txt
index bbc76c64..1039e96e 100644
--- a/external/CMakeLists.txt
+++ b/external/CMakeLists.txt
@@ -1,4 +1,4 @@
-cmake_minimum_required(VERSION 3.20)
+cmake_minimum_required(VERSION 3.24)
 set(PROJECT_NAME scwx-external)
 
 set_property(DIRECTORY
@@ -6,18 +6,22 @@ set_property(DIRECTORY
              PROPERTY CMAKE_CONFIGURE_DEPENDS
              aws-sdk-cpp.cmake
              date.cmake
+             glad.cmake
              hsluv-c.cmake
              imgui.cmake
              maplibre-native-qt.cmake
              stb.cmake
              textflowcpp.cmake
-             units.cmake)
+             units.cmake
+             qt6ct.cmake)
 
 include(aws-sdk-cpp.cmake)
 include(date.cmake)
+include(glad.cmake)
 include(hsluv-c.cmake)
 include(imgui.cmake)
 include(maplibre-native-qt.cmake)
 include(stb.cmake)
 include(textflowcpp.cmake)
 include(units.cmake)
+include(qt6ct.cmake)
diff --git a/external/aws-sdk-cpp b/external/aws-sdk-cpp
index d5eb42fe..8d31e042 160000
--- a/external/aws-sdk-cpp
+++ b/external/aws-sdk-cpp
@@ -1 +1 @@
-Subproject commit d5eb42fe7c632868d4535b454ee2e5ba0e349b7f
+Subproject commit 8d31e042f950fe70924391a205cceaf342ecec00
diff --git a/external/aws-sdk-cpp.cmake b/external/aws-sdk-cpp.cmake
index 1ba641db..f88df9ca 100644
--- a/external/aws-sdk-cpp.cmake
+++ b/external/aws-sdk-cpp.cmake
@@ -1,4 +1,4 @@
-cmake_minimum_required(VERSION 3.20)
+cmake_minimum_required(VERSION 3.24)
 set(PROJECT_NAME scwx-aws-sdk-cpp)
 
 set(AWS_SDK_WARNINGS_ARE_ERRORS OFF)
@@ -21,6 +21,10 @@ set(MINIMIZE_SIZE      OFF  CACHE BOOL   "If enabled, the SDK will be built via
 # Save off ${CMAKE_CXX_FLAGS} before modifying compiler settings
 set(CMAKE_CXX_FLAGS_PREV "${CMAKE_CXX_FLAGS}")
 
+# Configure OpenSSL crypto library
+find_package(OpenSSL)
+add_library(crypto ALIAS OpenSSL::Crypto)
+
 # Fix CMake errors for internal variables not set
 include(aws-sdk-cpp/cmake/compiler_settings.cmake)
 set_msvc_warnings()
diff --git a/external/cmake-conan b/external/cmake-conan
index b240c809..b0e4d1ec 160000
--- a/external/cmake-conan
+++ b/external/cmake-conan
@@ -1 +1 @@
-Subproject commit b240c809b5ea097077fc8222cad71d2329288e48
+Subproject commit b0e4d1ec08edb35ef31033938567d621f6643c17
diff --git a/external/date b/external/date
index cc4685a2..a5db3aec 160000
--- a/external/date
+++ b/external/date
@@ -1 +1 @@
-Subproject commit cc4685a21e4a4fdae707ad1233c61bbaff241f93
+Subproject commit a5db3aecec580bc78b6c01c118f2628676769b69
diff --git a/external/date.cmake b/external/date.cmake
index a804f68c..7fce7ffc 100644
--- a/external/date.cmake
+++ b/external/date.cmake
@@ -1,4 +1,4 @@
-cmake_minimum_required(VERSION 3.20)
+cmake_minimum_required(VERSION 3.24)
 set(PROJECT_NAME scwx-date)
 
 set(USE_SYSTEM_TZ_DB ON)
diff --git a/external/glad b/external/glad
new file mode 160000
index 00000000..73db193f
--- /dev/null
+++ b/external/glad
@@ -0,0 +1 @@
+Subproject commit 73db193f853e2ee079bf3ca8a64aa2eaf6459043
diff --git a/external/glad.cmake b/external/glad.cmake
new file mode 100644
index 00000000..59ceb179
--- /dev/null
+++ b/external/glad.cmake
@@ -0,0 +1,11 @@
+cmake_minimum_required(VERSION 3.24)
+set(PROJECT_NAME scwx-glad)
+
+# Path to glad directory
+set(GLAD_SOURCES_DIR "${CMAKE_CURRENT_SOURCE_DIR}/glad/")
+
+# Path to glad CMake files
+add_subdirectory("${GLAD_SOURCES_DIR}/cmake" glad_cmake)
+
+# Specify glad settings
+glad_add_library(glad_gl_core_33 LOADER REPRODUCIBLE API gl:core=3.3)
diff --git a/external/hsluv-c b/external/hsluv-c
index 59539e04..982217c6 160000
--- a/external/hsluv-c
+++ b/external/hsluv-c
@@ -1 +1 @@
-Subproject commit 59539e04a6fa648935cbe57c2104041f23136c4a
+Subproject commit 982217c65a9ff574302335177d2dc078d9bfa6f5
diff --git a/external/hsluv-c.cmake b/external/hsluv-c.cmake
index 0129d39d..eec8f0f2 100644
--- a/external/hsluv-c.cmake
+++ b/external/hsluv-c.cmake
@@ -1,4 +1,4 @@
-cmake_minimum_required(VERSION 3.20)
+cmake_minimum_required(VERSION 3.24)
 set(PROJECT_NAME scwx-hsluv-c)
 
 set(HSLUV_C_TESTS OFF)
diff --git a/external/imgui b/external/imgui
index 6ccc561a..45acd5e0 160000
--- a/external/imgui
+++ b/external/imgui
@@ -1 +1 @@
-Subproject commit 6ccc561a2ab497ad4ae6ee1dbd3b992ffada35cb
+Subproject commit 45acd5e0e82f4c954432533ae9985ff0e1aad6d5
diff --git a/external/imgui-backend-qt b/external/imgui-backend-qt
index 0fe974eb..023345ca 160000
--- a/external/imgui-backend-qt
+++ b/external/imgui-backend-qt
@@ -1 +1 @@
-Subproject commit 0fe974ebd037844c9f23d6325dbcc128e9973749
+Subproject commit 023345ca8abf731fc50568c0197ceebe76bb4324
diff --git a/external/imgui.cmake b/external/imgui.cmake
index 443817ef..3eba6ee0 100644
--- a/external/imgui.cmake
+++ b/external/imgui.cmake
@@ -1,4 +1,4 @@
-cmake_minimum_required(VERSION 3.20)
+cmake_minimum_required(VERSION 3.24)
 set(PROJECT_NAME scwx-imgui)
 
 find_package(QT NAMES Qt6
@@ -12,7 +12,7 @@ find_package(Qt${QT_VERSION_MAJOR}
              
 find_package(Freetype)
 
-set(IMGUI_SOURCES imgui/imconfig.h
+set(IMGUI_SOURCES include/scwx/external/imgui/imconfig.h
                   imgui/imgui.cpp
                   imgui/imgui.h
                   imgui/imgui_demo.cpp
@@ -33,8 +33,9 @@ set(IMGUI_SOURCES imgui/imconfig.h
 add_library(imgui STATIC ${IMGUI_SOURCES})
 
 target_include_directories(imgui PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/imgui)
+target_include_directories(imgui PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include)
 
-target_compile_definitions(imgui PRIVATE IMGUI_ENABLE_FREETYPE)
+target_compile_definitions(imgui PUBLIC IMGUI_USER_CONFIG=)
 
 target_link_libraries(imgui PRIVATE Qt${QT_VERSION_MAJOR}::Widgets
                                     Freetype::Freetype)
diff --git a/external/include/scwx/external/imgui/imconfig.h b/external/include/scwx/external/imgui/imconfig.h
new file mode 100644
index 00000000..ac954a77
--- /dev/null
+++ b/external/include/scwx/external/imgui/imconfig.h
@@ -0,0 +1,147 @@
+// clang-format off
+//-----------------------------------------------------------------------------
+// DEAR IMGUI COMPILE-TIME OPTIONS
+// Runtime options (clipboard callbacks, enabling various features, etc.) can generally be set via the ImGuiIO structure.
+// You can use ImGui::SetAllocatorFunctions() before calling ImGui::CreateContext() to rewire memory allocation functions.
+//-----------------------------------------------------------------------------
+// A) You may edit imconfig.h (and not overwrite it when updating Dear ImGui, or maintain a patch/rebased branch with your modifications to it)
+// B) or '#define IMGUI_USER_CONFIG "my_imgui_config.h"' in your project and then add directives in your own file without touching this template.
+//-----------------------------------------------------------------------------
+// You need to make sure that configuration settings are defined consistently _everywhere_ Dear ImGui is used, which include the imgui*.cpp
+// files but also _any_ of your code that uses Dear ImGui. This is because some compile-time options have an affect on data structures.
+// Defining those options in imconfig.h will ensure every compilation unit gets to see the same data structure layouts.
+// Call IMGUI_CHECKVERSION() from your .cpp file to verify that the data structures your files are using are matching the ones imgui.cpp is using.
+//-----------------------------------------------------------------------------
+
+#pragma once
+
+//---- Define assertion handler. Defaults to calling assert().
+// If your macro uses multiple statements, make sure is enclosed in a 'do { .. } while (0)' block so it can be used as a single statement.
+//#define IM_ASSERT(_EXPR)  MyAssert(_EXPR)
+//#define IM_ASSERT(_EXPR)  ((void)(_EXPR))     // Disable asserts
+
+//---- Define attributes of all API symbols declarations, e.g. for DLL under Windows
+// Using Dear ImGui via a shared library is not recommended, because of function call overhead and because we don't guarantee backward nor forward ABI compatibility.
+// - Windows DLL users: heaps and globals are not shared across DLL boundaries! You will need to call SetCurrentContext() + SetAllocatorFunctions()
+//   for each static/DLL boundary you are calling from. Read "Context and Memory Allocators" section of imgui.cpp for more details.
+//#define IMGUI_API __declspec(dllexport)                   // MSVC Windows: DLL export
+//#define IMGUI_API __declspec(dllimport)                   // MSVC Windows: DLL import
+//#define IMGUI_API __attribute__((visibility("default")))  // GCC/Clang: override visibility when set is hidden
+
+//---- Don't define obsolete functions/enums/behaviors. Consider enabling from time to time after updating to clean your code of obsolete function/names.
+#define IMGUI_DISABLE_OBSOLETE_FUNCTIONS
+
+//---- Disable all of Dear ImGui or don't implement standard windows/tools.
+// It is very strongly recommended to NOT disable the demo windows and debug tool during development. They are extremely useful in day to day work. Please read comments in imgui_demo.cpp.
+//#define IMGUI_DISABLE                                     // Disable everything: all headers and source files will be empty.
+//#define IMGUI_DISABLE_DEMO_WINDOWS                        // Disable demo windows: ShowDemoWindow()/ShowStyleEditor() will be empty.
+//#define IMGUI_DISABLE_DEBUG_TOOLS                         // Disable metrics/debugger and other debug tools: ShowMetricsWindow(), ShowDebugLogWindow() and ShowIDStackToolWindow() will be empty.
+
+//---- Don't implement some functions to reduce linkage requirements.
+//#define IMGUI_DISABLE_WIN32_DEFAULT_CLIPBOARD_FUNCTIONS   // [Win32] Don't implement default clipboard handler. Won't use and link with OpenClipboard/GetClipboardData/CloseClipboard etc. (user32.lib/.a, kernel32.lib/.a)
+//#define IMGUI_ENABLE_WIN32_DEFAULT_IME_FUNCTIONS          // [Win32] [Default with Visual Studio] Implement default IME handler (require imm32.lib/.a, auto-link for Visual Studio, -limm32 on command-line for MinGW)
+//#define IMGUI_DISABLE_WIN32_DEFAULT_IME_FUNCTIONS         // [Win32] [Default with non-Visual Studio compilers] Don't implement default IME handler (won't require imm32.lib/.a)
+//#define IMGUI_DISABLE_WIN32_FUNCTIONS                     // [Win32] Won't use and link with any Win32 function (clipboard, IME).
+//#define IMGUI_ENABLE_OSX_DEFAULT_CLIPBOARD_FUNCTIONS      // [OSX] Implement default OSX clipboard handler (need to link with '-framework ApplicationServices', this is why this is not the default).
+//#define IMGUI_DISABLE_DEFAULT_SHELL_FUNCTIONS             // Don't implement default platform_io.Platform_OpenInShellFn() handler (Win32: ShellExecute(), require shell32.lib/.a, Mac/Linux: use system("")).
+//#define IMGUI_DISABLE_DEFAULT_FORMAT_FUNCTIONS            // Don't implement ImFormatString/ImFormatStringV so you can implement them yourself (e.g. if you don't want to link with vsnprintf)
+//#define IMGUI_DISABLE_DEFAULT_MATH_FUNCTIONS              // Don't implement ImFabs/ImSqrt/ImPow/ImFmod/ImCos/ImSin/ImAcos/ImAtan2 so you can implement them yourself.
+//#define IMGUI_DISABLE_FILE_FUNCTIONS                      // Don't implement ImFileOpen/ImFileClose/ImFileRead/ImFileWrite and ImFileHandle at all (replace them with dummies)
+//#define IMGUI_DISABLE_DEFAULT_FILE_FUNCTIONS              // Don't implement ImFileOpen/ImFileClose/ImFileRead/ImFileWrite and ImFileHandle so you can implement them yourself if you don't want to link with fopen/fclose/fread/fwrite. This will also disable the LogToTTY() function.
+//#define IMGUI_DISABLE_DEFAULT_ALLOCATORS                  // Don't implement default allocators calling malloc()/free() to avoid linking with them. You will need to call ImGui::SetAllocatorFunctions().
+//#define IMGUI_DISABLE_DEFAULT_FONT                        // Disable default embedded font (ProggyClean.ttf), remove ~9.5 KB from output binary. AddFontDefault() will assert.
+//#define IMGUI_DISABLE_SSE                                 // Disable use of SSE intrinsics even if available
+
+//---- Enable Test Engine / Automation features.
+//#define IMGUI_ENABLE_TEST_ENGINE                          // Enable imgui_test_engine hooks. Generally set automatically by include "imgui_te_config.h", see Test Engine for details.
+
+//---- Include imgui_user.h at the end of imgui.h as a convenience
+// May be convenient for some users to only explicitly include vanilla imgui.h and have extra stuff included.
+//#define IMGUI_INCLUDE_IMGUI_USER_H
+//#define IMGUI_USER_H_FILENAME         "my_folder/my_imgui_user.h"
+
+//---- Pack vertex colors as BGRA8 instead of RGBA8 (to avoid converting from one to another). Need dedicated backend support.
+//#define IMGUI_USE_BGRA_PACKED_COLOR
+
+//---- Use legacy CRC32-adler tables (used before 1.91.6), in order to preserve old .ini data that you cannot afford to invalidate.
+//#define IMGUI_USE_LEGACY_CRC32_ADLER
+
+//---- Use 32-bit for ImWchar (default is 16-bit) to support Unicode planes 1-16. (e.g. point beyond 0xFFFF like emoticons, dingbats, symbols, shapes, ancient languages, etc...)
+//#define IMGUI_USE_WCHAR32
+
+//---- Avoid multiple STB libraries implementations, or redefine path/filenames to prioritize another version
+// By default the embedded implementations are declared static and not available outside of Dear ImGui sources files.
+//#define IMGUI_STB_TRUETYPE_FILENAME   "my_folder/stb_truetype.h"
+//#define IMGUI_STB_RECT_PACK_FILENAME  "my_folder/stb_rect_pack.h"
+//#define IMGUI_STB_SPRINTF_FILENAME    "my_folder/stb_sprintf.h"    // only used if IMGUI_USE_STB_SPRINTF is defined.
+//#define IMGUI_DISABLE_STB_TRUETYPE_IMPLEMENTATION
+//#define IMGUI_DISABLE_STB_RECT_PACK_IMPLEMENTATION
+//#define IMGUI_DISABLE_STB_SPRINTF_IMPLEMENTATION                   // only disabled if IMGUI_USE_STB_SPRINTF is defined.
+
+//---- Use stb_sprintf.h for a faster implementation of vsnprintf instead of the one from libc (unless IMGUI_DISABLE_DEFAULT_FORMAT_FUNCTIONS is defined)
+// Compatibility checks of arguments and formats done by clang and GCC will be disabled in order to support the extra formats provided by stb_sprintf.h.
+//#define IMGUI_USE_STB_SPRINTF
+
+//---- Use FreeType to build and rasterize the font atlas (instead of stb_truetype which is embedded by default in Dear ImGui)
+// Requires FreeType headers to be available in the include path. Requires program to be compiled with 'misc/freetype/imgui_freetype.cpp' (in this repository) + the FreeType library (not provided).
+// On Windows you may use vcpkg with 'vcpkg install freetype --triplet=x64-windows' + 'vcpkg integrate install'.
+#define IMGUI_ENABLE_FREETYPE
+
+//---- Use FreeType + plutosvg or lunasvg to render OpenType SVG fonts (SVGinOT)
+// Only works in combination with IMGUI_ENABLE_FREETYPE.
+// - plutosvg is currently easier to install, as e.g. it is part of vcpkg. It will support more fonts and may load them faster. See misc/freetype/README for instructions.
+// - Both require headers to be available in the include path + program to be linked with the library code (not provided).
+// - (note: lunasvg implementation is based on Freetype's rsvg-port.c which is licensed under CeCILL-C Free Software License Agreement)
+//#define IMGUI_ENABLE_FREETYPE_PLUTOSVG
+//#define IMGUI_ENABLE_FREETYPE_LUNASVG
+
+//---- Use stb_truetype to build and rasterize the font atlas (default)
+// The only purpose of this define is if you want force compilation of the stb_truetype backend ALONG with the FreeType backend.
+//#define IMGUI_ENABLE_STB_TRUETYPE
+
+//---- Define constructor and implicit cast operators to convert back<>forth between your math types and ImVec2/ImVec4.
+// This will be inlined as part of ImVec2 and ImVec4 class declarations.
+/*
+#define IM_VEC2_CLASS_EXTRA                                                     \
+        constexpr ImVec2(const MyVec2& f) : x(f.x), y(f.y) {}                   \
+        operator MyVec2() const { return MyVec2(x,y); }
+
+#define IM_VEC4_CLASS_EXTRA                                                     \
+        constexpr ImVec4(const MyVec4& f) : x(f.x), y(f.y), z(f.z), w(f.w) {}   \
+        operator MyVec4() const { return MyVec4(x,y,z,w); }
+*/
+//---- ...Or use Dear ImGui's own very basic math operators.
+//#define IMGUI_DEFINE_MATH_OPERATORS
+
+//---- Use 32-bit vertex indices (default is 16-bit) is one way to allow large meshes with more than 64K vertices.
+// Your renderer backend will need to support it (most example renderer backends support both 16/32-bit indices).
+// Another way to allow large meshes while keeping 16-bit indices is to handle ImDrawCmd::VtxOffset in your renderer.
+// Read about ImGuiBackendFlags_RendererHasVtxOffset for details.
+//#define ImDrawIdx unsigned int
+
+//---- Override ImDrawCallback signature (will need to modify renderer backends accordingly)
+//struct ImDrawList;
+//struct ImDrawCmd;
+//typedef void (*MyImDrawCallback)(const ImDrawList* draw_list, const ImDrawCmd* cmd, void* my_renderer_user_data);
+//#define ImDrawCallback MyImDrawCallback
+
+//---- Debug Tools: Macro to break in Debugger (we provide a default implementation of this in the codebase)
+// (use 'Metrics->Tools->Item Picker' to pick widgets with the mouse and break into them for easy debugging.)
+//#define IM_DEBUG_BREAK  IM_ASSERT(0)
+//#define IM_DEBUG_BREAK  __debugbreak()
+
+//---- Debug Tools: Enable highlight ID conflicts _before_ hovering items. When io.ConfigDebugHighlightIdConflicts is set.
+// (THIS WILL SLOW DOWN DEAR IMGUI. Only use occasionally and disable after use)
+//#define IMGUI_DEBUG_HIGHLIGHT_ALL_ID_CONFLICTS
+
+//---- Debug Tools: Enable slower asserts
+//#define IMGUI_DEBUG_PARANOID
+
+//---- Tip: You can add extra functions within the ImGui:: namespace from anywhere (e.g. your own sources/header files)
+/*
+namespace ImGui
+{
+    void MyFunction(const char* name, MyMatrix44* mtx);
+}
+*/
+// clang-format on
diff --git a/external/maplibre-native b/external/maplibre-native
index 3d4ca3fd..3654f5fa 160000
--- a/external/maplibre-native
+++ b/external/maplibre-native
@@ -1 +1 @@
-Subproject commit 3d4ca3fdf07c50db3002b11bff93c81ec380e493
+Subproject commit 3654f5fa9f06534d7fd2d95b810049a82e5953ef
diff --git a/external/maplibre-native-qt b/external/maplibre-native-qt
index 805ccf62..8b406978 160000
--- a/external/maplibre-native-qt
+++ b/external/maplibre-native-qt
@@ -1 +1 @@
-Subproject commit 805ccf6204a546e43fed599631ad5d698f68ae86
+Subproject commit 8b40697895c19da4479cd037a76608f4c36935e8
diff --git a/external/maplibre-native-qt.cmake b/external/maplibre-native-qt.cmake
index 6a508040..7db42c10 100644
--- a/external/maplibre-native-qt.cmake
+++ b/external/maplibre-native-qt.cmake
@@ -1,4 +1,4 @@
-cmake_minimum_required(VERSION 3.20)
+cmake_minimum_required(VERSION 3.24)
 set(PROJECT_NAME scwx-mln)
 
 set(gtest_disable_pthreads ON)
@@ -19,11 +19,28 @@ if (MSVC)
     target_link_options(MLNQtCore PRIVATE "$<$:/DEBUG>")
     target_link_options(MLNQtCore PRIVATE "$<$:/OPT:REF>")
     target_link_options(MLNQtCore PRIVATE "$<$:/OPT:ICF>")
+
+    # Enable multi-processor compilation
+    target_compile_options(MLNQtCore PRIVATE "/MP")
+    target_compile_options(mbgl-core PRIVATE "/MP")
+    target_compile_options(mbgl-vendor-csscolorparser PRIVATE "/MP")
+    target_compile_options(mbgl-vendor-nunicode PRIVATE "/MP")
+    target_compile_options(mbgl-vendor-parsedate PRIVATE "/MP")
+
+    if (TARGET mbgl-vendor-sqlite)
+        target_compile_options(mbgl-vendor-sqlite PRIVATE "/MP")
+    endif()
 else()
     target_compile_options(mbgl-core PRIVATE "$<$:-g>")
     target_compile_options(MLNQtCore PRIVATE "$<$:-g>")
 endif()
 
+if (APPLE)
+    # Enable GL check error debug
+    target_compile_definitions(mbgl-core PRIVATE MLN_GL_CHECK_ERRORS=1)
+    target_compile_definitions(MLNQtCore PRIVATE MLN_GL_CHECK_ERRORS=1)
+endif()
+
 set(MLN_INCLUDE_DIRS ${CMAKE_CURRENT_SOURCE_DIR}/maplibre-native/include
                      ${CMAKE_CURRENT_SOURCE_DIR}/maplibre-native-qt/src/core/include
                      ${CMAKE_CURRENT_BINARY_DIR}/maplibre-native-qt/src/core/include PARENT_SCOPE)
diff --git a/external/qt6ct b/external/qt6ct
new file mode 160000
index 00000000..2c569c6c
--- /dev/null
+++ b/external/qt6ct
@@ -0,0 +1 @@
+Subproject commit 2c569c6c4776ea5a1299030c079b16f70473c9e6
diff --git a/external/qt6ct.cmake b/external/qt6ct.cmake
new file mode 100644
index 00000000..cdae0b25
--- /dev/null
+++ b/external/qt6ct.cmake
@@ -0,0 +1,62 @@
+cmake_minimum_required(VERSION 3.16.0)
+set(PROJECT_NAME scwx-qt6ct)
+
+find_package(QT NAMES Qt6
+             COMPONENTS Gui Widgets
+             REQUIRED)
+find_package(Qt${QT_VERSION_MAJOR}
+             COMPONENTS Gui Widgets
+             REQUIRED)
+
+#extract version from qt6ct.h
+file(STRINGS "${CMAKE_CURRENT_SOURCE_DIR}/qt6ct/src/qt6ct-common/qt6ct.h"
+     QT6CT_VERSION_DATA REGEX "^#define[ \t]+QT6CT_VERSION_[A-Z]+[ \t]+[0-9]+.*$")
+
+if(QT6CT_VERSION_DATA)
+  foreach(item IN ITEMS MAJOR MINOR)
+    string(REGEX REPLACE ".*#define[ \t]+QT6CT_VERSION_${item}[ \t]+([0-9]+).*"
+       "\\1" QT6CT_VERSION_${item} ${QT6CT_VERSION_DATA})
+  endforeach()
+  set(QT6CT_VERSION "${QT6CT_VERSION_MAJOR}.${QT6CT_VERSION_MINOR}")
+  set(QT6CT_SOVERSION "${QT6CT_VERSION_MAJOR}")
+  message(STATUS "qt6ct version: ${QT6CT_VERSION}")
+else()
+  message(FATAL_ERROR "invalid header")
+endif()
+
+set(qt6ct-common-source
+  qt6ct/src/qt6ct-common/qt6ct.cpp
+)
+
+set(qt6ct-widgets-source
+  qt6ct/src/qt6ct/paletteeditdialog.cpp
+  qt6ct/src/qt6ct/paletteeditdialog.ui
+)
+
+set(CMAKE_AUTOMOC ON)
+set(CMAKE_AUTORCC ON)
+set(CMAKE_AUTOUIC ON)
+
+include_directories(qt6ct/src/qt6ct-common)
+
+add_library(qt6ct-common STATIC ${qt6ct-common-source})
+set_target_properties(qt6ct-common PROPERTIES VERSION ${QT6CT_VERSION})
+target_link_libraries(qt6ct-common PRIVATE Qt6::Gui)
+target_compile_definitions(qt6ct-common PRIVATE QT6CT_LIBRARY)
+
+add_library(qt6ct-widgets STATIC ${qt6ct-widgets-source})
+set_target_properties(qt6ct-widgets PROPERTIES VERSION ${QT6CT_VERSION})
+target_link_libraries(qt6ct-widgets PRIVATE Qt6::Widgets qt6ct-common)
+target_compile_definitions(qt6ct-widgets PRIVATE QT6CT_LIBRARY)
+
+if (MSVC)
+    # Produce PDB file for debug
+    target_compile_options(qt6ct-common PRIVATE "$<$:/Zi>")
+    target_compile_options(qt6ct-widgets PRIVATE "$<$:/Zi>")
+else()
+    target_compile_options(qt6ct-common PRIVATE "$<$:-g>")
+    target_compile_options(qt6ct-widgets PRIVATE "$<$:-g>")
+endif()
+
+target_include_directories( qt6ct-common INTERFACE qt6ct/src )
+target_include_directories( qt6ct-widgets INTERFACE qt6ct/src )
diff --git a/external/stb b/external/stb
index beebb24b..f58f558c 160000
--- a/external/stb
+++ b/external/stb
@@ -1 +1 @@
-Subproject commit beebb24b945efdea3b9bba23affb8eb3ba8982e7
+Subproject commit f58f558c120e9b32c217290b80bad1a0729fbb2c
diff --git a/external/stb.cmake b/external/stb.cmake
index c26bedaf..570af425 100644
--- a/external/stb.cmake
+++ b/external/stb.cmake
@@ -1,4 +1,4 @@
-cmake_minimum_required(VERSION 3.20)
+cmake_minimum_required(VERSION 3.24)
 set(PROJECT_NAME scwx-stb)
 
 set(STB_INCLUDE_DIR ${CMAKE_CURRENT_SOURCE_DIR}/stb PARENT_SCOPE)
diff --git a/external/textflowcpp.cmake b/external/textflowcpp.cmake
index 1e36da18..31020665 100644
--- a/external/textflowcpp.cmake
+++ b/external/textflowcpp.cmake
@@ -1,4 +1,4 @@
-cmake_minimum_required(VERSION 3.20)
+cmake_minimum_required(VERSION 3.24)
 set(PROJECT_NAME scwx-textflowcpp)
 
 set(TEXTFLOWCPP_INCLUDE_DIR ${CMAKE_CURRENT_SOURCE_DIR}/textflowcpp PARENT_SCOPE)
diff --git a/external/units.cmake b/external/units.cmake
index d037ae54..cc70ac1c 100644
--- a/external/units.cmake
+++ b/external/units.cmake
@@ -1,4 +1,4 @@
-cmake_minimum_required(VERSION 3.20)
+cmake_minimum_required(VERSION 3.24)
 set(PROJECT_NAME scwx-units)
 
 add_subdirectory(units)
diff --git a/requirements.txt b/requirements.txt
new file mode 100644
index 00000000..afdd2a37
--- /dev/null
+++ b/requirements.txt
@@ -0,0 +1,3 @@
+conan
+geopandas
+GitPython
diff --git a/scwx-qt/CMakeLists.txt b/scwx-qt/CMakeLists.txt
index f4e636e7..e47bc3fb 100644
--- a/scwx-qt/CMakeLists.txt
+++ b/scwx-qt/CMakeLists.txt
@@ -1,4 +1,4 @@
-cmake_minimum_required(VERSION 3.21)
+cmake_minimum_required(VERSION 3.24)
 
 set_property(DIRECTORY
              APPEND
diff --git a/scwx-qt/gl/map_color.vert b/scwx-qt/gl/map_color.vert
index 6ae98e92..609d1c34 100644
--- a/scwx-qt/gl/map_color.vert
+++ b/scwx-qt/gl/map_color.vert
@@ -26,6 +26,9 @@ void main()
    // Always set displayed to true
    vsOut.displayed = 1;
 
+   // Initialize texCoord to default value
+   vsOut.texCoord = vec3(0.0f, 0.0f, 0.0f);
+
    // Pass the threshold and time range to the geometry shader
    vsOut.threshold = aThreshold;
    vsOut.timeRange = aTimeRange;
diff --git a/scwx-qt/gl/radar.frag b/scwx-qt/gl/radar.frag
index 0c605b8a..b6491e8b 100644
--- a/scwx-qt/gl/radar.frag
+++ b/scwx-qt/gl/radar.frag
@@ -9,14 +9,14 @@ uniform float uDataMomentScale;
 
 uniform bool uCFPEnabled;
 
-flat in uint dataMoment;
-flat in uint cfpMoment;
+in float dataMoment;
+in float cfpMoment;
 
 layout (location = 0) out vec4 fragColor;
 
 void main()
 {
-   float texCoord = float(dataMoment - uDataMomentOffset) / uDataMomentScale;
+   float texCoord = (dataMoment - float(uDataMomentOffset)) / uDataMomentScale;
 
    if (uCFPEnabled && cfpMoment > 8u)
    {
diff --git a/scwx-qt/gl/radar.vert b/scwx-qt/gl/radar.vert
index b4da9f17..97754b73 100644
--- a/scwx-qt/gl/radar.vert
+++ b/scwx-qt/gl/radar.vert
@@ -13,8 +13,8 @@ layout (location = 2) in uint aCfpMoment;
 uniform mat4 uMVPMatrix;
 uniform vec2 uMapScreenCoord;
 
-flat out uint dataMoment;
-flat out uint cfpMoment;
+out float dataMoment;
+out float cfpMoment;
 
 vec2 latLngToScreenCoordinate(in vec2 latLng)
 {
diff --git a/scwx-qt/gl/threshold.geom b/scwx-qt/gl/threshold.geom
index 677a80cd..deead87d 100644
--- a/scwx-qt/gl/threshold.geom
+++ b/scwx-qt/gl/threshold.geom
@@ -21,7 +21,9 @@ smooth out vec4 color;
 void main()
 {
    if (gsIn[0].displayed != 0 &&
-       (gsIn[0].threshold <= 0 ||            // If Threshold: 0 was specified, no threshold
+       (gsIn[0].threshold == 0 ||            // If Threshold: 0 was specified, no threshold
+        uMapDistance == 0 ||                 // If uMapDistance is zero, threshold is disabled
+        (gsIn[0].threshold < 0 && -(gsIn[0].threshold) <= uMapDistance) || // If Threshold is negative and below current map distance
         gsIn[0].threshold >= uMapDistance || // If Threshold is above current map distance
         gsIn[0].threshold >= 999) &&         // If Threshold: 999 was specified (or greater), no threshold
        (gsIn[0].timeRange[0] == 0 ||              // If there is no start time specified
diff --git a/scwx-qt/res/config/radar_sites.json b/scwx-qt/res/config/radar_sites.json
index 8f2a1b90..20d7397c 100644
--- a/scwx-qt/res/config/radar_sites.json
+++ b/scwx-qt/res/config/radar_sites.json
@@ -67,7 +67,7 @@
 	{ "type": "wsr88d", "id": "KLVX", "lat": 37.9753058, "lon": -85.9438455,  "country": "USA", "state": "KY",  "place": "Louisville",              "tz": "America/New_York",             "elevation": 833.0 },
 	{ "type": "wsr88d", "id": "KPAH", "lat": 37.068333,  "lon": -88.771944,   "country": "USA", "state": "KY",  "place": "Paducah",                 "tz": "America/Chicago",              "elevation": 506.0 },
 	{ "type": "wsr88d", "id": "KPOE", "lat": 31.1556923, "lon": -92.9762596,  "country": "USA", "state": "LA",  "place": "Fort Polk",               "tz": "America/Chicago",              "elevation": 473.0 },
-	{ "type": "wsr88d", "id": "KHDC", "lat": 30.519306,  "lon": -90.424028,   "country": "USA", "state": "LA",  "place": "New Orleans (Hammond)",   "tz": "America/Chicago",              "elevation": 43.0 },
+	{ "type": "wsr88d", "id": "KHDC", "lat": 30.5196,    "lon": -90.4074,     "country": "USA", "state": "LA",  "place": "New Orleans (Hammond)",   "tz": "America/Chicago",              "elevation": 43.0 },
 	{ "type": "wsr88d", "id": "KLCH", "lat": 30.125306,  "lon": -93.215889,   "country": "USA", "state": "LA",  "place": "Lake Charles",            "tz": "America/Chicago",              "elevation": 137.0 },
 	{ "type": "wsr88d", "id": "KSHV", "lat": 32.450833,  "lon": -93.84125,    "country": "USA", "state": "LA",  "place": "Shreveport",              "tz": "America/Chicago",              "elevation": 387.0 },
 	{ "type": "wsr88d", "id": "KLIX", "lat": 30.3367133, "lon": -89.8256618,  "country": "USA", "state": "LA",  "place": "New Orleans (Slidell)",   "tz": "America/Chicago",              "elevation": 179.0 },
diff --git a/scwx-qt/res/icons/font-awesome-6/briefcase-solid.svg b/scwx-qt/res/icons/font-awesome-6/briefcase-solid.svg
new file mode 100644
index 00000000..b16bc330
--- /dev/null
+++ b/scwx-qt/res/icons/font-awesome-6/briefcase-solid.svg
@@ -0,0 +1 @@
+
diff --git a/scwx-qt/res/icons/font-awesome-6/building-columns-solid.svg b/scwx-qt/res/icons/font-awesome-6/building-columns-solid.svg
new file mode 100644
index 00000000..cf0df19a
--- /dev/null
+++ b/scwx-qt/res/icons/font-awesome-6/building-columns-solid.svg
@@ -0,0 +1 @@
+
diff --git a/scwx-qt/res/icons/font-awesome-6/building-solid.svg b/scwx-qt/res/icons/font-awesome-6/building-solid.svg
new file mode 100644
index 00000000..6f6d3f24
--- /dev/null
+++ b/scwx-qt/res/icons/font-awesome-6/building-solid.svg
@@ -0,0 +1 @@
+
diff --git a/scwx-qt/res/icons/font-awesome-6/caravan-solid.svg b/scwx-qt/res/icons/font-awesome-6/caravan-solid.svg
new file mode 100644
index 00000000..c341214f
--- /dev/null
+++ b/scwx-qt/res/icons/font-awesome-6/caravan-solid.svg
@@ -0,0 +1 @@
+
diff --git a/scwx-qt/res/icons/font-awesome-6/house-solid-white.svg b/scwx-qt/res/icons/font-awesome-6/house-solid-white.svg
new file mode 100644
index 00000000..59f65e1e
--- /dev/null
+++ b/scwx-qt/res/icons/font-awesome-6/house-solid-white.svg
@@ -0,0 +1 @@
+
diff --git a/scwx-qt/res/icons/font-awesome-6/location-crosshairs-solid.svg b/scwx-qt/res/icons/font-awesome-6/location-crosshairs-solid.svg
new file mode 100644
index 00000000..5bb1ea5c
--- /dev/null
+++ b/scwx-qt/res/icons/font-awesome-6/location-crosshairs-solid.svg
@@ -0,0 +1 @@
+
diff --git a/scwx-qt/res/icons/font-awesome-6/location-pin.svg b/scwx-qt/res/icons/font-awesome-6/location-pin.svg
new file mode 100644
index 00000000..4b6182cd
--- /dev/null
+++ b/scwx-qt/res/icons/font-awesome-6/location-pin.svg
@@ -0,0 +1,4 @@
+
diff --git a/scwx-qt/res/icons/font-awesome-6/star-solid-white.svg b/scwx-qt/res/icons/font-awesome-6/star-solid-white.svg
new file mode 100644
index 00000000..41bcd103
--- /dev/null
+++ b/scwx-qt/res/icons/font-awesome-6/star-solid-white.svg
@@ -0,0 +1 @@
+
diff --git a/scwx-qt/res/icons/font-awesome-6/tent-solid.svg b/scwx-qt/res/icons/font-awesome-6/tent-solid.svg
new file mode 100644
index 00000000..9f159d60
--- /dev/null
+++ b/scwx-qt/res/icons/font-awesome-6/tent-solid.svg
@@ -0,0 +1 @@
+
diff --git a/scwx-qt/res/icons/scwx.icns b/scwx-qt/res/icons/scwx.icns
new file mode 100644
index 00000000..bda4ea32
Binary files /dev/null and b/scwx-qt/res/icons/scwx.icns differ
diff --git a/scwx-qt/res/linux/net.supercellwx.app.desktop b/scwx-qt/res/linux/net.supercellwx.app.desktop
new file mode 100644
index 00000000..8e671522
--- /dev/null
+++ b/scwx-qt/res/linux/net.supercellwx.app.desktop
@@ -0,0 +1,7 @@
+[Desktop Entry]
+Type=Application
+Name=Supercell Wx
+Comment=Weather Radar and Data Viewer
+Exec=supercell-wx
+Icon=net.supercellwx.app.png
+Categories=Network;Science;
diff --git a/scwx-qt/res/scwx-qt.plist.in b/scwx-qt/res/scwx-qt.plist.in
new file mode 100644
index 00000000..f9db57c0
--- /dev/null
+++ b/scwx-qt/res/scwx-qt.plist.in
@@ -0,0 +1,42 @@
+
+
+
+
+	CFBundleInfoDictionaryVersion
+	6.0
+	CFBundlePackageType
+	APPL
+
+	CFBundleName
+	${MACOSX_BUNDLE_BUNDLE_NAME}
+	CFBundleIdentifier
+	${MACOSX_BUNDLE_GUI_IDENTIFIER}
+	CFBundleExecutable
+	${MACOSX_BUNDLE_EXECUTABLE_NAME}
+
+	CFBundleVersion
+	${MACOSX_BUNDLE_BUNDLE_VERSION}
+	CFBundleShortVersionString
+	${MACOSX_BUNDLE_SHORT_VERSION_STRING}
+
+	LSMinimumSystemVersion
+	${CMAKE_OSX_DEPLOYMENT_TARGET}
+
+	NSHumanReadableCopyright
+	${MACOSX_BUNDLE_COPYRIGHT}
+
+	CFBundleIconFile
+	${MACOSX_BUNDLE_ICON_FILE}
+
+	CFBundleDevelopmentRegion
+	en
+	CFBundleAllowMixedLocalizations
+	
+
+	NSPrincipalClass
+	NSApplication
+
+	NSSupportsAutomaticGraphicsSwitching
+	
+
+
diff --git a/scwx-qt/res/textures/images/dot.svg b/scwx-qt/res/textures/images/dot.svg
new file mode 100644
index 00000000..8f765035
--- /dev/null
+++ b/scwx-qt/res/textures/images/dot.svg
@@ -0,0 +1,9 @@
+
diff --git a/scwx-qt/res/textures/images/location-marker.svg b/scwx-qt/res/textures/images/location-marker.svg
new file mode 100644
index 00000000..3eef9d9e
--- /dev/null
+++ b/scwx-qt/res/textures/images/location-marker.svg
@@ -0,0 +1,11 @@
+
diff --git a/scwx-qt/scwx-qt.cmake b/scwx-qt/scwx-qt.cmake
index 78d45217..ecd26ef9 100644
--- a/scwx-qt/scwx-qt.cmake
+++ b/scwx-qt/scwx-qt.cmake
@@ -1,4 +1,4 @@
-cmake_minimum_required(VERSION 3.21)
+cmake_minimum_required(VERSION 3.24)
 
 project(scwx-qt LANGUAGES CXX)
 
@@ -6,17 +6,19 @@ set(CMAKE_INCLUDE_CURRENT_DIR ON)
 
 set(CMAKE_AUTOUIC ON)
 set(CMAKE_AUTOMOC ON)
-set(CMAKE_AUTORCC ON)
+set(CMAKE_AUTORCC OFF)
 
 set(CMAKE_CXX_STANDARD 20)
 set(CMAKE_CXX_STANDARD_REQUIRED ON)
 
+OPTION(SCWX_DISABLE_CONSOLE "Disables the Windows console in release mode" ON)
+
 find_package(Boost)
 find_package(Fontconfig)
 find_package(geographiclib)
 find_package(geos)
-find_package(GLEW)
 find_package(glm)
+find_package(OpenGL)
 find_package(Python COMPONENTS Interpreter)
 find_package(SQLite3)
 
@@ -30,7 +32,9 @@ find_package(QT NAMES Qt6
                         Positioning
                         SerialPort
                         Svg
-                        Widgets REQUIRED)
+                        Widgets
+                        Sql
+             REQUIRED)
 
 find_package(Qt${QT_VERSION_MAJOR}
              COMPONENTS Gui
@@ -49,9 +53,13 @@ find_package(Qt${QT_VERSION_MAJOR}
 set(SRC_EXE_MAIN source/scwx/qt/main/main.cpp)
 
 set(HDR_MAIN source/scwx/qt/main/application.hpp
-             source/scwx/qt/main/main_window.hpp)
+             source/scwx/qt/main/check_privilege.hpp
+             source/scwx/qt/main/main_window.hpp
+             source/scwx/qt/main/process_validation.hpp)
 set(SRC_MAIN source/scwx/qt/main/application.cpp
-             source/scwx/qt/main/main_window.cpp)
+             source/scwx/qt/main/check_privilege.cpp
+             source/scwx/qt/main/main_window.cpp
+             source/scwx/qt/main/process_validation.cpp)
 set(UI_MAIN  source/scwx/qt/main/main_window.ui)
 set(HDR_CONFIG source/scwx/qt/config/county_database.hpp
                source/scwx/qt/config/radar_site.hpp)
@@ -95,11 +103,13 @@ set(HDR_MANAGER source/scwx/qt/manager/alert_manager.hpp
                 source/scwx/qt/manager/log_manager.hpp
                 source/scwx/qt/manager/media_manager.hpp
                 source/scwx/qt/manager/placefile_manager.hpp
+                source/scwx/qt/manager/marker_manager.hpp
                 source/scwx/qt/manager/position_manager.hpp
                 source/scwx/qt/manager/radar_product_manager.hpp
                 source/scwx/qt/manager/radar_product_manager_notifier.hpp
                 source/scwx/qt/manager/resource_manager.hpp
                 source/scwx/qt/manager/settings_manager.hpp
+                source/scwx/qt/manager/task_manager.hpp
                 source/scwx/qt/manager/text_event_manager.hpp
                 source/scwx/qt/manager/thread_manager.hpp
                 source/scwx/qt/manager/timeline_manager.hpp
@@ -111,11 +121,13 @@ set(SRC_MANAGER source/scwx/qt/manager/alert_manager.cpp
                 source/scwx/qt/manager/log_manager.cpp
                 source/scwx/qt/manager/media_manager.cpp
                 source/scwx/qt/manager/placefile_manager.cpp
+                source/scwx/qt/manager/marker_manager.cpp
                 source/scwx/qt/manager/position_manager.cpp
                 source/scwx/qt/manager/radar_product_manager.cpp
                 source/scwx/qt/manager/radar_product_manager_notifier.cpp
                 source/scwx/qt/manager/resource_manager.cpp
                 source/scwx/qt/manager/settings_manager.cpp
+                source/scwx/qt/manager/task_manager.cpp
                 source/scwx/qt/manager/text_event_manager.cpp
                 source/scwx/qt/manager/thread_manager.cpp
                 source/scwx/qt/manager/timeline_manager.cpp
@@ -132,6 +144,7 @@ set(HDR_MAP source/scwx/qt/map/alert_layer.hpp
             source/scwx/qt/map/overlay_layer.hpp
             source/scwx/qt/map/overlay_product_layer.hpp
             source/scwx/qt/map/placefile_layer.hpp
+            source/scwx/qt/map/marker_layer.hpp
             source/scwx/qt/map/radar_product_layer.hpp
             source/scwx/qt/map/radar_range_layer.hpp
             source/scwx/qt/map/radar_site_layer.hpp)
@@ -146,6 +159,7 @@ set(SRC_MAP source/scwx/qt/map/alert_layer.cpp
             source/scwx/qt/map/overlay_layer.cpp
             source/scwx/qt/map/overlay_product_layer.cpp
             source/scwx/qt/map/placefile_layer.cpp
+            source/scwx/qt/map/marker_layer.cpp
             source/scwx/qt/map/radar_product_layer.cpp
             source/scwx/qt/map/radar_range_layer.cpp
             source/scwx/qt/map/radar_site_layer.cpp)
@@ -154,6 +168,7 @@ set(HDR_MODEL source/scwx/qt/model/alert_model.hpp
               source/scwx/qt/model/imgui_context_model.hpp
               source/scwx/qt/model/layer_model.hpp
               source/scwx/qt/model/placefile_model.hpp
+              source/scwx/qt/model/marker_model.hpp
               source/scwx/qt/model/radar_site_model.hpp
               source/scwx/qt/model/tree_item.hpp
               source/scwx/qt/model/tree_model.hpp)
@@ -162,6 +177,7 @@ set(SRC_MODEL source/scwx/qt/model/alert_model.cpp
               source/scwx/qt/model/imgui_context_model.cpp
               source/scwx/qt/model/layer_model.cpp
               source/scwx/qt/model/placefile_model.cpp
+              source/scwx/qt/model/marker_model.cpp
               source/scwx/qt/model/radar_site_model.cpp
               source/scwx/qt/model/tree_item.cpp
               source/scwx/qt/model/tree_model.cpp)
@@ -169,9 +185,11 @@ set(HDR_REQUEST source/scwx/qt/request/download_request.hpp
                 source/scwx/qt/request/nexrad_file_request.hpp)
 set(SRC_REQUEST source/scwx/qt/request/download_request.cpp
                 source/scwx/qt/request/nexrad_file_request.cpp)
-set(HDR_SETTINGS source/scwx/qt/settings/audio_settings.hpp
+set(HDR_SETTINGS source/scwx/qt/settings/alert_palette_settings.hpp
+                 source/scwx/qt/settings/audio_settings.hpp
                  source/scwx/qt/settings/general_settings.hpp
                  source/scwx/qt/settings/hotkey_settings.hpp
+                 source/scwx/qt/settings/line_settings.hpp
                  source/scwx/qt/settings/map_settings.hpp
                  source/scwx/qt/settings/palette_settings.hpp
                  source/scwx/qt/settings/product_settings.hpp
@@ -185,9 +203,11 @@ set(HDR_SETTINGS source/scwx/qt/settings/audio_settings.hpp
                  source/scwx/qt/settings/text_settings.hpp
                  source/scwx/qt/settings/ui_settings.hpp
                  source/scwx/qt/settings/unit_settings.hpp)
-set(SRC_SETTINGS source/scwx/qt/settings/audio_settings.cpp
+set(SRC_SETTINGS source/scwx/qt/settings/alert_palette_settings.cpp
+                 source/scwx/qt/settings/audio_settings.cpp
                  source/scwx/qt/settings/general_settings.cpp
                  source/scwx/qt/settings/hotkey_settings.cpp
+                 source/scwx/qt/settings/line_settings.cpp
                  source/scwx/qt/settings/map_settings.cpp
                  source/scwx/qt/settings/palette_settings.cpp
                  source/scwx/qt/settings/product_settings.cpp
@@ -210,6 +230,7 @@ set(HDR_TYPES source/scwx/qt/types/alert_types.hpp
               source/scwx/qt/types/layer_types.hpp
               source/scwx/qt/types/location_types.hpp
               source/scwx/qt/types/map_types.hpp
+              source/scwx/qt/types/marker_types.hpp
               source/scwx/qt/types/media_types.hpp
               source/scwx/qt/types/qt_types.hpp
               source/scwx/qt/types/radar_product_record.hpp
@@ -238,10 +259,13 @@ set(HDR_UI source/scwx/qt/ui/about_dialog.hpp
            source/scwx/qt/ui/alert_dialog.hpp
            source/scwx/qt/ui/alert_dock_widget.hpp
            source/scwx/qt/ui/animation_dock_widget.hpp
+           source/scwx/qt/ui/api_key_edit_widget.hpp
            source/scwx/qt/ui/collapsible_group.hpp
            source/scwx/qt/ui/county_dialog.hpp
-           source/scwx/qt/ui/wfo_dialog.hpp
+           source/scwx/qt/ui/custom_layer_dialog.hpp
            source/scwx/qt/ui/download_dialog.hpp
+           source/scwx/qt/ui/edit_line_dialog.hpp
+           source/scwx/qt/ui/edit_marker_dialog.hpp
            source/scwx/qt/ui/flow_layout.hpp
            source/scwx/qt/ui/gps_info_dialog.hpp
            source/scwx/qt/ui/hotkey_edit.hpp
@@ -252,22 +276,29 @@ set(HDR_UI source/scwx/qt/ui/about_dialog.hpp
            source/scwx/qt/ui/level2_products_widget.hpp
            source/scwx/qt/ui/level2_settings_widget.hpp
            source/scwx/qt/ui/level3_products_widget.hpp
+           source/scwx/qt/ui/line_label.hpp
            source/scwx/qt/ui/open_url_dialog.hpp
            source/scwx/qt/ui/placefile_dialog.hpp
            source/scwx/qt/ui/placefile_settings_widget.hpp
+           source/scwx/qt/ui/marker_dialog.hpp
+           source/scwx/qt/ui/marker_settings_widget.hpp
            source/scwx/qt/ui/progress_dialog.hpp
            source/scwx/qt/ui/radar_site_dialog.hpp
            source/scwx/qt/ui/serial_port_dialog.hpp
            source/scwx/qt/ui/settings_dialog.hpp
-           source/scwx/qt/ui/update_dialog.hpp)
+           source/scwx/qt/ui/update_dialog.hpp
+           source/scwx/qt/ui/wfo_dialog.hpp)
 set(SRC_UI source/scwx/qt/ui/about_dialog.cpp
            source/scwx/qt/ui/alert_dialog.cpp
            source/scwx/qt/ui/alert_dock_widget.cpp
            source/scwx/qt/ui/animation_dock_widget.cpp
+           source/scwx/qt/ui/api_key_edit_widget.cpp
            source/scwx/qt/ui/collapsible_group.cpp
            source/scwx/qt/ui/county_dialog.cpp
-           source/scwx/qt/ui/wfo_dialog.cpp
+           source/scwx/qt/ui/custom_layer_dialog.cpp
            source/scwx/qt/ui/download_dialog.cpp
+           source/scwx/qt/ui/edit_line_dialog.cpp
+           source/scwx/qt/ui/edit_marker_dialog.cpp
            source/scwx/qt/ui/flow_layout.cpp
            source/scwx/qt/ui/gps_info_dialog.cpp
            source/scwx/qt/ui/hotkey_edit.cpp
@@ -278,36 +309,47 @@ set(SRC_UI source/scwx/qt/ui/about_dialog.cpp
            source/scwx/qt/ui/level2_products_widget.cpp
            source/scwx/qt/ui/level2_settings_widget.cpp
            source/scwx/qt/ui/level3_products_widget.cpp
+           source/scwx/qt/ui/line_label.cpp
            source/scwx/qt/ui/open_url_dialog.cpp
            source/scwx/qt/ui/placefile_dialog.cpp
            source/scwx/qt/ui/placefile_settings_widget.cpp
+           source/scwx/qt/ui/marker_dialog.cpp
+           source/scwx/qt/ui/marker_settings_widget.cpp
            source/scwx/qt/ui/progress_dialog.cpp
            source/scwx/qt/ui/radar_site_dialog.cpp
            source/scwx/qt/ui/settings_dialog.cpp
            source/scwx/qt/ui/serial_port_dialog.cpp
-           source/scwx/qt/ui/update_dialog.cpp)
+           source/scwx/qt/ui/update_dialog.cpp
+           source/scwx/qt/ui/wfo_dialog.cpp)
 set(UI_UI  source/scwx/qt/ui/about_dialog.ui
            source/scwx/qt/ui/alert_dialog.ui
            source/scwx/qt/ui/alert_dock_widget.ui
            source/scwx/qt/ui/animation_dock_widget.ui
            source/scwx/qt/ui/collapsible_group.ui
            source/scwx/qt/ui/county_dialog.ui
-           source/scwx/qt/ui/wfo_dialog.ui
+           source/scwx/qt/ui/custom_layer_dialog.ui
+           source/scwx/qt/ui/edit_line_dialog.ui
+           source/scwx/qt/ui/edit_marker_dialog.ui
            source/scwx/qt/ui/gps_info_dialog.ui
            source/scwx/qt/ui/imgui_debug_dialog.ui
            source/scwx/qt/ui/layer_dialog.ui
            source/scwx/qt/ui/open_url_dialog.ui
            source/scwx/qt/ui/placefile_dialog.ui
            source/scwx/qt/ui/placefile_settings_widget.ui
+           source/scwx/qt/ui/marker_dialog.ui
+           source/scwx/qt/ui/marker_settings_widget.ui
            source/scwx/qt/ui/progress_dialog.ui
            source/scwx/qt/ui/radar_site_dialog.ui
            source/scwx/qt/ui/settings_dialog.ui
            source/scwx/qt/ui/serial_port_dialog.ui
-           source/scwx/qt/ui/update_dialog.ui)
-set(HDR_UI_SETTINGS source/scwx/qt/ui/settings/hotkey_settings_widget.hpp
+           source/scwx/qt/ui/update_dialog.ui
+           source/scwx/qt/ui/wfo_dialog.ui)
+set(HDR_UI_SETTINGS source/scwx/qt/ui/settings/alert_palette_settings_widget.hpp
+                    source/scwx/qt/ui/settings/hotkey_settings_widget.hpp
                     source/scwx/qt/ui/settings/settings_page_widget.hpp
                     source/scwx/qt/ui/settings/unit_settings_widget.hpp)
-set(SRC_UI_SETTINGS source/scwx/qt/ui/settings/hotkey_settings_widget.cpp
+set(SRC_UI_SETTINGS source/scwx/qt/ui/settings/alert_palette_settings_widget.cpp
+                    source/scwx/qt/ui/settings/hotkey_settings_widget.cpp
                     source/scwx/qt/ui/settings/settings_page_widget.cpp
                     source/scwx/qt/ui/settings/unit_settings_widget.cpp)
 set(HDR_UI_SETUP source/scwx/qt/ui/setup/audio_codec_page.hpp
@@ -322,6 +364,9 @@ set(SRC_UI_SETUP source/scwx/qt/ui/setup/audio_codec_page.cpp
                  source/scwx/qt/ui/setup/map_provider_page.cpp
                  source/scwx/qt/ui/setup/setup_wizard.cpp
                  source/scwx/qt/ui/setup/welcome_page.cpp)
+set(HDR_UI_WIDGETS source/scwx/qt/ui/widgets/focused_combo_box.hpp
+                   source/scwx/qt/ui/widgets/focused_double_spin_box.hpp
+                   source/scwx/qt/ui/widgets/focused_spin_box.hpp)
 set(HDR_UTIL source/scwx/qt/util/color.hpp
              source/scwx/qt/util/file.hpp
              source/scwx/qt/util/geographic_lib.hpp
@@ -331,8 +376,10 @@ set(HDR_UTIL source/scwx/qt/util/color.hpp
              source/scwx/qt/util/network.hpp
              source/scwx/qt/util/streams.hpp
              source/scwx/qt/util/texture_atlas.hpp
+             source/scwx/qt/util/q_color_modulate.hpp
              source/scwx/qt/util/q_file_buffer.hpp
              source/scwx/qt/util/q_file_input_stream.hpp
+             source/scwx/qt/util/queue_counter.hpp
              source/scwx/qt/util/time.hpp
              source/scwx/qt/util/tooltip.hpp)
 set(SRC_UTIL source/scwx/qt/util/color.cpp
@@ -343,8 +390,10 @@ set(SRC_UTIL source/scwx/qt/util/color.cpp
              source/scwx/qt/util/maplibre.cpp
              source/scwx/qt/util/network.cpp
              source/scwx/qt/util/texture_atlas.cpp
+             source/scwx/qt/util/q_color_modulate.cpp
              source/scwx/qt/util/q_file_buffer.cpp
              source/scwx/qt/util/q_file_input_stream.cpp
+             source/scwx/qt/util/queue_counter.cpp
              source/scwx/qt/util/time.cpp
              source/scwx/qt/util/tooltip.cpp)
 set(HDR_VIEW source/scwx/qt/view/level2_product_view.hpp
@@ -385,13 +434,13 @@ set(JSON_FILES res/config/radar_sites.json)
 set(TS_FILES ts/scwx_en_US.ts)
 
 set(RADAR_SITES_FILE ${scwx-qt_SOURCE_DIR}/res/config/radar_sites.json)
-set(COUNTY_DBF_FILES ${SCWX_DIR}/data/db/c_05mr24.dbf)
-set(ZONE_DBF_FILES   ${SCWX_DIR}/data/db/fz05mr24.dbf
-                     ${SCWX_DIR}/data/db/mz05mr24.dbf
-                     ${SCWX_DIR}/data/db/oz05mr24.dbf
-                     ${SCWX_DIR}/data/db/z_05mr24.dbf)
-set(STATE_DBF_FILES  ${SCWX_DIR}/data/db/s_05mr24.dbf)
-set(WFO_DBF_FILES    ${SCWX_DIR}/data/db/w_05mr24.dbf)
+set(COUNTY_DBF_FILES ${SCWX_DIR}/data/db/c_18mr25.dbf)
+set(ZONE_DBF_FILES   ${SCWX_DIR}/data/db/fz18mr25.dbf
+                     ${SCWX_DIR}/data/db/mz18mr25.dbf
+                     ${SCWX_DIR}/data/db/oz18mr25.dbf
+                     ${SCWX_DIR}/data/db/z_18mr25.dbf)
+set(STATE_DBF_FILES  ${SCWX_DIR}/data/db/s_18mr25.dbf)
+set(WFO_DBF_FILES    ${SCWX_DIR}/data/db/w_18mr25.dbf)
 set(COUNTIES_SQLITE_DB ${scwx-qt_BINARY_DIR}/res/db/counties.db)
 
 set(RESOURCE_INPUT  ${scwx-qt_SOURCE_DIR}/res/scwx-qt.rc.in)
@@ -429,17 +478,19 @@ set(PROJECT_SOURCES ${HDR_MAIN}
                     ${SRC_UI_SETTINGS}
                     ${HDR_UI_SETUP}
                     ${SRC_UI_SETUP}
+                    ${HDR_UI_WIDGETS}
                     ${HDR_UTIL}
                     ${SRC_UTIL}
                     ${HDR_VIEW}
                     ${SRC_VIEW}
                     ${SHADER_FILES}
                     ${JSON_FILES}
-                    ${RESOURCE_FILES}
                     ${TS_FILES}
                     ${CMAKE_FILES})
 set(EXECUTABLE_SOURCES ${SRC_EXE_MAIN})
 
+qt_add_resources(PROJECT_SOURCES ${RESOURCE_FILES})
+
 source_group("Header Files\\main"         FILES ${HDR_MAIN})
 source_group("Source Files\\main"         FILES ${SRC_MAIN})
 source_group("Header Files\\config"       FILES ${HDR_CONFIG})
@@ -468,6 +519,7 @@ source_group("Header Files\\ui\\settings" FILES ${HDR_UI_SETTINGS})
 source_group("Source Files\\ui\\settings" FILES ${SRC_UI_SETTINGS})
 source_group("Header Files\\ui\\setup"    FILES ${HDR_UI_SETUP})
 source_group("Source Files\\ui\\setup"    FILES ${SRC_UI_SETUP})
+source_group("Header Files\\ui\\widgets"  FILES ${HDR_UI_WIDGETS})
 source_group("UI Files\\ui"               FILES ${UI_UI})
 source_group("Header Files\\util"         FILES ${HDR_UTIL})
 source_group("Source Files\\util"         FILES ${SRC_UTIL})
@@ -480,6 +532,7 @@ source_group("I18N Files"                 FILES ${TS_FILES})
 
 add_library(scwx-qt OBJECT ${PROJECT_SOURCES})
 set_property(TARGET scwx-qt PROPERTY AUTOMOC ON)
+set_property(TARGET scwx-qt PROPERTY AUTOGEN_ORIGIN_DEPENDS OFF)
 
 add_custom_command(OUTPUT  ${COUNTIES_SQLITE_DB}
                    COMMAND ${Python_EXECUTABLE}
@@ -526,7 +579,8 @@ else()
                                -v ${SCWX_VERSION}
                                -c ${VERSIONS_CACHE}
                                -i ${VERSIONS_INPUT}
-                               -o ${VERSIONS_HEADER})
+                               -o ${VERSIONS_HEADER}
+                               -b ${SCWX_BUILD_NUM})
 endif()
 
 add_custom_target(scwx-qt_generate_versions ALL
@@ -572,7 +626,29 @@ set_target_properties(scwx-qt_update_radar_sites   PROPERTIES FOLDER generate)
 if (WIN32)
     set(APP_ICON_RESOURCE_WINDOWS ${RESOURCE_OUTPUT})
     qt_add_executable(supercell-wx ${EXECUTABLE_SOURCES} ${APP_ICON_RESOURCE_WINDOWS})
-    set_target_properties(supercell-wx PROPERTIES WIN32_EXECUTABLE $,TRUE,FALSE>)
+    if (SCWX_DISABLE_CONSOLE)
+        set_target_properties(supercell-wx PROPERTIES WIN32_EXECUTABLE $,TRUE,FALSE>)
+    endif()
+elseif (APPLE)
+    set(SCWX_ICON "${scwx-qt_SOURCE_DIR}/res/icons/scwx.icns")
+
+    set_source_files_properties(${SCWX_ICON} PROPERTIES MACOSX_PACKAGE_LOCATION "Resources")
+
+    qt_add_executable(supercell-wx ${EXECUTABLE_SOURCES} ${SCWX_ICON})
+
+    string(TIMESTAMP CURRENT_YEAR "%Y")
+
+    set_target_properties(supercell-wx PROPERTIES
+                          MACOSX_BUNDLE                      TRUE
+                          MACOSX_BUNDLE_INFO_PLIST           "${scwx-qt_SOURCE_DIR}/res/scwx-qt.plist.in"
+                          MACOSX_BUNDLE_GUI_IDENTIFIER       "net.supercellwx.app"
+                          MACOSX_BUNDLE_BUNDLE_NAME          "Supercell Wx"
+                          MACOSX_BUNDLE_BUNDLE_VERSION       "${SCWX_VERSION}"
+                          MACOSX_BUNDLE_SHORT_VERSION_STRING "${SCWX_VERSION}"
+                          MACOSX_BUNDLE_COPYRIGHT            "Copyright ${CURRENT_YEAR} Dan Paulat"
+                          MACOSX_BUNDLE_ICON_FILE            "scwx.icns"
+                          MACOSX_BUNDLE_INFO_STRING          "Free and open source advanced weather radar"
+                          RESOURCE                           ${SCWX_ICON})
 else()
     qt_add_executable(supercell-wx ${EXECUTABLE_SOURCES})
 endif()
@@ -582,12 +658,17 @@ if (WIN32)
     target_compile_definitions(supercell-wx PUBLIC WIN32_LEAN_AND_MEAN)
 endif()
 
-if (NOT MSVC)
+if (LINUX)
     # Qt emit keyword is incompatible with TBB
     target_compile_definitions(scwx-qt      PRIVATE QT_NO_EMIT)
     target_compile_definitions(supercell-wx PRIVATE QT_NO_EMIT)
 endif()
 
+if (APPLE)
+    target_compile_definitions(scwx-qt      PRIVATE GL_SILENCE_DEPRECATION)
+    target_compile_definitions(supercell-wx PRIVATE GL_SILENCE_DEPRECATION)
+endif()
+
 target_include_directories(scwx-qt PUBLIC ${scwx-qt_SOURCE_DIR}/source
                                           ${FTGL_INCLUDE_DIR}
                                           ${IMGUI_INCLUDE_DIRS}
@@ -643,6 +724,25 @@ else()
     target_compile_options(supercell-wx PRIVATE "$<$:-g>")
 endif()
 
+if (LINUX)
+    # Add wayland client packages
+    find_package(QT NAMES Qt6
+                 COMPONENTS WaylandClient
+                 REQUIRED)
+
+    find_package(Qt${QT_VERSION_MAJOR}
+                 COMPONENTS WaylandClient
+                 REQUIRED)
+    target_link_libraries(scwx-qt PUBLIC Qt${QT_VERSION_MAJOR}::WaylandClient)
+endif()
+
+if (LINUX)
+    find_package(mesa-glu REQUIRED)
+    target_link_libraries(scwx-qt PUBLIC mesa-glu::mesa-glu)
+else()
+    target_link_libraries(scwx-qt PUBLIC OpenGL::GLU)
+endif()
+
 target_link_libraries(scwx-qt PUBLIC Qt${QT_VERSION_MAJOR}::Widgets
                                      Qt${QT_VERSION_MAJOR}::OpenGLWidgets
                                      Qt${QT_VERSION_MAJOR}::Multimedia
@@ -651,6 +751,7 @@ target_link_libraries(scwx-qt PUBLIC Qt${QT_VERSION_MAJOR}::Widgets
                                      Qt${QT_VERSION_MAJOR}::Svg
                                      Boost::json
                                      Boost::timer
+                                     Boost::atomic
                                      QMapLibre::Core
                                      $<$:opengl32>
                                      $<$:SetupAPI>
@@ -658,18 +759,22 @@ target_link_libraries(scwx-qt PUBLIC Qt${QT_VERSION_MAJOR}::Widgets
                                      GeographicLib::GeographicLib
                                      GEOS::geos
                                      GEOS::geos_cxx_flags
-                                     GLEW::GLEW
+                                     glad_gl_core_33
                                      glm::glm
                                      imgui
+                                     qt6ct-common
+                                     qt6ct-widgets
                                      SQLite::SQLite3
                                      wxdata)
 
 target_link_libraries(supercell-wx PRIVATE scwx-qt
                                            wxdata)
 
-# Set DT_RUNPATH for Linux targets
-set_target_properties(MLNQtCore    PROPERTIES INSTALL_RPATH "\$ORIGIN/../lib") # QMapLibre::Core
-set_target_properties(supercell-wx PROPERTIES INSTALL_RPATH "\$ORIGIN/../lib")
+if (LINUX)
+    # Set DT_RUNPATH for Linux targets
+    set_target_properties(MLNQtCore    PROPERTIES INSTALL_RPATH "\$ORIGIN/../lib") # QMapLibre::Core
+    set_target_properties(supercell-wx PROPERTIES INSTALL_RPATH "\$ORIGIN/../lib")
+endif()
 
 install(TARGETS supercell-wx
                 MLNQtCore # QMapLibre::Core
@@ -679,7 +784,15 @@ install(TARGETS supercell-wx
                                "^(/usr)?/lib/.*\\.so(\\..*)?"
         RUNTIME
           COMPONENT supercell-wx
+        BUNDLE
+          DESTINATION .
+          COMPONENT supercell-wx
+          OPTIONAL
         LIBRARY
+          COMPONENT supercell-wx
+          OPTIONAL
+        FRAMEWORK
+          DESTINATION Frameworks
           COMPONENT supercell-wx
           OPTIONAL)
 
@@ -701,14 +814,64 @@ install(SCRIPT ${deploy_script_qmaplibre_core}
 install(SCRIPT ${deploy_script_scwx}
         COMPONENT supercell-wx)
 
+if (APPLE)
+    # Install additional script to fix up the bundle
+    install(CODE [[
+            include (BundleUtilities)
+
+            # Define the bundle path
+            set(BUNDLE_PATH "${CMAKE_INSTALL_PREFIX}/supercell-wx.app")
+
+            file(GLOB_RECURSE PLUGIN_DYLIBS "${BUNDLE_PATH}/Contents/PlugIns/**/*.dylib")
+
+            # Add the correct rpath for plugins to find bundled frameworks
+            foreach(PLUGIN_DYLIB ${PLUGIN_DYLIBS})
+                execute_process(
+                    COMMAND install_name_tool -add_rpath "@loader_path/../../Frameworks"
+                    ${PLUGIN_DYLIB}
+                    )
+            endforeach()
+
+            # Fix up the bundle with all dependencies
+            fixup_bundle(
+                "${BUNDLE_PATH}"
+                ""
+                "${CMAKE_INSTALL_PREFIX}/lib;${CMAKE_INSTALL_PREFIX}/Frameworks"
+                )
+
+            # Re-sign the bundle
+            execute_process(
+                COMMAND codesign --force --deep --sign - "${BUNDLE_PATH}"
+                )
+
+            # Verify the bundle
+            verify_app("${BUNDLE_PATH}")
+
+            # Rename to "Supercell Wx.app"
+            file(REMOVE_RECURSE
+                 "${CMAKE_INSTALL_PREFIX}/Supercell Wx.app")
+            file(RENAME
+                 "${BUNDLE_PATH}"
+                 "${CMAKE_INSTALL_PREFIX}/Supercell Wx.app")
+
+            # Remove extra directories
+            file(REMOVE_RECURSE
+                 "${CMAKE_INSTALL_PREFIX}/Frameworks")
+            file(REMOVE_RECURSE
+                 "${CMAKE_INSTALL_PREFIX}/lib")
+            ]]
+            COMPONENT supercell-wx)
+endif()
+
+set(CPACK_PACKAGE_NAME          "Supercell Wx")
+set(CPACK_PACKAGE_VENDOR        "Dan Paulat")
+set(CPACK_PACKAGE_CHECKSUM      SHA256)
+set(CPACK_RESOURCE_FILE_LICENSE "${SCWX_DIR}/LICENSE.txt")
+
 if (MSVC)
-    set(CPACK_PACKAGE_NAME                "Supercell Wx")
-    set(CPACK_PACKAGE_VENDOR              "Dan Paulat")
     set(CPACK_PACKAGE_FILE_NAME           "supercell-wx-v${SCWX_VERSION}-windows-x64")
     set(CPACK_PACKAGE_INSTALL_DIRECTORY   "Supercell Wx")
     set(CPACK_PACKAGE_ICON                "${CMAKE_CURRENT_SOURCE_DIR}/res/icons/scwx-256.ico")
-    set(CPACK_PACKAGE_CHECKSUM            SHA256)
-    set(CPACK_RESOURCE_FILE_LICENSE       "${SCWX_DIR}/LICENSE.txt")
     set(CPACK_GENERATOR                   WIX)
     set(CPACK_PACKAGE_EXECUTABLES         "supercell-wx;Supercell Wx")
     set(CPACK_WIX_UPGRADE_GUID            36AD0F51-4D4F-4B5D-AB61-94C6B4E4FE1C)
@@ -720,5 +883,15 @@ if (MSVC)
     set(CPACK_INSTALL_CMAKE_PROJECTS
         "${CMAKE_CURRENT_BINARY_DIR};${CMAKE_PROJECT_NAME};supercell-wx;/")
 
+    include(CPack)
+elseif(APPLE)
+    set(CPACK_PACKAGE_FILE_NAME   "supercell-wx-v${SCWX_VERSION}-macos")
+    set(CPACK_PACKAGE_ICON        "${SCWX_ICON}")
+    set(CPACK_PACKAGE_VERSION     "${SCWX_VERSION}")
+
+    set(CPACK_GENERATOR DragNDrop)
+
+    set(CPACK_COMPONENTS_ALL supercell-wx)
+
     include(CPack)
 endif()
diff --git a/scwx-qt/scwx-qt.qrc b/scwx-qt/scwx-qt.qrc
index c9e00337..ccb2e42a 100644
--- a/scwx-qt/scwx-qt.qrc
+++ b/scwx-qt/scwx-qt.qrc
@@ -32,6 +32,10 @@
         res/icons/font-awesome-6/angles-up-solid.svg
         res/icons/font-awesome-6/backward-step-solid.svg
         res/icons/font-awesome-6/book-solid.svg
+        res/icons/font-awesome-6/briefcase-solid.svg
+        res/icons/font-awesome-6/building-columns-solid.svg
+        res/icons/font-awesome-6/building-solid.svg
+        res/icons/font-awesome-6/caravan-solid.svg
         res/icons/font-awesome-6/copy-regular.svg
         res/icons/font-awesome-6/discord.svg
         res/icons/font-awesome-6/earth-americas-solid.svg
@@ -40,8 +44,11 @@
         res/icons/font-awesome-6/gears-solid.svg
         res/icons/font-awesome-6/github.svg
         res/icons/font-awesome-6/house-solid.svg
+        res/icons/font-awesome-6/house-solid-white.svg
         res/icons/font-awesome-6/keyboard-regular.svg
         res/icons/font-awesome-6/layer-group-solid.svg
+        res/icons/font-awesome-6/location-crosshairs-solid.svg
+        res/icons/font-awesome-6/location-pin.svg
         res/icons/font-awesome-6/palette-solid.svg
         res/icons/font-awesome-6/pause-solid.svg
         res/icons/font-awesome-6/play-solid.svg
@@ -53,7 +60,9 @@
         res/icons/font-awesome-6/square-minus-regular.svg
         res/icons/font-awesome-6/square-plus-regular.svg
         res/icons/font-awesome-6/star-solid.svg
+        res/icons/font-awesome-6/star-solid-white.svg
         res/icons/font-awesome-6/stop-solid.svg
+        res/icons/font-awesome-6/tent-solid.svg
         res/icons/font-awesome-6/volume-high-solid.svg
         res/palettes/wct/CC.pal
         res/palettes/wct/Default16.pal
@@ -70,11 +79,20 @@
         res/palettes/wct/SW.pal
         res/palettes/wct/VIL.pal
         res/palettes/wct/ZDR.pal
+        ../external/qt6ct/colors/airy.conf
+        ../external/qt6ct/colors/darker.conf
+        ../external/qt6ct/colors/dusk.conf
+        ../external/qt6ct/colors/ia_ora.conf
+        ../external/qt6ct/colors/sand.conf
+        ../external/qt6ct/colors/simple.conf
+        ../external/qt6ct/colors/waves.conf
         res/textures/lines/default-1x7.png
         res/textures/lines/test-pattern.png
         res/textures/images/cursor-17.png
         res/textures/images/crosshairs-24.png
         res/textures/images/dot-3.png
+        res/textures/images/dot.svg
+        res/textures/images/location-marker.svg
         res/textures/images/mapbox-logo.svg
         res/textures/images/maptiler-logo.svg
     
diff --git a/scwx-qt/source/scwx/qt/config/radar_site.cpp b/scwx-qt/source/scwx/qt/config/radar_site.cpp
index c0cb4636..69815636 100644
--- a/scwx-qt/source/scwx/qt/config/radar_site.cpp
+++ b/scwx-qt/source/scwx/qt/config/radar_site.cpp
@@ -10,7 +10,7 @@
 
 #include 
 
-#if !defined(_MSC_VER)
+#if (__cpp_lib_chrono < 201907L)
 #   include 
 #endif
 
@@ -51,6 +51,7 @@ public:
    std::string state_ {};
    std::string place_ {};
    std::string tzName_ {};
+   double      altitude_ {0.0};
 
    const scwx::util::time_zone* timeZone_ {nullptr};
 };
@@ -142,6 +143,11 @@ const scwx::util::time_zone* RadarSite::time_zone() const
    return p->timeZone_;
 }
 
+units::length::feet RadarSite::altitude() const
+{
+   return units::length::feet(p->altitude_);
+}
+
 std::shared_ptr RadarSite::Get(const std::string& id)
 {
    std::shared_lock           lock(siteMutex_);
@@ -239,7 +245,7 @@ size_t RadarSite::ReadConfig(const std::string& path)
    bool   dataValid  = true;
    size_t sitesAdded = 0;
 
-   boost::json::value j = util::json::ReadJsonFile(path);
+   boost::json::value j = util::json::ReadJsonQFile(path);
 
    dataValid = j.is_array();
 
@@ -268,10 +274,12 @@ size_t RadarSite::ReadConfig(const std::string& path)
             site->p->state_ = boost::json::value_to(o.at("state"));
             site->p->place_ = boost::json::value_to(o.at("place"));
             site->p->tzName_ = boost::json::value_to(o.at("tz"));
+            site->p->altitude_ =
+               boost::json::value_to(o.at("elevation"));
 
             try
             {
-#if defined(_MSC_VER)
+#if (__cpp_lib_chrono >= 201907L)
                using namespace std::chrono;
 #else
                using namespace date;
diff --git a/scwx-qt/source/scwx/qt/config/radar_site.hpp b/scwx-qt/source/scwx/qt/config/radar_site.hpp
index 16f6e710..cf622d7a 100644
--- a/scwx-qt/source/scwx/qt/config/radar_site.hpp
+++ b/scwx-qt/source/scwx/qt/config/radar_site.hpp
@@ -6,12 +6,9 @@
 #include 
 #include 
 #include 
+#include 
 
-namespace scwx
-{
-namespace qt
-{
-namespace config
+namespace scwx::qt::config
 {
 
 class RadarSiteImpl;
@@ -28,18 +25,19 @@ public:
    RadarSite(RadarSite&&) noexcept;
    RadarSite& operator=(RadarSite&&) noexcept;
 
-   std::string type() const;
-   std::string type_name() const;
-   std::string id() const;
-   double      latitude() const;
-   double      longitude() const;
-   std::string country() const;
-   std::string state() const;
-   std::string place() const;
-   std::string location_name() const;
-   std::string tz_name() const;
+   [[nodiscard]] std::string                 type() const;
+   [[nodiscard]] std::string                 type_name() const;
+   [[nodiscard]] std::string                 id() const;
+   [[nodiscard]] double                      latitude() const;
+   [[nodiscard]] double                      longitude() const;
+   [[nodiscard]] std::string                 country() const;
+   [[nodiscard]] std::string                 state() const;
+   [[nodiscard]] std::string                 place() const;
+   [[nodiscard]] std::string                 location_name() const;
+   [[nodiscard]] std::string                 tz_name() const;
+   [[nodiscard]] units::length::feet altitude() const;
 
-   const scwx::util::time_zone* time_zone() const;
+   [[nodiscard]] const scwx::util::time_zone* time_zone() const;
 
    static std::shared_ptr              Get(const std::string& id);
    static std::vector> GetAll();
@@ -67,6 +65,4 @@ private:
 
 std::string GetRadarIdFromSiteId(const std::string& siteId);
 
-} // namespace config
-} // namespace qt
-} // namespace scwx
+} // namespace scwx::qt::config
diff --git a/scwx-qt/source/scwx/qt/gl/draw/draw_item.cpp b/scwx-qt/source/scwx/qt/gl/draw/draw_item.cpp
index c6737a10..0f18e40f 100644
--- a/scwx-qt/source/scwx/qt/gl/draw/draw_item.cpp
+++ b/scwx-qt/source/scwx/qt/gl/draw/draw_item.cpp
@@ -30,13 +30,11 @@ static const std::string logPrefix_ = "scwx::qt::gl::draw::draw_item";
 class DrawItem::Impl
 {
 public:
-   explicit Impl(OpenGLFunctions& gl) : gl_ {gl} {}
-   ~Impl() {}
-
-   OpenGLFunctions& gl_;
+   explicit Impl() = default;
+   ~Impl()         = default;
 };
 
-DrawItem::DrawItem(OpenGLFunctions& gl) : p(std::make_unique(gl)) {}
+DrawItem::DrawItem() : p(std::make_unique()) {}
 DrawItem::~DrawItem() = default;
 
 DrawItem::DrawItem(DrawItem&&) noexcept            = default;
@@ -74,7 +72,7 @@ void DrawItem::UseDefaultProjection(
                                      0.0f,
                                      static_cast(params.height));
 
-   p->gl_.glUniformMatrix4fv(
+   glUniformMatrix4fv(
       uMVPMatrixLocation, 1, GL_FALSE, glm::value_ptr(projection));
 }
 
@@ -91,7 +89,7 @@ void DrawItem::UseRotationProjection(
                             glm::radians(params.bearing),
                             glm::vec3(0.0f, 0.0f, 1.0f));
 
-   p->gl_.glUniformMatrix4fv(
+   glUniformMatrix4fv(
       uMVPMatrixLocation, 1, GL_FALSE, glm::value_ptr(projection));
 }
 
@@ -100,16 +98,14 @@ void DrawItem::UseMapProjection(
    GLint                                         uMVPMatrixLocation,
    GLint                                         uMapScreenCoordLocation)
 {
-   OpenGLFunctions& gl = p->gl_;
-
    const glm::mat4 uMVPMatrix = util::maplibre::GetMapMatrix(params);
 
-   gl.glUniform2fv(uMapScreenCoordLocation,
-                   1,
-                   glm::value_ptr(util::maplibre::LatLongToScreenCoordinate(
-                      {params.latitude, params.longitude})));
+   glUniform2fv(uMapScreenCoordLocation,
+                1,
+                glm::value_ptr(util::maplibre::LatLongToScreenCoordinate(
+                   {params.latitude, params.longitude})));
 
-   gl.glUniformMatrix4fv(
+   glUniformMatrix4fv(
       uMVPMatrixLocation, 1, GL_FALSE, glm::value_ptr(uMVPMatrix));
 }
 
diff --git a/scwx-qt/source/scwx/qt/gl/draw/draw_item.hpp b/scwx-qt/source/scwx/qt/gl/draw/draw_item.hpp
index f7df44c4..28350190 100644
--- a/scwx-qt/source/scwx/qt/gl/draw/draw_item.hpp
+++ b/scwx-qt/source/scwx/qt/gl/draw/draw_item.hpp
@@ -21,8 +21,8 @@ namespace draw
 class DrawItem
 {
 public:
-   explicit DrawItem(OpenGLFunctions& gl);
-   ~DrawItem();
+   explicit DrawItem();
+   virtual ~DrawItem();
 
    DrawItem(const DrawItem&)            = delete;
    DrawItem& operator=(const DrawItem&) = delete;
diff --git a/scwx-qt/source/scwx/qt/gl/draw/geo_icons.cpp b/scwx-qt/source/scwx/qt/gl/draw/geo_icons.cpp
index 622718e3..05cc26a5 100644
--- a/scwx-qt/source/scwx/qt/gl/draw/geo_icons.cpp
+++ b/scwx-qt/source/scwx/qt/gl/draw/geo_icons.cpp
@@ -4,6 +4,7 @@
 #include 
 #include 
 #include 
+#include 
 
 #include 
 
@@ -38,7 +39,7 @@ static constexpr std::size_t kIntegersPerVertex_ = 4;
 static constexpr std::size_t kIntegerBufferLength_ =
    kNumTriangles * kVerticesPerTriangle * kIntegersPerVertex_;
 
-struct GeoIconDrawItem
+struct GeoIconDrawItem : types::EventHandler
 {
    units::length::nautical_miles       threshold_ {};
    std::chrono::sys_time startTime_ {};
@@ -144,7 +145,7 @@ public:
 };
 
 GeoIcons::GeoIcons(const std::shared_ptr& context) :
-    DrawItem(context->gl()), p(std::make_unique(context))
+    DrawItem(), p(std::make_unique(context))
 {
 }
 GeoIcons::~GeoIcons() = default;
@@ -165,8 +166,6 @@ void GeoIcons::set_thresholded(bool thresholded)
 
 void GeoIcons::Initialize()
 {
-   gl::OpenGLFunctions& gl = p->context_->gl();
-
    p->shaderProgram_ = p->context_->GetShaderProgram(
       {{GL_VERTEX_SHADER, ":/gl/geo_texture2d.vert"},
        {GL_GEOMETRY_SHADER, ":/gl/threshold.geom"},
@@ -181,88 +180,95 @@ void GeoIcons::Initialize()
    p->uSelectedTimeLocation_ =
       p->shaderProgram_->GetUniformLocation("uSelectedTime");
 
-   gl.glGenVertexArrays(1, &p->vao_);
-   gl.glGenBuffers(static_cast(p->vbo_.size()), p->vbo_.data());
+   glGenVertexArrays(1, &p->vao_);
+   glGenBuffers(static_cast(p->vbo_.size()), p->vbo_.data());
 
-   gl.glBindVertexArray(p->vao_);
-   gl.glBindBuffer(GL_ARRAY_BUFFER, p->vbo_[0]);
-   gl.glBufferData(GL_ARRAY_BUFFER, 0u, nullptr, GL_DYNAMIC_DRAW);
+   glBindVertexArray(p->vao_);
+   glBindBuffer(GL_ARRAY_BUFFER, p->vbo_[0]);
+   glBufferData(GL_ARRAY_BUFFER, 0u, nullptr, GL_DYNAMIC_DRAW);
+
+   // NOLINTBEGIN(modernize-use-nullptr)
+   // NOLINTBEGIN(performance-no-int-to-ptr)
+   // NOLINTBEGIN(cppcoreguidelines-avoid-magic-numbers)
 
    // aLatLong
-   gl.glVertexAttribPointer(0,
-                            2,
-                            GL_FLOAT,
-                            GL_FALSE,
-                            kPointsPerVertex * sizeof(float),
-                            static_cast(0));
-   gl.glEnableVertexAttribArray(0);
+   glVertexAttribPointer(0,
+                         2,
+                         GL_FLOAT,
+                         GL_FALSE,
+                         kPointsPerVertex * sizeof(float),
+                         static_cast(0));
+   glEnableVertexAttribArray(0);
 
    // aXYOffset
-   gl.glVertexAttribPointer(1,
-                            2,
-                            GL_FLOAT,
-                            GL_FALSE,
-                            kPointsPerVertex * sizeof(float),
-                            reinterpret_cast(2 * sizeof(float)));
-   gl.glEnableVertexAttribArray(1);
+   glVertexAttribPointer(1,
+                         2,
+                         GL_FLOAT,
+                         GL_FALSE,
+                         kPointsPerVertex * sizeof(float),
+                         reinterpret_cast(2 * sizeof(float)));
+   glEnableVertexAttribArray(1);
 
    // aModulate
-   gl.glVertexAttribPointer(3,
-                            4,
-                            GL_FLOAT,
-                            GL_FALSE,
-                            kPointsPerVertex * sizeof(float),
-                            reinterpret_cast(4 * sizeof(float)));
-   gl.glEnableVertexAttribArray(3);
+   glVertexAttribPointer(3,
+                         4,
+                         GL_FLOAT,
+                         GL_FALSE,
+                         kPointsPerVertex * sizeof(float),
+                         reinterpret_cast(4 * sizeof(float)));
+   glEnableVertexAttribArray(3);
 
    // aAngle
-   gl.glVertexAttribPointer(4,
-                            1,
-                            GL_FLOAT,
-                            GL_FALSE,
-                            kPointsPerVertex * sizeof(float),
-                            reinterpret_cast(8 * sizeof(float)));
-   gl.glEnableVertexAttribArray(4);
+   glVertexAttribPointer(4,
+                         1,
+                         GL_FLOAT,
+                         GL_FALSE,
+                         kPointsPerVertex * sizeof(float),
+                         reinterpret_cast(8 * sizeof(float)));
+   glEnableVertexAttribArray(4);
 
-   gl.glBindBuffer(GL_ARRAY_BUFFER, p->vbo_[1]);
-   gl.glBufferData(GL_ARRAY_BUFFER, 0u, nullptr, GL_DYNAMIC_DRAW);
+   glBindBuffer(GL_ARRAY_BUFFER, p->vbo_[1]);
+   glBufferData(GL_ARRAY_BUFFER, 0u, nullptr, GL_DYNAMIC_DRAW);
 
    // aTexCoord
-   gl.glVertexAttribPointer(2,
-                            3,
-                            GL_FLOAT,
-                            GL_FALSE,
-                            kPointsPerTexCoord * sizeof(float),
-                            static_cast(0));
-   gl.glEnableVertexAttribArray(2);
+   glVertexAttribPointer(2,
+                         3,
+                         GL_FLOAT,
+                         GL_FALSE,
+                         kPointsPerTexCoord * sizeof(float),
+                         static_cast(0));
+   glEnableVertexAttribArray(2);
 
-   gl.glBindBuffer(GL_ARRAY_BUFFER, p->vbo_[2]);
-   gl.glBufferData(GL_ARRAY_BUFFER, 0u, nullptr, GL_DYNAMIC_DRAW);
+   glBindBuffer(GL_ARRAY_BUFFER, p->vbo_[2]);
+   glBufferData(GL_ARRAY_BUFFER, 0u, nullptr, GL_DYNAMIC_DRAW);
 
    // aThreshold
-   gl.glVertexAttribIPointer(5, //
-                             1,
-                             GL_INT,
-                             0,
-                             static_cast(0));
-   gl.glEnableVertexAttribArray(5);
+   glVertexAttribIPointer(5, //
+                          1,
+                          GL_INT,
+                          0,
+                          static_cast(0));
+   glEnableVertexAttribArray(5);
 
    // aTimeRange
-   gl.glVertexAttribIPointer(6, //
-                             2,
-                             GL_INT,
-                             kIntegersPerVertex_ * sizeof(GLint),
-                             reinterpret_cast(1 * sizeof(GLint)));
-   gl.glEnableVertexAttribArray(6);
+   glVertexAttribIPointer(6, //
+                          2,
+                          GL_INT,
+                          kIntegersPerVertex_ * sizeof(GLint),
+                          reinterpret_cast(1 * sizeof(GLint)));
+   glEnableVertexAttribArray(6);
 
    // aDisplayed
-   gl.glVertexAttribPointer(7,
-                            1,
-                            GL_INT,
-                            GL_FALSE,
-                            kIntegersPerVertex_ * sizeof(GLint),
-                            reinterpret_cast(3 * sizeof(float)));
-   gl.glEnableVertexAttribArray(7);
+   glVertexAttribIPointer(7,
+                          1,
+                          GL_INT,
+                          kIntegersPerVertex_ * sizeof(GLint),
+                          reinterpret_cast(3 * sizeof(GLint)));
+   glEnableVertexAttribArray(7);
+
+   // NOLINTEND(cppcoreguidelines-avoid-magic-numbers)
+   // NOLINTEND(performance-no-int-to-ptr)
+   // NOLINTEND(modernize-use-nullptr)
 
    p->dirty_ = true;
 }
@@ -284,9 +290,7 @@ void GeoIcons::Render(const QMapLibre::CustomLayerRenderParameters& params,
 
    if (!p->currentIconList_.empty())
    {
-      gl::OpenGLFunctions& gl = p->context_->gl();
-
-      gl.glBindVertexArray(p->vao_);
+      glBindVertexArray(p->vao_);
 
       p->Update(textureAtlasChanged);
       p->shaderProgram_->Use();
@@ -299,40 +303,38 @@ void GeoIcons::Render(const QMapLibre::CustomLayerRenderParameters& params,
          // If thresholding is enabled, set the map distance
          units::length::nautical_miles mapDistance =
             util::maplibre::GetMapDistance(params);
-         gl.glUniform1f(p->uMapDistanceLocation_, mapDistance.value());
+         glUniform1f(p->uMapDistanceLocation_, mapDistance.value());
       }
       else
       {
          // If thresholding is disabled, set the map distance to 0
-         gl.glUniform1f(p->uMapDistanceLocation_, 0.0f);
+         glUniform1f(p->uMapDistanceLocation_, 0.0f);
       }
 
       // Selected time
       std::chrono::system_clock::time_point selectedTime =
          (p->selectedTime_ == std::chrono::system_clock::time_point {}) ?
-            std::chrono::system_clock::now() :
+            scwx::util::time::now() :
             p->selectedTime_;
-      gl.glUniform1i(
+      glUniform1i(
          p->uSelectedTimeLocation_,
          static_cast(std::chrono::duration_cast(
                                selectedTime.time_since_epoch())
                                .count()));
 
       // Interpolate texture coordinates
-      gl.glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
-      gl.glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
+      glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
+      glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
 
       // Draw icons
-      gl.glDrawArrays(GL_TRIANGLES, 0, p->numVertices_);
+      glDrawArrays(GL_TRIANGLES, 0, p->numVertices_);
    }
 }
 
 void GeoIcons::Deinitialize()
 {
-   gl::OpenGLFunctions& gl = p->context_->gl();
-
-   gl.glDeleteVertexArrays(1, &p->vao_);
-   gl.glDeleteBuffers(static_cast(p->vbo_.size()), p->vbo_.data());
+   glDeleteVertexArrays(1, &p->vao_);
+   glDeleteBuffers(static_cast(p->vbo_.size()), p->vbo_.data());
 
    std::unique_lock lock {p->iconMutex_};
 
@@ -692,7 +694,7 @@ void GeoIcons::Impl::UpdateSingleBuffer(
                                hoverIcons.end(),
                                [&di](auto& entry) { return entry.di_ == di; });
 
-   if (di->visible_ && !di->hoverText_.empty())
+   if (di->visible_ && (!di->hoverText_.empty() || di->event_ != nullptr))
    {
       const units::angle::radians radians = angle;
 
@@ -848,8 +850,6 @@ void GeoIcons::Impl::UpdateModifiedIconBuffers()
 
 void GeoIcons::Impl::Update(bool textureAtlasChanged)
 {
-   gl::OpenGLFunctions& gl = context_->gl();
-
    UpdateModifiedIconBuffers();
 
    // If the texture atlas has changed
@@ -865,11 +865,12 @@ void GeoIcons::Impl::Update(bool textureAtlasChanged)
       UpdateTextureBuffer();
 
       // Buffer texture data
-      gl.glBindBuffer(GL_ARRAY_BUFFER, vbo_[1]);
-      gl.glBufferData(GL_ARRAY_BUFFER,
-                      sizeof(float) * textureBuffer_.size(),
-                      textureBuffer_.data(),
-                      GL_DYNAMIC_DRAW);
+      glBindBuffer(GL_ARRAY_BUFFER, vbo_[1]);
+      glBufferData(
+         GL_ARRAY_BUFFER,
+         static_cast(sizeof(float) * textureBuffer_.size()),
+         textureBuffer_.data(),
+         GL_DYNAMIC_DRAW);
 
       lastTextureAtlasChanged_ = false;
    }
@@ -878,18 +879,20 @@ void GeoIcons::Impl::Update(bool textureAtlasChanged)
    if (dirty_)
    {
       // Buffer vertex data
-      gl.glBindBuffer(GL_ARRAY_BUFFER, vbo_[0]);
-      gl.glBufferData(GL_ARRAY_BUFFER,
-                      sizeof(float) * currentIconBuffer_.size(),
-                      currentIconBuffer_.data(),
-                      GL_DYNAMIC_DRAW);
+      glBindBuffer(GL_ARRAY_BUFFER, vbo_[0]);
+      glBufferData(
+         GL_ARRAY_BUFFER,
+         static_cast(sizeof(float) * currentIconBuffer_.size()),
+         currentIconBuffer_.data(),
+         GL_DYNAMIC_DRAW);
 
       // Buffer threshold data
-      gl.glBindBuffer(GL_ARRAY_BUFFER, vbo_[2]);
-      gl.glBufferData(GL_ARRAY_BUFFER,
-                      sizeof(GLint) * currentIntegerBuffer_.size(),
-                      currentIntegerBuffer_.data(),
-                      GL_DYNAMIC_DRAW);
+      glBindBuffer(GL_ARRAY_BUFFER, vbo_[2]);
+      glBufferData(
+         GL_ARRAY_BUFFER,
+         static_cast(sizeof(GLint) * currentIntegerBuffer_.size()),
+         currentIntegerBuffer_.data(),
+         GL_DYNAMIC_DRAW);
 
       numVertices_ =
          static_cast(currentIconBuffer_.size() / kPointsPerVertex);
@@ -904,7 +907,7 @@ bool GeoIcons::RunMousePicking(
    const QPointF&   mouseGlobalPos,
    const glm::vec2& mouseCoords,
    const common::Coordinate& /* mouseGeoCoords */,
-   std::shared_ptr& /* eventHandler */)
+   std::shared_ptr& eventHandler)
 {
    std::unique_lock lock {p->iconMutex_};
 
@@ -928,7 +931,7 @@ bool GeoIcons::RunMousePicking(
    // If no time has been selected, use the current time
    std::chrono::system_clock::time_point selectedTime =
       (p->selectedTime_ == std::chrono::system_clock::time_point {}) ?
-         std::chrono::system_clock::now() :
+         scwx::util::time::now() :
          p->selectedTime_;
 
    // For each pickable icon
@@ -947,8 +950,10 @@ bool GeoIcons::RunMousePicking(
                    units::length::nautical_miles {icon.di_->threshold_}
                       .value())) < 999 &&
 
-                // Map distance is beyond the threshold
-                icon.di_->threshold_ < mapDistance) ||
+                // Map distance is beyond/within the threshold
+                icon.di_->threshold_ < mapDistance &&
+                (icon.di_->threshold_.value() >= 0.0 ||
+                 -(icon.di_->threshold_) > mapDistance)) ||
 
              (
                 // Geo icon has a start time
@@ -992,12 +997,27 @@ bool GeoIcons::RunMousePicking(
    if (it != p->currentHoverIcons_.crend())
    {
       itemPicked = true;
-      util::tooltip::Show(it->di_->hoverText_, mouseGlobalPos);
+      if (!it->di_->hoverText_.empty())
+      {
+         // Show tooltip
+         util::tooltip::Show(it->di_->hoverText_, mouseGlobalPos);
+      }
+      if (it->di_->event_ != nullptr)
+      {
+         eventHandler = it->di_;
+      }
    }
 
    return itemPicked;
 }
 
+void GeoIcons::RegisterEventHandler(
+   const std::shared_ptr& di,
+   const std::function&     eventHandler)
+{
+   di->event_ = eventHandler;
+}
+
 } // namespace draw
 } // namespace gl
 } // namespace qt
diff --git a/scwx-qt/source/scwx/qt/gl/draw/geo_icons.hpp b/scwx-qt/source/scwx/qt/gl/draw/geo_icons.hpp
index 4d819681..073fc118 100644
--- a/scwx-qt/source/scwx/qt/gl/draw/geo_icons.hpp
+++ b/scwx-qt/source/scwx/qt/gl/draw/geo_icons.hpp
@@ -183,6 +183,16 @@ public:
     */
    void FinishIcons();
 
+   /**
+    * Registers an event handler for an icon.
+    *
+    * @param [in] di Icon draw item
+    * @param [in] eventHandler Event handler function
+    */
+   static void
+   RegisterEventHandler(const std::shared_ptr& di,
+                        const std::function&     eventHandler);
+
 private:
    class Impl;
 
diff --git a/scwx-qt/source/scwx/qt/gl/draw/geo_lines.cpp b/scwx-qt/source/scwx/qt/gl/draw/geo_lines.cpp
index d9f2c023..e35cca4f 100644
--- a/scwx-qt/source/scwx/qt/gl/draw/geo_lines.cpp
+++ b/scwx-qt/source/scwx/qt/gl/draw/geo_lines.cpp
@@ -3,6 +3,7 @@
 #include 
 #include 
 #include 
+#include 
 
 #include 
 
@@ -50,6 +51,7 @@ struct GeoLineDrawItem : types::EventHandler
    units::angle::degrees angle_ {};
    std::string                  hoverText_ {};
    GeoLines::HoverCallback      hoverCallback_ {nullptr};
+   size_t                       lineIndex_ {0};
 };
 
 class GeoLines::Impl
@@ -87,10 +89,10 @@ public:
    void UpdateBuffers();
    void UpdateModifiedLineBuffers();
    void UpdateSingleBuffer(const std::shared_ptr& di,
-                           std::size_t                             lineIndex,
                            std::vector&                     linesBuffer,
-                           std::vector&          integerBuffer,
-                           std::vector& hoverLines);
+                           std::vector&                 integerBuffer,
+                           std::unordered_map,
+                                              LineHoverEntry>& hoverLines);
 
    std::shared_ptr context_;
 
@@ -112,8 +114,10 @@ public:
    std::vector newLinesBuffer_ {};
    std::vector newIntegerBuffer_ {};
 
-   std::vector currentHoverLines_ {};
-   std::vector newHoverLines_ {};
+   std::unordered_map, LineHoverEntry>
+      currentHoverLines_ {};
+   std::unordered_map, LineHoverEntry>
+      newHoverLines_ {};
 
    std::shared_ptr shaderProgram_;
    GLint                          uMVPMatrixLocation_;
@@ -127,7 +131,7 @@ public:
 };
 
 GeoLines::GeoLines(std::shared_ptr context) :
-    DrawItem(context->gl()), p(std::make_unique(context))
+    DrawItem(), p(std::make_unique(context))
 {
 }
 GeoLines::~GeoLines() = default;
@@ -148,8 +152,6 @@ void GeoLines::set_thresholded(bool thresholded)
 
 void GeoLines::Initialize()
 {
-   gl::OpenGLFunctions& gl = p->context_->gl();
-
    p->shaderProgram_ = p->context_->GetShaderProgram(
       {{GL_VERTEX_SHADER, ":/gl/geo_texture2d.vert"},
        {GL_GEOMETRY_SHADER, ":/gl/threshold.geom"},
@@ -164,79 +166,86 @@ void GeoLines::Initialize()
    p->uSelectedTimeLocation_ =
       p->shaderProgram_->GetUniformLocation("uSelectedTime");
 
-   gl.glGenVertexArrays(1, &p->vao_);
-   gl.glGenBuffers(static_cast(p->vbo_.size()), p->vbo_.data());
+   glGenVertexArrays(1, &p->vao_);
+   glGenBuffers(static_cast(p->vbo_.size()), p->vbo_.data());
 
-   gl.glBindVertexArray(p->vao_);
-   gl.glBindBuffer(GL_ARRAY_BUFFER, p->vbo_[0]);
-   gl.glBufferData(GL_ARRAY_BUFFER,
-                   sizeof(float) * kLineBufferLength_,
-                   nullptr,
-                   GL_DYNAMIC_DRAW);
+   glBindVertexArray(p->vao_);
+   glBindBuffer(GL_ARRAY_BUFFER, p->vbo_[0]);
+   glBufferData(GL_ARRAY_BUFFER,
+                sizeof(float) * kLineBufferLength_,
+                nullptr,
+                GL_DYNAMIC_DRAW);
+
+   // NOLINTBEGIN(modernize-use-nullptr)
+   // NOLINTBEGIN(performance-no-int-to-ptr)
+   // NOLINTBEGIN(cppcoreguidelines-avoid-magic-numbers)
 
    // aLatLong
-   gl.glVertexAttribPointer(0,
-                            2,
-                            GL_FLOAT,
-                            GL_FALSE,
-                            kPointsPerVertex * sizeof(float),
-                            static_cast(0));
-   gl.glEnableVertexAttribArray(0);
+   glVertexAttribPointer(0,
+                         2,
+                         GL_FLOAT,
+                         GL_FALSE,
+                         kPointsPerVertex * sizeof(float),
+                         static_cast(0));
+   glEnableVertexAttribArray(0);
 
    // aXYOffset
-   gl.glVertexAttribPointer(1,
-                            2,
-                            GL_FLOAT,
-                            GL_FALSE,
-                            kPointsPerVertex * sizeof(float),
-                            reinterpret_cast(2 * sizeof(float)));
-   gl.glEnableVertexAttribArray(1);
+   glVertexAttribPointer(1,
+                         2,
+                         GL_FLOAT,
+                         GL_FALSE,
+                         kPointsPerVertex * sizeof(float),
+                         reinterpret_cast(2 * sizeof(float)));
+   glEnableVertexAttribArray(1);
 
    // aModulate
-   gl.glVertexAttribPointer(3,
-                            4,
-                            GL_FLOAT,
-                            GL_FALSE,
-                            kPointsPerVertex * sizeof(float),
-                            reinterpret_cast(4 * sizeof(float)));
-   gl.glEnableVertexAttribArray(3);
+   glVertexAttribPointer(3,
+                         4,
+                         GL_FLOAT,
+                         GL_FALSE,
+                         kPointsPerVertex * sizeof(float),
+                         reinterpret_cast(4 * sizeof(float)));
+   glEnableVertexAttribArray(3);
 
    // aAngle
-   gl.glVertexAttribPointer(4,
-                            1,
-                            GL_FLOAT,
-                            GL_FALSE,
-                            kPointsPerVertex * sizeof(float),
-                            reinterpret_cast(8 * sizeof(float)));
-   gl.glEnableVertexAttribArray(4);
+   glVertexAttribPointer(4,
+                         1,
+                         GL_FLOAT,
+                         GL_FALSE,
+                         kPointsPerVertex * sizeof(float),
+                         reinterpret_cast(8 * sizeof(float)));
+   glEnableVertexAttribArray(4);
 
-   gl.glBindBuffer(GL_ARRAY_BUFFER, p->vbo_[1]);
-   gl.glBufferData(GL_ARRAY_BUFFER, 0u, nullptr, GL_DYNAMIC_DRAW);
+   glBindBuffer(GL_ARRAY_BUFFER, p->vbo_[1]);
+   glBufferData(GL_ARRAY_BUFFER, 0u, nullptr, GL_DYNAMIC_DRAW);
 
    // aThreshold
-   gl.glVertexAttribIPointer(5, //
-                             1,
-                             GL_INT,
-                             kIntegersPerVertex_ * sizeof(GLint),
-                             static_cast(0));
-   gl.glEnableVertexAttribArray(5);
+   glVertexAttribIPointer(5, //
+                          1,
+                          GL_INT,
+                          kIntegersPerVertex_ * sizeof(GLint),
+                          static_cast(0));
+   glEnableVertexAttribArray(5);
 
    // aTimeRange
-   gl.glVertexAttribIPointer(6, //
-                             2,
-                             GL_INT,
-                             kIntegersPerVertex_ * sizeof(GLint),
-                             reinterpret_cast(1 * sizeof(GLint)));
-   gl.glEnableVertexAttribArray(6);
+   glVertexAttribIPointer(6, //
+                          2,
+                          GL_INT,
+                          kIntegersPerVertex_ * sizeof(GLint),
+                          reinterpret_cast(1 * sizeof(GLint)));
+   glEnableVertexAttribArray(6);
 
    // aDisplayed
-   gl.glVertexAttribPointer(7,
-                            1,
-                            GL_INT,
-                            GL_FALSE,
-                            kIntegersPerVertex_ * sizeof(GLint),
-                            reinterpret_cast(3 * sizeof(float)));
-   gl.glEnableVertexAttribArray(7);
+   glVertexAttribIPointer(7,
+                          1,
+                          GL_INT,
+                          kIntegersPerVertex_ * sizeof(GLint),
+                          reinterpret_cast(3 * sizeof(GLint)));
+   glEnableVertexAttribArray(7);
+
+   // NOLINTEND(cppcoreguidelines-avoid-magic-numbers)
+   // NOLINTEND(performance-no-int-to-ptr)
+   // NOLINTEND(modernize-use-nullptr)
 
    p->dirty_ = true;
 }
@@ -252,9 +261,7 @@ void GeoLines::Render(const QMapLibre::CustomLayerRenderParameters& params)
 
    if (p->newLineList_.size() > 0)
    {
-      gl::OpenGLFunctions& gl = p->context_->gl();
-
-      gl.glBindVertexArray(p->vao_);
+      glBindVertexArray(p->vao_);
 
       p->Update();
       p->shaderProgram_->Use();
@@ -267,39 +274,37 @@ void GeoLines::Render(const QMapLibre::CustomLayerRenderParameters& params)
          // If thresholding is enabled, set the map distance
          units::length::nautical_miles mapDistance =
             util::maplibre::GetMapDistance(params);
-         gl.glUniform1f(p->uMapDistanceLocation_, mapDistance.value());
+         glUniform1f(p->uMapDistanceLocation_, mapDistance.value());
       }
       else
       {
          // If thresholding is disabled, set the map distance to 0
-         gl.glUniform1f(p->uMapDistanceLocation_, 0.0f);
+         glUniform1f(p->uMapDistanceLocation_, 0.0f);
       }
 
       // Selected time
       std::chrono::system_clock::time_point selectedTime =
          (p->selectedTime_ == std::chrono::system_clock::time_point {}) ?
-            std::chrono::system_clock::now() :
+            scwx::util::time::now() :
             p->selectedTime_;
-      gl.glUniform1i(
+      glUniform1i(
          p->uSelectedTimeLocation_,
          static_cast(std::chrono::duration_cast(
                                selectedTime.time_since_epoch())
                                .count()));
 
       // Draw icons
-      gl.glDrawArrays(GL_TRIANGLES,
-                      0,
-                      static_cast(p->currentLineList_.size() *
-                                           kVerticesPerRectangle));
+      glDrawArrays(GL_TRIANGLES,
+                   0,
+                   static_cast(p->currentLineList_.size() *
+                                        kVerticesPerRectangle));
    }
 }
 
 void GeoLines::Deinitialize()
 {
-   gl::OpenGLFunctions& gl = p->context_->gl();
-
-   gl.glDeleteVertexArrays(1, &p->vao_);
-   gl.glDeleteBuffers(static_cast(p->vbo_.size()), p->vbo_.data());
+   glDeleteVertexArrays(1, &p->vao_);
+   glDeleteBuffers(static_cast(p->vbo_.size()), p->vbo_.data());
 
    std::unique_lock lock {p->lineMutex_};
 
@@ -324,7 +329,9 @@ void GeoLines::StartLines()
 
 std::shared_ptr GeoLines::AddLine()
 {
-   return p->newLineList_.emplace_back(std::make_shared());
+   auto& di = p->newLineList_.emplace_back(std::make_shared());
+   di->lineIndex_ = p->newLineList_.size() - 1;
+   return di;
 }
 
 void GeoLines::SetLineLocation(const std::shared_ptr& di,
@@ -471,7 +478,7 @@ void GeoLines::Impl::UpdateBuffers()
 
       // Update line buffer
       UpdateSingleBuffer(
-         di, i, newLinesBuffer_, newIntegerBuffer_, newHoverLines_);
+         di, newLinesBuffer_, newIntegerBuffer_, newHoverLines_);
    }
 
    // All lines have been updated
@@ -489,23 +496,15 @@ void GeoLines::Impl::UpdateModifiedLineBuffers()
    // Update buffers for modified lines
    for (auto& di : dirtyLines_)
    {
-      // Find modified line in the current list
-      auto it =
-         std::find(currentLineList_.cbegin(), currentLineList_.cend(), di);
-
-      // Ignore invalid lines
-      if (it == currentLineList_.cend())
+      // Check if modified line is in the current list
+      if (di->lineIndex_ >= currentLineList_.size() ||
+          currentLineList_[di->lineIndex_] != di)
       {
          continue;
       }
 
-      auto lineIndex = std::distance(currentLineList_.cbegin(), it);
-
-      UpdateSingleBuffer(di,
-                         lineIndex,
-                         currentLinesBuffer_,
-                         currentIntegerBuffer_,
-                         currentHoverLines_);
+      UpdateSingleBuffer(
+         di, currentLinesBuffer_, currentIntegerBuffer_, currentHoverLines_);
    }
 
    // Clear list of modified lines
@@ -518,10 +517,10 @@ void GeoLines::Impl::UpdateModifiedLineBuffers()
 
 void GeoLines::Impl::UpdateSingleBuffer(
    const std::shared_ptr& di,
-   std::size_t                             lineIndex,
    std::vector&                     lineBuffer,
    std::vector&                     integerBuffer,
-   std::vector&            hoverLines)
+   std::unordered_map, LineHoverEntry>&
+      hoverLines)
 {
    // Threshold value
    units::length::nautical_miles threshold = di->threshold_;
@@ -589,10 +588,10 @@ void GeoLines::Impl::UpdateSingleBuffer(
 
    // Buffer position data
    auto lineBufferPosition = lineBuffer.end();
-   auto lineBufferOffset   = lineIndex * kLineBufferLength_;
+   auto lineBufferOffset   = di->lineIndex_ * kLineBufferLength_;
 
    auto integerBufferPosition = integerBuffer.end();
-   auto integerBufferOffset   = lineIndex * kIntegerBufferLength_;
+   auto integerBufferOffset   = di->lineIndex_ * kIntegerBufferLength_;
 
    if (lineBufferOffset < lineBuffer.size())
    {
@@ -621,9 +620,7 @@ void GeoLines::Impl::UpdateSingleBuffer(
       std::copy(integerData.begin(), integerData.end(), integerBufferPosition);
    }
 
-   auto hoverIt = std::find_if(hoverLines.begin(),
-                               hoverLines.end(),
-                               [&di](auto& entry) { return entry.di_ == di; });
+   auto hoverIt = hoverLines.find(di);
 
    if (di->visible_ && (!di->hoverText_.empty() ||
                         di->hoverCallback_ != nullptr || di->event_ != nullptr))
@@ -645,17 +642,23 @@ void GeoLines::Impl::UpdateSingleBuffer(
 
       if (hoverIt == hoverLines.end())
       {
-         hoverLines.emplace_back(
-            LineHoverEntry {di, sc1, sc2, otl, otr, obl, obr});
+         hoverLines.emplace(di,
+                            LineHoverEntry {.di_  = di,
+                                            .p1_  = sc1,
+                                            .p2_  = sc2,
+                                            .otl_ = otl,
+                                            .otr_ = otr,
+                                            .obl_ = obl,
+                                            .obr_ = obr});
       }
       else
       {
-         hoverIt->p1_  = sc1;
-         hoverIt->p2_  = sc2;
-         hoverIt->otl_ = otl;
-         hoverIt->otr_ = otr;
-         hoverIt->obl_ = obl;
-         hoverIt->obr_ = obr;
+         hoverIt->second.p1_  = sc1;
+         hoverIt->second.p2_  = sc2;
+         hoverIt->second.otl_ = otl;
+         hoverIt->second.otr_ = otr;
+         hoverIt->second.obl_ = obl;
+         hoverIt->second.obr_ = obr;
       }
    }
    else if (hoverIt != hoverLines.end())
@@ -671,21 +674,21 @@ void GeoLines::Impl::Update()
    // If the lines have been updated
    if (dirty_)
    {
-      gl::OpenGLFunctions& gl = context_->gl();
-
       // Buffer lines data
-      gl.glBindBuffer(GL_ARRAY_BUFFER, vbo_[0]);
-      gl.glBufferData(GL_ARRAY_BUFFER,
-                      sizeof(float) * currentLinesBuffer_.size(),
-                      currentLinesBuffer_.data(),
-                      GL_DYNAMIC_DRAW);
+      glBindBuffer(GL_ARRAY_BUFFER, vbo_[0]);
+      glBufferData(
+         GL_ARRAY_BUFFER,
+         static_cast(sizeof(float) * currentLinesBuffer_.size()),
+         currentLinesBuffer_.data(),
+         GL_DYNAMIC_DRAW);
 
       // Buffer threshold data
-      gl.glBindBuffer(GL_ARRAY_BUFFER, vbo_[1]);
-      gl.glBufferData(GL_ARRAY_BUFFER,
-                      sizeof(GLint) * currentIntegerBuffer_.size(),
-                      currentIntegerBuffer_.data(),
-                      GL_DYNAMIC_DRAW);
+      glBindBuffer(GL_ARRAY_BUFFER, vbo_[1]);
+      glBufferData(
+         GL_ARRAY_BUFFER,
+         static_cast(sizeof(GLint) * currentIntegerBuffer_.size()),
+         currentIntegerBuffer_.data(),
+         GL_DYNAMIC_DRAW);
    }
 
    dirty_ = false;
@@ -721,16 +724,18 @@ bool GeoLines::RunMousePicking(
    // If no time has been selected, use the current time
    std::chrono::system_clock::time_point selectedTime =
       (p->selectedTime_ == std::chrono::system_clock::time_point {}) ?
-         std::chrono::system_clock::now() :
+         scwx::util::time::now() :
          p->selectedTime_;
 
    // For each pickable line
    auto it = std::find_if(
       std::execution::par_unseq,
-      p->currentHoverLines_.rbegin(),
-      p->currentHoverLines_.rend(),
-      [&mapDistance, &selectedTime, &mapMatrix, &mouseCoords](const auto& line)
+      p->currentHoverLines_.cbegin(),
+      p->currentHoverLines_.cend(),
+      [&mapDistance, &selectedTime, &mapMatrix, &mouseCoords](
+         const auto& lineIt)
       {
+         const auto& line = lineIt.second;
          if ((
                 // Placefile is thresholded
                 mapDistance > units::length::meters {0.0} &&
@@ -740,8 +745,10 @@ bool GeoLines::RunMousePicking(
                    units::length::nautical_miles {line.di_->threshold_}
                       .value())) < 999 &&
 
-                // Map distance is beyond the threshold
-                line.di_->threshold_ < mapDistance) ||
+                // Map distance is beyond/within the threshold
+                line.di_->threshold_ < mapDistance &&
+                (line.di_->threshold_.value() >= 0.0 ||
+                 -(line.di_->threshold_) > mapDistance)) ||
 
              (
                 // Line has a start time
@@ -784,24 +791,24 @@ bool GeoLines::RunMousePicking(
          return util::maplibre::IsPointInPolygon({tl, bl, br, tr}, mouseCoords);
       });
 
-   if (it != p->currentHoverLines_.crend())
+   if (it != p->currentHoverLines_.cend())
    {
       itemPicked = true;
 
-      if (!it->di_->hoverText_.empty())
+      if (!it->second.di_->hoverText_.empty())
       {
          // Show tooltip
-         util::tooltip::Show(it->di_->hoverText_, mouseGlobalPos);
+         util::tooltip::Show(it->second.di_->hoverText_, mouseGlobalPos);
       }
-      else if (it->di_->hoverCallback_ != nullptr)
+      else if (it->second.di_->hoverCallback_ != nullptr)
       {
-         it->di_->hoverCallback_(it->di_, mouseGlobalPos);
+         it->second.di_->hoverCallback_(it->second.di_, mouseGlobalPos);
       }
 
-      if (it->di_->event_ != nullptr)
+      if (it->second.di_->event_ != nullptr)
       {
          // Register event handler
-         eventHandler = it->di_;
+         eventHandler = it->second.di_;
       }
    }
 
diff --git a/scwx-qt/source/scwx/qt/gl/draw/geo_lines.hpp b/scwx-qt/source/scwx/qt/gl/draw/geo_lines.hpp
index b7b792d0..d6727110 100644
--- a/scwx-qt/source/scwx/qt/gl/draw/geo_lines.hpp
+++ b/scwx-qt/source/scwx/qt/gl/draw/geo_lines.hpp
@@ -19,9 +19,8 @@ struct GeoLineDrawItem;
 class GeoLines : public DrawItem
 {
 public:
-   typedef std::function&,
-                              const QPointF&)>
-      HoverCallback;
+   using HoverCallback = std::function&, const QPointF&)>;
 
    explicit GeoLines(std::shared_ptr context);
    ~GeoLines();
diff --git a/scwx-qt/source/scwx/qt/gl/draw/icons.cpp b/scwx-qt/source/scwx/qt/gl/draw/icons.cpp
index 473b8a79..b72f4443 100644
--- a/scwx-qt/source/scwx/qt/gl/draw/icons.cpp
+++ b/scwx-qt/source/scwx/qt/gl/draw/icons.cpp
@@ -21,12 +21,11 @@ namespace draw
 static const std::string logPrefix_ = "scwx::qt::gl::draw::icons";
 static const auto        logger_    = scwx::util::Logger::Create(logPrefix_);
 
-static constexpr std::size_t kNumRectangles        = 1;
-static constexpr std::size_t kNumTriangles         = kNumRectangles * 2;
-static constexpr std::size_t kVerticesPerTriangle  = 3;
-static constexpr std::size_t kVerticesPerRectangle = kVerticesPerTriangle * 2;
-static constexpr std::size_t kPointsPerVertex      = 10;
-static constexpr std::size_t kPointsPerTexCoord    = 3;
+static constexpr std::size_t kNumRectangles       = 1;
+static constexpr std::size_t kNumTriangles        = kNumRectangles * 2;
+static constexpr std::size_t kVerticesPerTriangle = 3;
+static constexpr std::size_t kPointsPerVertex     = 10;
+static constexpr std::size_t kPointsPerTexCoord   = 3;
 static constexpr std::size_t kIconBufferLength =
    kNumTriangles * kVerticesPerTriangle * kPointsPerVertex;
 static constexpr std::size_t kTextureBufferLength =
@@ -117,7 +116,7 @@ public:
 };
 
 Icons::Icons(const std::shared_ptr& context) :
-    DrawItem(context->gl()), p(std::make_unique(context))
+    DrawItem(), p(std::make_unique(context))
 {
 }
 Icons::~Icons() = default;
@@ -127,8 +126,6 @@ Icons& Icons::operator=(Icons&&) noexcept = default;
 
 void Icons::Initialize()
 {
-   gl::OpenGLFunctions& gl = p->context_->gl();
-
    p->shaderProgram_ = p->context_->GetShaderProgram(
       {{GL_VERTEX_SHADER, ":/gl/texture2d_array.vert"},
        {GL_GEOMETRY_SHADER, ":/gl/threshold.geom"},
@@ -136,69 +133,77 @@ void Icons::Initialize()
 
    p->uMVPMatrixLocation_ = p->shaderProgram_->GetUniformLocation("uMVPMatrix");
 
-   gl.glGenVertexArrays(1, &p->vao_);
-   gl.glGenBuffers(static_cast(p->vbo_.size()), p->vbo_.data());
+   glGenVertexArrays(1, &p->vao_);
+   glGenBuffers(static_cast(p->vbo_.size()), p->vbo_.data());
 
-   gl.glBindVertexArray(p->vao_);
-   gl.glBindBuffer(GL_ARRAY_BUFFER, p->vbo_[0]);
-   gl.glBufferData(GL_ARRAY_BUFFER, 0u, nullptr, GL_DYNAMIC_DRAW);
+   glBindVertexArray(p->vao_);
+   glBindBuffer(GL_ARRAY_BUFFER, p->vbo_[0]);
+   glBufferData(GL_ARRAY_BUFFER, 0u, nullptr, GL_DYNAMIC_DRAW);
+
+   // NOLINTBEGIN(modernize-use-nullptr)
+   // NOLINTBEGIN(performance-no-int-to-ptr)
+   // NOLINTBEGIN(cppcoreguidelines-avoid-magic-numbers)
 
    // aVertex
-   gl.glVertexAttribPointer(0,
-                            2,
-                            GL_FLOAT,
-                            GL_FALSE,
-                            kPointsPerVertex * sizeof(float),
-                            reinterpret_cast(0));
-   gl.glEnableVertexAttribArray(0);
+   glVertexAttribPointer(0,
+                         2,
+                         GL_FLOAT,
+                         GL_FALSE,
+                         kPointsPerVertex * sizeof(float),
+                         reinterpret_cast(0));
+   glEnableVertexAttribArray(0);
 
    // aXYOffset
-   gl.glVertexAttribPointer(1,
-                            2,
-                            GL_FLOAT,
-                            GL_FALSE,
-                            kPointsPerVertex * sizeof(float),
-                            reinterpret_cast(2 * sizeof(float)));
-   gl.glEnableVertexAttribArray(1);
+   glVertexAttribPointer(1,
+                         2,
+                         GL_FLOAT,
+                         GL_FALSE,
+                         kPointsPerVertex * sizeof(float),
+                         reinterpret_cast(2 * sizeof(float)));
+   glEnableVertexAttribArray(1);
 
    // aModulate
-   gl.glVertexAttribPointer(3,
-                            4,
-                            GL_FLOAT,
-                            GL_FALSE,
-                            kPointsPerVertex * sizeof(float),
-                            reinterpret_cast(4 * sizeof(float)));
-   gl.glEnableVertexAttribArray(3);
+   glVertexAttribPointer(3,
+                         4,
+                         GL_FLOAT,
+                         GL_FALSE,
+                         kPointsPerVertex * sizeof(float),
+                         reinterpret_cast(4 * sizeof(float)));
+   glEnableVertexAttribArray(3);
 
    // aAngle
-   gl.glVertexAttribPointer(4,
-                            1,
-                            GL_FLOAT,
-                            GL_FALSE,
-                            kPointsPerVertex * sizeof(float),
-                            reinterpret_cast(8 * sizeof(float)));
-   gl.glEnableVertexAttribArray(4);
+   glVertexAttribPointer(4,
+                         1,
+                         GL_FLOAT,
+                         GL_FALSE,
+                         kPointsPerVertex * sizeof(float),
+                         reinterpret_cast(8 * sizeof(float)));
+   glEnableVertexAttribArray(4);
 
    // aDisplayed
-   gl.glVertexAttribPointer(5,
-                            1,
-                            GL_FLOAT,
-                            GL_FALSE,
-                            kPointsPerVertex * sizeof(float),
-                            reinterpret_cast(9 * sizeof(float)));
-   gl.glEnableVertexAttribArray(5);
+   glVertexAttribPointer(5,
+                         1,
+                         GL_FLOAT,
+                         GL_FALSE,
+                         kPointsPerVertex * sizeof(float),
+                         reinterpret_cast(9 * sizeof(float)));
+   glEnableVertexAttribArray(5);
 
-   gl.glBindBuffer(GL_ARRAY_BUFFER, p->vbo_[1]);
-   gl.glBufferData(GL_ARRAY_BUFFER, 0u, nullptr, GL_DYNAMIC_DRAW);
+   glBindBuffer(GL_ARRAY_BUFFER, p->vbo_[1]);
+   glBufferData(GL_ARRAY_BUFFER, 0u, nullptr, GL_DYNAMIC_DRAW);
 
    // aTexCoord
-   gl.glVertexAttribPointer(2,
-                            3,
-                            GL_FLOAT,
-                            GL_FALSE,
-                            kPointsPerTexCoord * sizeof(float),
-                            static_cast(0));
-   gl.glEnableVertexAttribArray(2);
+   glVertexAttribPointer(2,
+                         3,
+                         GL_FLOAT,
+                         GL_FALSE,
+                         kPointsPerTexCoord * sizeof(float),
+                         static_cast(0));
+   glEnableVertexAttribArray(2);
+
+   // NOLINTEND(cppcoreguidelines-avoid-magic-numbers)
+   // NOLINTEND(performance-no-int-to-ptr)
+   // NOLINTEND(modernize-use-nullptr)
 
    p->dirty_ = true;
 }
@@ -220,29 +225,25 @@ void Icons::Render(const QMapLibre::CustomLayerRenderParameters& params,
 
    if (!p->currentIconList_.empty())
    {
-      gl::OpenGLFunctions& gl = p->context_->gl();
-
-      gl.glBindVertexArray(p->vao_);
+      glBindVertexArray(p->vao_);
 
       p->Update(textureAtlasChanged);
       p->shaderProgram_->Use();
       UseDefaultProjection(params, p->uMVPMatrixLocation_);
 
       // Interpolate texture coordinates
-      gl.glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
-      gl.glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
+      glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
+      glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
 
       // Draw icons
-      gl.glDrawArrays(GL_TRIANGLES, 0, p->numVertices_);
+      glDrawArrays(GL_TRIANGLES, 0, p->numVertices_);
    }
 }
 
 void Icons::Deinitialize()
 {
-   gl::OpenGLFunctions& gl = p->context_->gl();
-
-   gl.glDeleteVertexArrays(1, &p->vao_);
-   gl.glDeleteBuffers(static_cast(p->vbo_.size()), p->vbo_.data());
+   glDeleteVertexArrays(1, &p->vao_);
+   glDeleteBuffers(static_cast(p->vbo_.size()), p->vbo_.data());
 
    std::unique_lock lock {p->iconMutex_};
 
@@ -680,8 +681,6 @@ void Icons::Impl::UpdateModifiedIconBuffers()
 
 void Icons::Impl::Update(bool textureAtlasChanged)
 {
-   gl::OpenGLFunctions& gl = context_->gl();
-
    UpdateModifiedIconBuffers();
 
    // If the texture atlas has changed
@@ -697,11 +696,12 @@ void Icons::Impl::Update(bool textureAtlasChanged)
       UpdateTextureBuffer();
 
       // Buffer texture data
-      gl.glBindBuffer(GL_ARRAY_BUFFER, vbo_[1]);
-      gl.glBufferData(GL_ARRAY_BUFFER,
-                      sizeof(float) * textureBuffer_.size(),
-                      textureBuffer_.data(),
-                      GL_DYNAMIC_DRAW);
+      glBindBuffer(GL_ARRAY_BUFFER, vbo_[1]);
+      glBufferData(
+         GL_ARRAY_BUFFER,
+         static_cast(sizeof(float) * textureBuffer_.size()),
+         textureBuffer_.data(),
+         GL_DYNAMIC_DRAW);
 
       lastTextureAtlasChanged_ = false;
    }
@@ -710,11 +710,12 @@ void Icons::Impl::Update(bool textureAtlasChanged)
    if (dirty_)
    {
       // Buffer vertex data
-      gl.glBindBuffer(GL_ARRAY_BUFFER, vbo_[0]);
-      gl.glBufferData(GL_ARRAY_BUFFER,
-                      sizeof(float) * currentIconBuffer_.size(),
-                      currentIconBuffer_.data(),
-                      GL_DYNAMIC_DRAW);
+      glBindBuffer(GL_ARRAY_BUFFER, vbo_[0]);
+      glBufferData(
+         GL_ARRAY_BUFFER,
+         static_cast(sizeof(float) * currentIconBuffer_.size()),
+         currentIconBuffer_.data(),
+         GL_DYNAMIC_DRAW);
 
       numVertices_ =
          static_cast(currentIconBuffer_.size() / kPointsPerVertex);
@@ -741,7 +742,7 @@ bool Icons::RunMousePicking(
 
    // For each pickable icon
    auto it = std::find_if( //
-      std::execution::par_unseq,
+      std::execution::par,
       p->currentHoverIcons_.crbegin(),
       p->currentHoverIcons_.crend(),
       [&mouseLocalCoords](const auto& icon)
diff --git a/scwx-qt/source/scwx/qt/gl/draw/linked_vectors.cpp b/scwx-qt/source/scwx/qt/gl/draw/linked_vectors.cpp
index 3dbab117..dfc3dd07 100644
--- a/scwx-qt/source/scwx/qt/gl/draw/linked_vectors.cpp
+++ b/scwx-qt/source/scwx/qt/gl/draw/linked_vectors.cpp
@@ -63,14 +63,12 @@ class LinkedVectors::Impl
 {
 public:
    explicit Impl(std::shared_ptr context) :
-       context_ {context}, geoLines_ {std::make_shared(context)}
+       geoLines_ {std::make_shared(context)}
    {
    }
 
    ~Impl() {}
 
-   std::shared_ptr context_;
-
    bool borderEnabled_ {true};
    bool visible_ {true};
 
@@ -79,7 +77,7 @@ public:
 };
 
 LinkedVectors::LinkedVectors(std::shared_ptr context) :
-    DrawItem(context->gl()), p(std::make_unique(context))
+    DrawItem(), p(std::make_unique(context))
 {
 }
 LinkedVectors::~LinkedVectors() = default;
diff --git a/scwx-qt/source/scwx/qt/gl/draw/placefile_icons.cpp b/scwx-qt/source/scwx/qt/gl/draw/placefile_icons.cpp
index abc852f8..21dd25c2 100644
--- a/scwx-qt/source/scwx/qt/gl/draw/placefile_icons.cpp
+++ b/scwx-qt/source/scwx/qt/gl/draw/placefile_icons.cpp
@@ -3,6 +3,7 @@
 #include 
 #include 
 #include 
+#include 
 
 #include 
 
@@ -140,7 +141,7 @@ public:
 };
 
 PlacefileIcons::PlacefileIcons(const std::shared_ptr& context) :
-    DrawItem(context->gl()), p(std::make_unique(context))
+    DrawItem(), p(std::make_unique(context))
 {
 }
 PlacefileIcons::~PlacefileIcons() = default;
@@ -161,9 +162,6 @@ void PlacefileIcons::set_thresholded(bool thresholded)
 
 void PlacefileIcons::Initialize()
 {
-   gl::OpenGLFunctions& gl   = p->context_->gl();
-   auto&                gl30 = p->context_->gl30();
-
    p->shaderProgram_ = p->context_->GetShaderProgram(
       {{GL_VERTEX_SHADER, ":/gl/geo_texture2d.vert"},
        {GL_GEOMETRY_SHADER, ":/gl/threshold.geom"},
@@ -178,82 +176,90 @@ void PlacefileIcons::Initialize()
    p->uSelectedTimeLocation_ =
       p->shaderProgram_->GetUniformLocation("uSelectedTime");
 
-   gl.glGenVertexArrays(1, &p->vao_);
-   gl.glGenBuffers(static_cast(p->vbo_.size()), p->vbo_.data());
+   glGenVertexArrays(1, &p->vao_);
+   glGenBuffers(static_cast(p->vbo_.size()), p->vbo_.data());
 
-   gl.glBindVertexArray(p->vao_);
-   gl.glBindBuffer(GL_ARRAY_BUFFER, p->vbo_[0]);
-   gl.glBufferData(GL_ARRAY_BUFFER, 0u, nullptr, GL_DYNAMIC_DRAW);
+   glBindVertexArray(p->vao_);
+   glBindBuffer(GL_ARRAY_BUFFER, p->vbo_[0]);
+   glBufferData(GL_ARRAY_BUFFER, 0u, nullptr, GL_DYNAMIC_DRAW);
+
+   // NOLINTBEGIN(modernize-use-nullptr)
+   // NOLINTBEGIN(performance-no-int-to-ptr)
+   // NOLINTBEGIN(cppcoreguidelines-avoid-magic-numbers)
 
    // aLatLong
-   gl.glVertexAttribPointer(0,
-                            2,
-                            GL_FLOAT,
-                            GL_FALSE,
-                            kPointsPerVertex * sizeof(float),
-                            static_cast(0));
-   gl.glEnableVertexAttribArray(0);
+   glVertexAttribPointer(0,
+                         2,
+                         GL_FLOAT,
+                         GL_FALSE,
+                         kPointsPerVertex * sizeof(float),
+                         static_cast(0));
+   glEnableVertexAttribArray(0);
 
    // aXYOffset
-   gl.glVertexAttribPointer(1,
-                            2,
-                            GL_FLOAT,
-                            GL_FALSE,
-                            kPointsPerVertex * sizeof(float),
-                            reinterpret_cast(2 * sizeof(float)));
-   gl.glEnableVertexAttribArray(1);
+   glVertexAttribPointer(1,
+                         2,
+                         GL_FLOAT,
+                         GL_FALSE,
+                         kPointsPerVertex * sizeof(float),
+                         reinterpret_cast(2 * sizeof(float)));
+   glEnableVertexAttribArray(1);
 
    // aModulate
-   gl.glVertexAttribPointer(3,
-                            4,
-                            GL_FLOAT,
-                            GL_FALSE,
-                            kPointsPerVertex * sizeof(float),
-                            reinterpret_cast(4 * sizeof(float)));
-   gl.glEnableVertexAttribArray(3);
+   glVertexAttribPointer(3,
+                         4,
+                         GL_FLOAT,
+                         GL_FALSE,
+                         kPointsPerVertex * sizeof(float),
+                         reinterpret_cast(4 * sizeof(float)));
+   glEnableVertexAttribArray(3);
 
    // aAngle
-   gl.glVertexAttribPointer(4,
-                            1,
-                            GL_FLOAT,
-                            GL_FALSE,
-                            kPointsPerVertex * sizeof(float),
-                            reinterpret_cast(8 * sizeof(float)));
-   gl.glEnableVertexAttribArray(4);
+   glVertexAttribPointer(4,
+                         1,
+                         GL_FLOAT,
+                         GL_FALSE,
+                         kPointsPerVertex * sizeof(float),
+                         reinterpret_cast(8 * sizeof(float)));
+   glEnableVertexAttribArray(4);
 
-   gl.glBindBuffer(GL_ARRAY_BUFFER, p->vbo_[1]);
-   gl.glBufferData(GL_ARRAY_BUFFER, 0u, nullptr, GL_DYNAMIC_DRAW);
+   glBindBuffer(GL_ARRAY_BUFFER, p->vbo_[1]);
+   glBufferData(GL_ARRAY_BUFFER, 0u, nullptr, GL_DYNAMIC_DRAW);
 
    // aTexCoord
-   gl.glVertexAttribPointer(2,
-                            3,
-                            GL_FLOAT,
-                            GL_FALSE,
-                            kPointsPerTexCoord * sizeof(float),
-                            static_cast(0));
-   gl.glEnableVertexAttribArray(2);
+   glVertexAttribPointer(2,
+                         3,
+                         GL_FLOAT,
+                         GL_FALSE,
+                         kPointsPerTexCoord * sizeof(float),
+                         static_cast(0));
+   glEnableVertexAttribArray(2);
 
-   gl.glBindBuffer(GL_ARRAY_BUFFER, p->vbo_[2]);
-   gl.glBufferData(GL_ARRAY_BUFFER, 0u, nullptr, GL_DYNAMIC_DRAW);
+   glBindBuffer(GL_ARRAY_BUFFER, p->vbo_[2]);
+   glBufferData(GL_ARRAY_BUFFER, 0u, nullptr, GL_DYNAMIC_DRAW);
 
    // aThreshold
-   gl.glVertexAttribIPointer(5, //
-                             1,
-                             GL_INT,
-                             0,
-                             static_cast(0));
-   gl.glEnableVertexAttribArray(5);
+   glVertexAttribIPointer(5, //
+                          1,
+                          GL_INT,
+                          0,
+                          static_cast(0));
+   glEnableVertexAttribArray(5);
 
    // aTimeRange
-   gl.glVertexAttribIPointer(6, //
-                             2,
-                             GL_INT,
-                             kIntegersPerVertex_ * sizeof(GLint),
-                             reinterpret_cast(1 * sizeof(GLint)));
-   gl.glEnableVertexAttribArray(6);
+   glVertexAttribIPointer(6, //
+                          2,
+                          GL_INT,
+                          kIntegersPerVertex_ * sizeof(GLint),
+                          reinterpret_cast(1 * sizeof(GLint)));
+   glEnableVertexAttribArray(6);
 
    // aDisplayed
-   gl30.glVertexAttribI1i(7, 1);
+   glVertexAttribI1i(7, 1);
+
+   // NOLINTEND(cppcoreguidelines-avoid-magic-numbers)
+   // NOLINTEND(performance-no-int-to-ptr)
+   // NOLINTEND(modernize-use-nullptr)
 
    p->dirty_ = true;
 }
@@ -266,9 +272,7 @@ void PlacefileIcons::Render(
 
    if (!p->currentIconList_.empty())
    {
-      gl::OpenGLFunctions& gl = p->context_->gl();
-
-      gl.glBindVertexArray(p->vao_);
+      glBindVertexArray(p->vao_);
 
       p->Update(textureAtlasChanged);
       p->shaderProgram_->Use();
@@ -281,40 +285,38 @@ void PlacefileIcons::Render(
          // If thresholding is enabled, set the map distance
          units::length::nautical_miles mapDistance =
             util::maplibre::GetMapDistance(params);
-         gl.glUniform1f(p->uMapDistanceLocation_, mapDistance.value());
+         glUniform1f(p->uMapDistanceLocation_, mapDistance.value());
       }
       else
       {
          // If thresholding is disabled, set the map distance to 0
-         gl.glUniform1f(p->uMapDistanceLocation_, 0.0f);
+         glUniform1f(p->uMapDistanceLocation_, 0.0f);
       }
 
       // Selected time
       std::chrono::system_clock::time_point selectedTime =
          (p->selectedTime_ == std::chrono::system_clock::time_point {}) ?
-            std::chrono::system_clock::now() :
+            scwx::util::time::now() :
             p->selectedTime_;
-      gl.glUniform1i(
+      glUniform1i(
          p->uSelectedTimeLocation_,
          static_cast(std::chrono::duration_cast(
                                selectedTime.time_since_epoch())
                                .count()));
 
       // Interpolate texture coordinates
-      gl.glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
-      gl.glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
+      glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
+      glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
 
       // Draw icons
-      gl.glDrawArrays(GL_TRIANGLES, 0, p->numVertices_);
+      glDrawArrays(GL_TRIANGLES, 0, p->numVertices_);
    }
 }
 
 void PlacefileIcons::Deinitialize()
 {
-   gl::OpenGLFunctions& gl = p->context_->gl();
-
-   gl.glDeleteVertexArrays(1, &p->vao_);
-   gl.glDeleteBuffers(static_cast(p->vbo_.size()), p->vbo_.data());
+   glDeleteVertexArrays(1, &p->vao_);
+   glDeleteBuffers(static_cast(p->vbo_.size()), p->vbo_.data());
 
    std::unique_lock lock {p->iconMutex_};
 
@@ -642,8 +644,6 @@ void PlacefileIcons::Impl::UpdateTextureBuffer()
 
 void PlacefileIcons::Impl::Update(bool textureAtlasChanged)
 {
-   gl::OpenGLFunctions& gl = context_->gl();
-
    // If the texture atlas has changed
    if (dirty_ || textureAtlasChanged)
    {
@@ -657,29 +657,32 @@ void PlacefileIcons::Impl::Update(bool textureAtlasChanged)
       UpdateTextureBuffer();
 
       // Buffer texture data
-      gl.glBindBuffer(GL_ARRAY_BUFFER, vbo_[1]);
-      gl.glBufferData(GL_ARRAY_BUFFER,
-                      sizeof(float) * textureBuffer_.size(),
-                      textureBuffer_.data(),
-                      GL_DYNAMIC_DRAW);
+      glBindBuffer(GL_ARRAY_BUFFER, vbo_[1]);
+      glBufferData(
+         GL_ARRAY_BUFFER,
+         static_cast(sizeof(float) * textureBuffer_.size()),
+         textureBuffer_.data(),
+         GL_DYNAMIC_DRAW);
    }
 
    // If buffers need updating
    if (dirty_)
    {
       // Buffer vertex data
-      gl.glBindBuffer(GL_ARRAY_BUFFER, vbo_[0]);
-      gl.glBufferData(GL_ARRAY_BUFFER,
-                      sizeof(float) * currentIconBuffer_.size(),
-                      currentIconBuffer_.data(),
-                      GL_DYNAMIC_DRAW);
+      glBindBuffer(GL_ARRAY_BUFFER, vbo_[0]);
+      glBufferData(
+         GL_ARRAY_BUFFER,
+         static_cast(sizeof(float) * currentIconBuffer_.size()),
+         currentIconBuffer_.data(),
+         GL_DYNAMIC_DRAW);
 
       // Buffer threshold data
-      gl.glBindBuffer(GL_ARRAY_BUFFER, vbo_[2]);
-      gl.glBufferData(GL_ARRAY_BUFFER,
-                      sizeof(GLint) * currentIntegerBuffer_.size(),
-                      currentIntegerBuffer_.data(),
-                      GL_DYNAMIC_DRAW);
+      glBindBuffer(GL_ARRAY_BUFFER, vbo_[2]);
+      glBufferData(
+         GL_ARRAY_BUFFER,
+         static_cast(sizeof(GLint) * currentIntegerBuffer_.size()),
+         currentIntegerBuffer_.data(),
+         GL_DYNAMIC_DRAW);
 
       numVertices_ =
          static_cast(currentIconBuffer_.size() / kPointsPerVertex);
@@ -718,7 +721,7 @@ bool PlacefileIcons::RunMousePicking(
    // If no time has been selected, use the current time
    std::chrono::system_clock::time_point selectedTime =
       (p->selectedTime_ == std::chrono::system_clock::time_point {}) ?
-         std::chrono::system_clock::now() :
+         scwx::util::time::now() :
          p->selectedTime_;
 
    // For each pickable icon
@@ -737,8 +740,10 @@ bool PlacefileIcons::RunMousePicking(
                    units::length::nautical_miles {icon.di_->threshold_}
                       .value())) < 999 &&
 
-                // Map distance is beyond the threshold
-                icon.di_->threshold_ < mapDistance) ||
+                // Map distance is beyond/within the threshold
+                icon.di_->threshold_ < mapDistance &&
+                (icon.di_->threshold_.value() >= 0.0 ||
+                 -(icon.di_->threshold_) > mapDistance)) ||
 
              (
                 // Line has a start time
diff --git a/scwx-qt/source/scwx/qt/gl/draw/placefile_images.cpp b/scwx-qt/source/scwx/qt/gl/draw/placefile_images.cpp
index aafaef8d..b14a2723 100644
--- a/scwx-qt/source/scwx/qt/gl/draw/placefile_images.cpp
+++ b/scwx-qt/source/scwx/qt/gl/draw/placefile_images.cpp
@@ -2,6 +2,7 @@
 #include 
 #include 
 #include 
+#include 
 
 #include 
 #include 
@@ -117,7 +118,7 @@ public:
 };
 
 PlacefileImages::PlacefileImages(const std::shared_ptr& context) :
-    DrawItem(context->gl()), p(std::make_unique(context))
+    DrawItem(), p(std::make_unique(context))
 {
 }
 PlacefileImages::~PlacefileImages() = default;
@@ -139,9 +140,6 @@ void PlacefileImages::set_thresholded(bool thresholded)
 
 void PlacefileImages::Initialize()
 {
-   gl::OpenGLFunctions& gl   = p->context_->gl();
-   auto&                gl30 = p->context_->gl30();
-
    p->shaderProgram_ = p->context_->GetShaderProgram(
       {{GL_VERTEX_SHADER, ":/gl/geo_texture2d.vert"},
        {GL_GEOMETRY_SHADER, ":/gl/threshold.geom"},
@@ -156,73 +154,81 @@ void PlacefileImages::Initialize()
    p->uSelectedTimeLocation_ =
       p->shaderProgram_->GetUniformLocation("uSelectedTime");
 
-   gl.glGenVertexArrays(1, &p->vao_);
-   gl.glGenBuffers(static_cast(p->vbo_.size()), p->vbo_.data());
+   glGenVertexArrays(1, &p->vao_);
+   glGenBuffers(static_cast(p->vbo_.size()), p->vbo_.data());
 
-   gl.glBindVertexArray(p->vao_);
-   gl.glBindBuffer(GL_ARRAY_BUFFER, p->vbo_[0]);
-   gl.glBufferData(GL_ARRAY_BUFFER, 0u, nullptr, GL_DYNAMIC_DRAW);
+   glBindVertexArray(p->vao_);
+   glBindBuffer(GL_ARRAY_BUFFER, p->vbo_[0]);
+   glBufferData(GL_ARRAY_BUFFER, 0u, nullptr, GL_DYNAMIC_DRAW);
+
+   // NOLINTBEGIN(modernize-use-nullptr)
+   // NOLINTBEGIN(performance-no-int-to-ptr)
+   // NOLINTBEGIN(cppcoreguidelines-avoid-magic-numbers)
 
    // aLatLong
-   gl.glVertexAttribPointer(0,
-                            2,
-                            GL_FLOAT,
-                            GL_FALSE,
-                            kPointsPerVertex * sizeof(float),
-                            static_cast(0));
-   gl.glEnableVertexAttribArray(0);
+   glVertexAttribPointer(0,
+                         2,
+                         GL_FLOAT,
+                         GL_FALSE,
+                         kPointsPerVertex * sizeof(float),
+                         static_cast(0));
+   glEnableVertexAttribArray(0);
 
    // aXYOffset
-   gl.glVertexAttribPointer(1,
-                            2,
-                            GL_FLOAT,
-                            GL_FALSE,
-                            kPointsPerVertex * sizeof(float),
-                            reinterpret_cast(2 * sizeof(float)));
-   gl.glEnableVertexAttribArray(1);
+   glVertexAttribPointer(1,
+                         2,
+                         GL_FLOAT,
+                         GL_FALSE,
+                         kPointsPerVertex * sizeof(float),
+                         reinterpret_cast(2 * sizeof(float)));
+   glEnableVertexAttribArray(1);
 
    // aModulate
-   gl.glVertexAttribPointer(3,
-                            4,
-                            GL_FLOAT,
-                            GL_FALSE,
-                            kPointsPerVertex * sizeof(float),
-                            reinterpret_cast(4 * sizeof(float)));
-   gl.glEnableVertexAttribArray(3);
+   glVertexAttribPointer(3,
+                         4,
+                         GL_FLOAT,
+                         GL_FALSE,
+                         kPointsPerVertex * sizeof(float),
+                         reinterpret_cast(4 * sizeof(float)));
+   glEnableVertexAttribArray(3);
 
-   gl.glBindBuffer(GL_ARRAY_BUFFER, p->vbo_[1]);
-   gl.glBufferData(GL_ARRAY_BUFFER, 0u, nullptr, GL_DYNAMIC_DRAW);
+   glBindBuffer(GL_ARRAY_BUFFER, p->vbo_[1]);
+   glBufferData(GL_ARRAY_BUFFER, 0u, nullptr, GL_DYNAMIC_DRAW);
 
    // aTexCoord
-   gl.glVertexAttribPointer(2,
-                            3,
-                            GL_FLOAT,
-                            GL_FALSE,
-                            kPointsPerTexCoord * sizeof(float),
-                            static_cast(0));
-   gl.glEnableVertexAttribArray(2);
+   glVertexAttribPointer(2,
+                         3,
+                         GL_FLOAT,
+                         GL_FALSE,
+                         kPointsPerTexCoord * sizeof(float),
+                         static_cast(0));
+   glEnableVertexAttribArray(2);
 
-   gl.glBindBuffer(GL_ARRAY_BUFFER, p->vbo_[2]);
-   gl.glBufferData(GL_ARRAY_BUFFER, 0u, nullptr, GL_DYNAMIC_DRAW);
+   glBindBuffer(GL_ARRAY_BUFFER, p->vbo_[2]);
+   glBufferData(GL_ARRAY_BUFFER, 0u, nullptr, GL_DYNAMIC_DRAW);
 
    // aThreshold
-   gl.glVertexAttribIPointer(5, //
-                             1,
-                             GL_INT,
-                             kIntegersPerVertex_ * sizeof(GLint),
-                             static_cast(0));
-   gl.glEnableVertexAttribArray(5);
+   glVertexAttribIPointer(5, //
+                          1,
+                          GL_INT,
+                          kIntegersPerVertex_ * sizeof(GLint),
+                          static_cast(0));
+   glEnableVertexAttribArray(5);
 
    // aTimeRange
-   gl.glVertexAttribIPointer(6, //
-                             2,
-                             GL_INT,
-                             kIntegersPerVertex_ * sizeof(GLint),
-                             reinterpret_cast(1 * sizeof(GLint)));
-   gl.glEnableVertexAttribArray(6);
+   glVertexAttribIPointer(6, //
+                          2,
+                          GL_INT,
+                          kIntegersPerVertex_ * sizeof(GLint),
+                          reinterpret_cast(1 * sizeof(GLint)));
+   glEnableVertexAttribArray(6);
 
    // aDisplayed
-   gl30.glVertexAttribI1i(7, 1);
+   glVertexAttribI1i(7, 1);
+
+   // NOLINTEND(cppcoreguidelines-avoid-magic-numbers)
+   // NOLINTEND(performance-no-int-to-ptr)
+   // NOLINTEND(modernize-use-nullptr)
 
    p->dirty_ = true;
 }
@@ -235,9 +241,7 @@ void PlacefileImages::Render(
 
    if (!p->currentImageList_.empty())
    {
-      gl::OpenGLFunctions& gl = p->context_->gl();
-
-      gl.glBindVertexArray(p->vao_);
+      glBindVertexArray(p->vao_);
 
       p->Update(textureAtlasChanged);
       p->shaderProgram_->Use();
@@ -250,40 +254,38 @@ void PlacefileImages::Render(
          // If thresholding is enabled, set the map distance
          units::length::nautical_miles mapDistance =
             util::maplibre::GetMapDistance(params);
-         gl.glUniform1f(p->uMapDistanceLocation_, mapDistance.value());
+         glUniform1f(p->uMapDistanceLocation_, mapDistance.value());
       }
       else
       {
          // If thresholding is disabled, set the map distance to 0
-         gl.glUniform1f(p->uMapDistanceLocation_, 0.0f);
+         glUniform1f(p->uMapDistanceLocation_, 0.0f);
       }
 
       // Selected time
       std::chrono::system_clock::time_point selectedTime =
          (p->selectedTime_ == std::chrono::system_clock::time_point {}) ?
-            std::chrono::system_clock::now() :
+            scwx::util::time::now() :
             p->selectedTime_;
-      gl.glUniform1i(
+      glUniform1i(
          p->uSelectedTimeLocation_,
          static_cast(std::chrono::duration_cast(
                                selectedTime.time_since_epoch())
                                .count()));
 
       // Interpolate texture coordinates
-      gl.glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
-      gl.glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
+      glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
+      glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
 
       // Draw images
-      gl.glDrawArrays(GL_TRIANGLES, 0, p->numVertices_);
+      glDrawArrays(GL_TRIANGLES, 0, p->numVertices_);
    }
 }
 
 void PlacefileImages::Deinitialize()
 {
-   gl::OpenGLFunctions& gl = p->context_->gl();
-
-   gl.glDeleteVertexArrays(1, &p->vao_);
-   gl.glDeleteBuffers(static_cast(p->vbo_.size()), p->vbo_.data());
+   glDeleteVertexArrays(1, &p->vao_);
+   glDeleteBuffers(static_cast(p->vbo_.size()), p->vbo_.data());
 
    std::unique_lock lock {p->imageMutex_};
 
@@ -439,8 +441,6 @@ void PlacefileImages::Impl::UpdateTextureBuffer()
 
 void PlacefileImages::Impl::Update(bool textureAtlasChanged)
 {
-   gl::OpenGLFunctions& gl = context_->gl();
-
    // If the texture atlas has changed
    if (dirty_ || textureAtlasChanged)
    {
@@ -454,29 +454,32 @@ void PlacefileImages::Impl::Update(bool textureAtlasChanged)
       UpdateTextureBuffer();
 
       // Buffer texture data
-      gl.glBindBuffer(GL_ARRAY_BUFFER, vbo_[1]);
-      gl.glBufferData(GL_ARRAY_BUFFER,
-                      sizeof(float) * textureBuffer_.size(),
-                      textureBuffer_.data(),
-                      GL_DYNAMIC_DRAW);
+      glBindBuffer(GL_ARRAY_BUFFER, vbo_[1]);
+      glBufferData(
+         GL_ARRAY_BUFFER,
+         static_cast(sizeof(float) * textureBuffer_.size()),
+         textureBuffer_.data(),
+         GL_DYNAMIC_DRAW);
    }
 
    // If buffers need updating
    if (dirty_)
    {
       // Buffer vertex data
-      gl.glBindBuffer(GL_ARRAY_BUFFER, vbo_[0]);
-      gl.glBufferData(GL_ARRAY_BUFFER,
-                      sizeof(float) * currentImageBuffer_.size(),
-                      currentImageBuffer_.data(),
-                      GL_DYNAMIC_DRAW);
+      glBindBuffer(GL_ARRAY_BUFFER, vbo_[0]);
+      glBufferData(
+         GL_ARRAY_BUFFER,
+         static_cast(sizeof(float) * currentImageBuffer_.size()),
+         currentImageBuffer_.data(),
+         GL_DYNAMIC_DRAW);
 
       // Buffer threshold data
-      gl.glBindBuffer(GL_ARRAY_BUFFER, vbo_[2]);
-      gl.glBufferData(GL_ARRAY_BUFFER,
-                      sizeof(GLint) * currentIntegerBuffer_.size(),
-                      currentIntegerBuffer_.data(),
-                      GL_DYNAMIC_DRAW);
+      glBindBuffer(GL_ARRAY_BUFFER, vbo_[2]);
+      glBufferData(
+         GL_ARRAY_BUFFER,
+         static_cast(sizeof(GLint) * currentIntegerBuffer_.size()),
+         currentIntegerBuffer_.data(),
+         GL_DYNAMIC_DRAW);
 
       numVertices_ =
          static_cast(currentImageBuffer_.size() / kPointsPerVertex);
diff --git a/scwx-qt/source/scwx/qt/gl/draw/placefile_lines.cpp b/scwx-qt/source/scwx/qt/gl/draw/placefile_lines.cpp
index 8fdce9f1..b908389c 100644
--- a/scwx-qt/source/scwx/qt/gl/draw/placefile_lines.cpp
+++ b/scwx-qt/source/scwx/qt/gl/draw/placefile_lines.cpp
@@ -3,6 +3,7 @@
 #include 
 #include 
 #include 
+#include 
 
 #include 
 
@@ -18,13 +19,9 @@ namespace draw
 static const std::string logPrefix_ = "scwx::qt::gl::draw::placefile_lines";
 static const auto        logger_    = scwx::util::Logger::Create(logPrefix_);
 
-static constexpr std::size_t kNumRectangles        = 1;
-static constexpr std::size_t kNumTriangles         = kNumRectangles * 2;
 static constexpr std::size_t kVerticesPerTriangle  = 3;
 static constexpr std::size_t kVerticesPerRectangle = kVerticesPerTriangle * 2;
 static constexpr std::size_t kPointsPerVertex      = 9;
-static constexpr std::size_t kBufferLength =
-   kNumTriangles * kVerticesPerTriangle * kPointsPerVertex;
 
 // Threshold, start time, end time
 static constexpr std::size_t kIntegersPerVertex_ = 3;
@@ -110,7 +107,7 @@ public:
 };
 
 PlacefileLines::PlacefileLines(const std::shared_ptr& context) :
-    DrawItem(context->gl()), p(std::make_unique(context))
+    DrawItem(), p(std::make_unique(context))
 {
 }
 PlacefileLines::~PlacefileLines() = default;
@@ -131,9 +128,6 @@ void PlacefileLines::set_thresholded(bool thresholded)
 
 void PlacefileLines::Initialize()
 {
-   gl::OpenGLFunctions& gl   = p->context_->gl();
-   auto&                gl30 = p->context_->gl30();
-
    p->shaderProgram_ = p->context_->GetShaderProgram(
       {{GL_VERTEX_SHADER, ":/gl/geo_texture2d.vert"},
        {GL_GEOMETRY_SHADER, ":/gl/threshold.geom"},
@@ -148,70 +142,78 @@ void PlacefileLines::Initialize()
    p->uSelectedTimeLocation_ =
       p->shaderProgram_->GetUniformLocation("uSelectedTime");
 
-   gl.glGenVertexArrays(1, &p->vao_);
-   gl.glGenBuffers(2, p->vbo_.data());
+   glGenVertexArrays(1, &p->vao_);
+   glGenBuffers(2, p->vbo_.data());
 
-   gl.glBindVertexArray(p->vao_);
-   gl.glBindBuffer(GL_ARRAY_BUFFER, p->vbo_[0]);
-   gl.glBufferData(GL_ARRAY_BUFFER, 0u, nullptr, GL_DYNAMIC_DRAW);
+   glBindVertexArray(p->vao_);
+   glBindBuffer(GL_ARRAY_BUFFER, p->vbo_[0]);
+   glBufferData(GL_ARRAY_BUFFER, 0u, nullptr, GL_DYNAMIC_DRAW);
+
+   // NOLINTBEGIN(modernize-use-nullptr)
+   // NOLINTBEGIN(performance-no-int-to-ptr)
+   // NOLINTBEGIN(cppcoreguidelines-avoid-magic-numbers)
 
    // aLatLong
-   gl.glVertexAttribPointer(0,
-                            2,
-                            GL_FLOAT,
-                            GL_FALSE,
-                            kPointsPerVertex * sizeof(float),
-                            static_cast(0));
-   gl.glEnableVertexAttribArray(0);
+   glVertexAttribPointer(0,
+                         2,
+                         GL_FLOAT,
+                         GL_FALSE,
+                         kPointsPerVertex * sizeof(float),
+                         static_cast(0));
+   glEnableVertexAttribArray(0);
 
    // aXYOffset
-   gl.glVertexAttribPointer(1,
-                            2,
-                            GL_FLOAT,
-                            GL_FALSE,
-                            kPointsPerVertex * sizeof(float),
-                            reinterpret_cast(2 * sizeof(float)));
-   gl.glEnableVertexAttribArray(1);
+   glVertexAttribPointer(1,
+                         2,
+                         GL_FLOAT,
+                         GL_FALSE,
+                         kPointsPerVertex * sizeof(float),
+                         reinterpret_cast(2 * sizeof(float)));
+   glEnableVertexAttribArray(1);
 
    // aModulate
-   gl.glVertexAttribPointer(3,
-                            4,
-                            GL_FLOAT,
-                            GL_FALSE,
-                            kPointsPerVertex * sizeof(float),
-                            reinterpret_cast(4 * sizeof(float)));
-   gl.glEnableVertexAttribArray(3);
+   glVertexAttribPointer(3,
+                         4,
+                         GL_FLOAT,
+                         GL_FALSE,
+                         kPointsPerVertex * sizeof(float),
+                         reinterpret_cast(4 * sizeof(float)));
+   glEnableVertexAttribArray(3);
 
    // aAngle
-   gl.glVertexAttribPointer(4,
-                            1,
-                            GL_FLOAT,
-                            GL_FALSE,
-                            kPointsPerVertex * sizeof(float),
-                            reinterpret_cast(8 * sizeof(float)));
-   gl.glEnableVertexAttribArray(4);
+   glVertexAttribPointer(4,
+                         1,
+                         GL_FLOAT,
+                         GL_FALSE,
+                         kPointsPerVertex * sizeof(float),
+                         reinterpret_cast(8 * sizeof(float)));
+   glEnableVertexAttribArray(4);
 
-   gl.glBindBuffer(GL_ARRAY_BUFFER, p->vbo_[1]);
-   gl.glBufferData(GL_ARRAY_BUFFER, 0u, nullptr, GL_DYNAMIC_DRAW);
+   glBindBuffer(GL_ARRAY_BUFFER, p->vbo_[1]);
+   glBufferData(GL_ARRAY_BUFFER, 0u, nullptr, GL_DYNAMIC_DRAW);
 
    // aThreshold
-   gl.glVertexAttribIPointer(5, //
-                             1,
-                             GL_INT,
-                             kIntegersPerVertex_ * sizeof(GLint),
-                             static_cast