diff --git a/.github/workflows/actionlint.yml b/.github/workflows/actionlint.yml index c3c2d4e..2c59490 100644 --- a/.github/workflows/actionlint.yml +++ b/.github/workflows/actionlint.yml @@ -12,7 +12,7 @@ jobs: - uses: actions/checkout@v7.0.1 - name: actionlint - uses: reviewdog/action-actionlint@v1.73.0 + uses: reviewdog/action-actionlint@v1.73.4 with: github_token: ${{ secrets.GITHUB_TOKEN }} reporter: github-pr-review diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 21a696e..dd9b848 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -5,6 +5,8 @@ on: paths: - 'Dockerfile' - '*/src/**' + - 'assets/**' + - 'rust-toolchain' - 'Cargo.toml' - 'Cargo.lock' - '.github/workflows/ci.yml' @@ -38,23 +40,7 @@ jobs: winget install --id=Google.Protobuf -l C:\protobuf --exact --accept-package-agreements --accept-source-agreements echo C:\protobuf\bin | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append - # depName=rustfs/cli - $env:RUSTFS_CLI_VERSION="v0.1.30" - $env:RUSTFS_CLI_ZIP_NAME="rustfs-cli-windows-amd64-$env:RUSTFS_CLI_VERSION.zip" - Invoke-WebRequest "https://github.com/rustfs/cli/releases/download/$env:RUSTFS_CLI_VERSION/$env:RUSTFS_CLI_ZIP_NAME" -OutFile "$env:RUSTFS_CLI_ZIP_NAME" - Expand-Archive -Path "$env:RUSTFS_CLI_ZIP_NAME" -DestinationPath C:\Windows\ - Remove-Item -Path "$env:RUSTFS_CLI_ZIP_NAME" - - - name: Prepare Map - shell: C:\shells\gitbash.exe {0} - run: | - rc alias set yr32srv https://rustfs-s3.yr32.net eewbot-ci "${{ secrets.YR32RUSTFS_EEWBOT_CI_SECRET }}" - pushd assets/shapefile/ - rc cp yr32srv/eewbot/shapefiles_v1.zip ./shapefiles_v1.zip - 7z x ./shapefiles_v1.zip -y - popd - - - uses: Swatinem/rust-cache@v2.9.1 + - uses: Swatinem/rust-cache@v2.9.2 with: prefix-key: ${{ env.RUST_CACHE_VERSION }}-rust @@ -83,12 +69,13 @@ jobs: mkdir "$DIR_NAME" cp ./target/release/renderer.exe "$DIR_NAME/" cp ./LICENSE "$DIR_NAME/" + cp ./THIRD_PARTY_NOTICES.md "$DIR_NAME/" cp ./CREDITS "$DIR_NAME/" cp ./README.md "$DIR_NAME/" 7z a "$DIR_NAME.zip" "$DIR_NAME/" - name: pre-release - uses: softprops/action-gh-release@v3.0.2 + uses: softprops/action-gh-release@v3.0.3 if: "! startsWith(github.ref, 'refs/tags/')" with: tag_name: master-${{ github.sha }} @@ -98,7 +85,7 @@ jobs: *.zip - name: tagged-release - uses: softprops/action-gh-release@v3.0.2 + uses: softprops/action-gh-release@v3.0.3 if: startsWith(github.ref, 'refs/tags/') with: fail_on_unmatched_files: true @@ -113,11 +100,9 @@ jobs: matrix: include: - ghimage: ubuntu-24.04 - rustfscliarch: amd64 arch_s: x86_64 - ghimage: ubuntu-24.04-arm - rustfscliarch: arm64 arch_s: aarch64 permissions: @@ -130,20 +115,8 @@ jobs: with: submodules: true - - run: | - # depName=rustfs/cli - RUSTFS_CLI_VERSION="v0.1.30" - RUSTFS_CLI_TAR_NAME="rustfs-cli-linux-${{ matrix.rustfscliarch }}-$RUSTFS_CLI_VERSION.tar.gz" - wget "https://github.com/rustfs/cli/releases/download/$RUSTFS_CLI_VERSION/$RUSTFS_CLI_TAR_NAME" - tar xf "$RUSTFS_CLI_TAR_NAME" - rm "$RUSTFS_CLI_TAR_NAME" - sudo mv rc /bin/ - - rc alias set yr32srv https://rustfs-s3.yr32.net eewbot-ci "${{ secrets.YR32RUSTFS_EEWBOT_CI_SECRET }}" - pushd assets/shapefile/ - rc cp yr32srv/eewbot/shapefiles_v1.zip ./shapefiles_v1.zip - unzip -o ./shapefiles_v1.zip - popd + - name: Install Toolchains + run: | sudo apt-get update -y sudo apt-get install -y protobuf-compiler @@ -151,7 +124,7 @@ jobs: run: | cargo install cargo-license - - uses: Swatinem/rust-cache@v2.9.1 + - uses: Swatinem/rust-cache@v2.9.2 with: prefix-key: ${{ env.RUST_CACHE_VERSION }}-rust @@ -174,12 +147,13 @@ jobs: mkdir "$DIR_NAME" cp ./target/release/renderer "$DIR_NAME/" cp ./LICENSE "$DIR_NAME/" + cp ./THIRD_PARTY_NOTICES.md "$DIR_NAME/" cp ./CREDITS "$DIR_NAME/" cp ./README.md "$DIR_NAME/" tar cvf "$DIR_NAME.tar.gz" "$DIR_NAME/" - name: pre-release - uses: softprops/action-gh-release@v3.0.2 + uses: softprops/action-gh-release@v3.0.3 if: "! startsWith(github.ref, 'refs/tags/')" with: tag_name: master-${{ github.sha }} @@ -189,7 +163,7 @@ jobs: *.tar.gz - name: tagged-release - uses: softprops/action-gh-release@v3.0.2 + uses: softprops/action-gh-release@v3.0.3 if: startsWith(github.ref, 'refs/tags/') with: fail_on_unmatched_files: true @@ -205,11 +179,9 @@ jobs: include: - ghimage: ubuntu-24.04 dockerarch: linux/amd64 - rustfscliarch: amd64 - ghimage: ubuntu-24.04-arm dockerarch: linux/arm64 - rustfscliarch: arm64 permissions: packages: write @@ -224,20 +196,8 @@ jobs: with: submodules: true - - run: | - # depName=rustfs/cli - RUSTFS_CLI_VERSION="v0.1.30" - RUSTFS_CLI_TAR_NAME="rustfs-cli-linux-${{ matrix.rustfscliarch }}-$RUSTFS_CLI_VERSION.tar.gz" - wget "https://github.com/rustfs/cli/releases/download/$RUSTFS_CLI_VERSION/$RUSTFS_CLI_TAR_NAME" - tar xf "$RUSTFS_CLI_TAR_NAME" - rm "$RUSTFS_CLI_TAR_NAME" - sudo mv rc /bin/ - - rc alias set yr32srv https://rustfs-s3.yr32.net eewbot-ci "${{ secrets.YR32RUSTFS_EEWBOT_CI_SECRET }}" - pushd assets/shapefile/ - rc cp yr32srv/eewbot/shapefiles_v1.zip ./shapefiles_v1.zip - unzip -o ./shapefiles_v1.zip - popd + - name: Install Toolchains + run: | sudo apt-get update -y sudo apt-get install -y protobuf-compiler @@ -252,7 +212,7 @@ jobs: toolchain: ${{ steps.toolchain.outputs.toolchain }} components: clippy - - uses: Swatinem/rust-cache@v2.9.1 + - uses: Swatinem/rust-cache@v2.9.2 with: prefix-key: ${{ env.RUST_CACHE_VERSION }}-rust @@ -274,12 +234,10 @@ jobs: include: - ghimage: ubuntu-24.04 dockerarch: linux/amd64 - rustfscliarch: amd64 arch_short: amd64 - ghimage: ubuntu-24.04-arm dockerarch: linux/arm64 - rustfscliarch: arm64 arch_short: arm64 permissions: @@ -295,20 +253,8 @@ jobs: with: submodules: true - - run: | - # depName=rustfs/cli - RUSTFS_CLI_VERSION="v0.1.30" - RUSTFS_CLI_TAR_NAME="rustfs-cli-linux-${{ matrix.rustfscliarch }}-$RUSTFS_CLI_VERSION.tar.gz" - wget "https://github.com/rustfs/cli/releases/download/$RUSTFS_CLI_VERSION/$RUSTFS_CLI_TAR_NAME" - tar xf "$RUSTFS_CLI_TAR_NAME" - rm "$RUSTFS_CLI_TAR_NAME" - sudo mv rc /bin/ - - rc alias set yr32srv https://rustfs-s3.yr32.net eewbot-ci "${{ secrets.YR32RUSTFS_EEWBOT_CI_SECRET }}" - pushd assets/shapefile/ - rc cp yr32srv/eewbot/shapefiles_v1.zip ./shapefiles_v1.zip - unzip -o ./shapefiles_v1.zip - popd + - name: Install Toolchains + run: | sudo apt-get update -y sudo apt-get install -y protobuf-compiler @@ -324,7 +270,7 @@ jobs: type=semver,pattern={{major}} type=sha - - uses: docker/setup-buildx-action@v4.2.0 + - uses: docker/setup-buildx-action@v4.3.0 - uses: docker/login-action@v4.6.0 with: @@ -378,7 +324,7 @@ jobs: pattern: digests-* merge-multiple: true - - uses: docker/setup-buildx-action@v4.2.0 + - uses: docker/setup-buildx-action@v4.3.0 - uses: docker/metadata-action@v6.2.0 id: meta diff --git a/.github/workflows/rendering.yml b/.github/workflows/rendering.yml index 120b2bd..7c04848 100644 --- a/.github/workflows/rendering.yml +++ b/.github/workflows/rendering.yml @@ -4,6 +4,8 @@ on: pull_request: paths: - '*/src/**' + - 'assets/**' + - 'rust-toolchain' - 'Cargo.toml' - 'Cargo.lock' - '.github/workflows/rendering.yml' @@ -22,22 +24,7 @@ jobs: - name: prepare dependencies run: | - sudo apt-get update -y && sudo apt-get install -y xvfb protobuf-compiler libxkbcommon-x11-dev - - - run: | - # depName=rustfs/cli - RUSTFS_CLI_VERSION="v0.1.30" - RUSTFS_CLI_TAR_NAME="rustfs-cli-linux-amd64-$RUSTFS_CLI_VERSION.tar.gz" - wget "https://github.com/rustfs/cli/releases/download/$RUSTFS_CLI_VERSION/$RUSTFS_CLI_TAR_NAME" - tar xf "$RUSTFS_CLI_TAR_NAME" - rm "$RUSTFS_CLI_TAR_NAME" - sudo mv rc /bin/ - - rc alias set yr32srv https://rustfs-s3.yr32.net eewbot-ci "${{ secrets.YR32RUSTFS_EEWBOT_CI_SECRET }}" - pushd assets/shapefile/ - rc cp yr32srv/eewbot/shapefiles_v1.zip ./shapefiles_v1.zip - unzip -o ./shapefiles_v1.zip - popd + sudo apt-get update -y && sudo apt-get install -y protobuf-compiler libegl1 - name: Get Rust toolchain id: toolchain @@ -45,12 +32,12 @@ jobs: awk -F'[ ="]+' '$1 == "channel" { print "toolchain=" $2 }' rust-toolchain >> "$GITHUB_OUTPUT" - name: cache dependencies - uses: Swatinem/rust-cache@v2.9.1 + uses: Swatinem/rust-cache@v2.9.2 - name: install eew-renderer-url run: | # depName=EEWBot/eew-renderer-url - EEW_RENDERER_URL_VERSION="v0.1.0" + EEW_RENDERER_URL_VERSION="v0.2.0" TAR_NAME="eew-renderer-url-linux-x86_64-$EEW_RENDERER_URL_VERSION.tar.gz" wget "https://github.com/EEWBot/eew-renderer-url/releases/download/$EEW_RENDERER_URL_VERSION/$TAR_NAME" tar xf $TAR_NAME @@ -62,9 +49,7 @@ jobs: - name: rendering run: | - Xvfb :0 -listen tcp -ac -screen 0 1024x768x24 & - sleep 1; - DISPLAY=:0 cargo run --release & + HEADLESS=true cargo run --release & sleep 3; mkdir images diff --git a/.github/workflows/review.yml b/.github/workflows/review.yml index ae6e022..fe6bf5d 100644 --- a/.github/workflows/review.yml +++ b/.github/workflows/review.yml @@ -4,6 +4,8 @@ on: pull_request: paths: - '*/src/**' + - 'rust-toolchain' + - 'assets/**' - 'Cargo.toml' - 'Cargo.lock' - '.github/workflows/review.yml' @@ -18,11 +20,9 @@ jobs: include: - ghimage: ubuntu-24.04 dockerarch: linux/amd64 - rustfscliarch: amd64 - ghimage: ubuntu-24.04-arm dockerarch: linux/arm64 - rustfscliarch: arm64 permissions: packages: write @@ -37,20 +37,8 @@ jobs: with: submodules: true - - run: | - # depName=rustfs/cli - RUSTFS_CLI_VERSION="v0.1.30" - RUSTFS_CLI_TAR_NAME="rustfs-cli-linux-${{ matrix.rustfscliarch }}-$RUSTFS_CLI_VERSION.tar.gz" - wget "https://github.com/rustfs/cli/releases/download/$RUSTFS_CLI_VERSION/$RUSTFS_CLI_TAR_NAME" - tar xf "$RUSTFS_CLI_TAR_NAME" - rm "$RUSTFS_CLI_TAR_NAME" - sudo mv rc /bin/ - - rc alias set yr32srv https://rustfs-s3.yr32.net eewbot-ci "${{ secrets.YR32RUSTFS_EEWBOT_CI_SECRET }}" - pushd assets/shapefile/ - rc cp yr32srv/eewbot/shapefiles_v1.zip ./shapefiles_v1.zip - unzip -o ./shapefiles_v1.zip - popd + - name: Install Toolchains + run: | sudo apt-get update -y sudo apt-get install -y protobuf-compiler @@ -66,7 +54,7 @@ jobs: components: clippy - name: cache dependencies - uses: Swatinem/rust-cache@v2.9.1 + uses: Swatinem/rust-cache@v2.9.2 - name: clippy check uses: yanorei32/action-clippy@50412b135153ebee7d4b8a5b868c4defdc663683 diff --git a/.gitignore b/.gitignore index 6fec9fb..b6f0a30 100644 --- a/.gitignore +++ b/.gitignore @@ -6,11 +6,11 @@ /target/ -*.dbf -*.shp -*.shx +# QGIS layer metadata sidecar file *.qmd +# QGIS coordinate reference system metadata *.prj + screenshot*.png log.txt *.log diff --git a/Cargo.lock b/Cargo.lock index 6fd5c32..9725907 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -329,7 +329,7 @@ dependencies = [ "base32768-table", "itertools 0.14.0", "phf 0.13.1", - "thiserror 2.0.19", + "thiserror 2.0.20", ] [[package]] @@ -533,9 +533,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.6.5" +version = "4.6.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "301b56658598e48f3648647ac6fc887be7e7108eddfa4e9b63fcf3ec58c0cadf" +checksum = "473c7e07f409a8d772161724aa8db6a765a2532a70f9667eeb7b49d3d02fbdca" dependencies = [ "clap_builder", "clap_derive", @@ -543,9 +543,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.6.5" +version = "4.6.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94a65403d1a1bd28f7dc68eb8506e8874808ee5eecb59298de588e2e1407a078" +checksum = "7b48fea5a88e9ae728a2dcbedbfc0e730f7d60da42e1cb049a83c9fb8b789889" dependencies = [ "anstream", "anstyle", @@ -562,7 +562,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn 3.0.2", + "syn 3.0.4", ] [[package]] @@ -731,9 +731,9 @@ dependencies = [ [[package]] name = "crossbeam-epoch" -version = "0.9.18" +version = "0.9.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +checksum = "2d6914041f254d6e9176c01941b21115dcfb7089e55135a35411081bd106ef3f" dependencies = [ "crossbeam-utils", ] @@ -899,7 +899,7 @@ checksum = "4bcb37146b17400613e7a7eb6a585f9319d3edaea6fc718e37a1ecf7f27c6373" dependencies = [ "proc-macro2", "quote", - "syn 3.0.2", + "syn 3.0.4", ] [[package]] @@ -1094,9 +1094,9 @@ dependencies = [ [[package]] name = "generic-array" -version = "1.4.4" +version = "1.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab4e5aa225bc56696909483320f0ff9b600f1a971b52e07a17d70f3d9b43254b" +checksum = "337d46834ee672ab3e48caca2cb0c78cc174fb12b3a68d0d88f99a0519a5e36e" dependencies = [ "rustversion", "typenum", @@ -1680,7 +1680,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "07033963ba89ebaf1584d767badaa2e8fcec21aedea6b8c0346d487d49c28667" dependencies = [ "cfg-if", - "windows-targets 0.52.6", + "windows-targets 0.53.3", ] [[package]] @@ -1830,9 +1830,9 @@ dependencies = [ [[package]] name = "moka" -version = "0.12.15" +version = "0.12.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "957228ad12042ee839f93c8f257b62b4c0ab5eaae1d4fa60de53b27c9d7c5046" +checksum = "4293f18e7567a1caf3c584855554377025c65e0aa445344d04171f5ad63d19b9" dependencies = [ "async-lock", "crossbeam-channel", @@ -2229,9 +2229,9 @@ dependencies = [ [[package]] name = "ordered-float" -version = "5.3.0" +version = "5.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7d950ca161dc355eaf28f82b11345ed76c6e1f6eb1f4f4479e0323b9e2fbd0e" +checksum = "8c7c9e0d9b23589f26070720bac724174bfec1083e82f7854cdd0267518343c0" dependencies = [ "num-traits", ] @@ -2754,7 +2754,7 @@ dependencies = [ "clap", "const_soft_float", "enum-map", - "generic-array 1.4.4", + "generic-array 1.4.5", "glium", "glutin-winit", "headers", @@ -2767,12 +2767,13 @@ dependencies = [ "prost-build", "rand 0.10.2", "renderer-assets", + "renderer-macros", "renderer-types", "rusttype", "sha1 0.11.0", "strum", "strum_macros", - "thiserror 2.0.19", + "thiserror 2.0.20", "tokio", "tracing", "tracing-subscriber", @@ -2796,6 +2797,14 @@ dependencies = [ "serde_json", ] +[[package]] +name = "renderer-macros" +version = "0.1.0" +dependencies = [ + "quote", + "syn 3.0.4", +] + [[package]] name = "renderer-types" version = "0.1.0" @@ -3004,7 +3013,7 @@ checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" dependencies = [ "proc-macro2", "quote", - "syn 3.0.2", + "syn 3.0.4", ] [[package]] @@ -3238,9 +3247,9 @@ dependencies = [ [[package]] name = "syn" -version = "3.0.2" +version = "3.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a207d6d6a2b7fc470b80443726053f18a2481b7e1eee970597051596567987a3" +checksum = "e6275cddf4610d1775e6d1fe9469b2e77d0f39fd98fb7450901b821e0c53649f" dependencies = [ "proc-macro2", "quote", @@ -3282,11 +3291,11 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.19" +version = "2.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09a43598840e33d5b0331f38c5e30d13bb11c11210a4b58f0d9b18a5a5eefcd9" +checksum = "ec86235f5fcc2a73650310756d2ac5b138a5780bbbdfae3eeccec992c435ba4f" dependencies = [ - "thiserror-impl 2.0.19", + "thiserror-impl 2.0.20", ] [[package]] @@ -3302,13 +3311,13 @@ dependencies = [ [[package]] name = "thiserror-impl" -version = "2.0.19" +version = "2.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43cbfe0cf76104d42a574802844187e84a305e531ed54455f11fbde0f10541cd" +checksum = "bc04cd3e1236dd4a98afca4569f2deb3f120e5422a4023be2cb683f8486292af" dependencies = [ "proc-macro2", "quote", - "syn 3.0.2", + "syn 3.0.4", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index d03724f..e9a9db0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,4 +6,5 @@ members = [ "renderer", "renderer-assets", "renderer-types", + "renderer-macros", ] diff --git a/Dockerfile b/Dockerfile index 64087bd..57693cf 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM rust:1.97.1-bookworm AS build-env +FROM rust:1.98.0-trixie AS build-env LABEL maintainer="yanorei32" SHELL ["/bin/bash", "-o", "pipefail", "-c"] @@ -32,22 +32,25 @@ RUN cargo build --release COPY . /usr/src/eew-renderer/ RUN touch assets/* src/* && cargo build --release -FROM debian:bookworm-slim +FROM debian:trixie-slim WORKDIR / RUN sed -i -e's/ main/ main contrib non-free/g' /etc/apt/sources.list.d/debian.sources && \ apt-get update && apt-get install -y \ libx11-6 libxcursor1 libx11-xcb1 libxi6 libxkbcommon-x11-0 \ - libgl1 libglvnd0 libgl1-mesa-dri libgl1-nvidia-glvnd-glx && \ - ln -s "/usr/lib/mesa-diverted/$(uname -m)-linux-gnu/libEGL.so.1" "/lib/$(uname -m)-linux-gnu/" + libgl1 libglvnd0 libgl1-mesa-dri libgl1-nvidia-glvnd-glx \ + libegl1 libegl-mesa0 \ + && rm -rf /var/lib/apt/lists/* -COPY ./init-virtualgl.sh / -RUN /init-virtualgl.sh && rm /init-virtualgl.sh && rm -rf /var/lib/apt/lists/* +ENV NVIDIA_VISIBLE_DEVICES=all +ENV NVIDIA_DRIVER_CAPABILITIES=graphics,utility +ENV HEADLESS=true COPY --chown=root:root --from=build-env \ /usr/src/eew-renderer/CREDITS \ /usr/src/eew-renderer/LICENSE \ + /usr/src/eew-renderer/THIRD_PARTY_NOTICES.md \ /usr/share/licenses/eew-renderer/ COPY --chown=root:root --from=build-env \ diff --git a/README.md b/README.md index 6eb3783..50f7651 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,5 @@ # EEW Renderer + image @@ -6,6 +7,8 @@ +[![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/EEWBot/eew-renderer) + ## Compatibility Basically, this project supports GL_VERSION >= 4.5 platforms. @@ -25,3 +28,11 @@ ProgramCreation(LinkingError("error: Too many vertex shader image uniforms (1 > This is thought to be due to the fact that the Vertex Shader cannot use textures with uniforms, and there are no plans to fix this. The workaround is to use an alternative GL implementation, such as LIBGL_ALWAYS_SOFTWARE. + +## License + +EEW Renderer source code is licensed under the [MIT License](LICENSE). + +This project also uses geographic data provided by third parties. +Those datasets and data derived from them are subject to their respective +terms of use. See [THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md) for details. diff --git a/THIRD_PARTY_NOTICES.md b/THIRD_PARTY_NOTICES.md new file mode 100644 index 0000000..2712ec8 --- /dev/null +++ b/THIRD_PARTY_NOTICES.md @@ -0,0 +1,167 @@ +# Third-Party Data Notices + +EEW Renderer includes or uses data derived from third-party geographic datasets. + +The MIT License in this repository applies to the source code and other original +works of the EEWBot project unless otherwise stated. It does not replace or +override the terms applicable to third-party datasets described below. + +This notice also applies to modified or converted forms of these datasets, +including simplified Shapefiles and data generated from them and embedded into +compiled binaries. + +## Natural Earth + +### Dataset + +Natural Earth 1:10m Cultural Vectors: + +* Admin 0 – Countries + +Source: + +`https://www.naturalearthdata.com/downloads/10m-cultural-vectors/` + +Terms of Use: + +`https://www.naturalearthdata.com/about/terms-of-use/` + +### License + +Natural Earth raster and vector map data are in the public domain. + +Natural Earth permits use, modification, redistribution, and commercial use +without requiring permission or attribution. + +Attribution is not required, but is provided here for provenance: + +> Made with Natural Earth. + +### Modifications + +Data derived from Natural Earth may have been simplified, filtered, transformed, +converted to another representation, or embedded into generated/compiled +EEW Renderer assets. + +These modifications were performed by the EEWBot project and are not provided +or endorsed by Natural Earth. + +--- + +## Japan Meteorological Agency (JMA) + +### Datasets + +The following datasets from the Japan Meteorological Agency's +"予報区等GISデータの一覧" are used: + +* 地震情報/細分区域 +* 津波予報区 + +Source: + +`https://www.data.jma.go.jp/developer/gis.html` + +Terms of Use: + +`https://www.jma.go.jp/jma/kishou/info/coment.html` + +Public Data License (Version 1.0 / PDL 1.0): + +`https://www.digital.go.jp/resources/open_data/public_data_license_v1.0` + +### Attribution and modifications + +Source: Japan Meteorological Agency (JMA), +"予報区等GISデータの一覧". + +The data used by EEW Renderer have been modified by the EEWBot project. +Modifications may include simplification, filtering, attribute processing, +geometry processing, conversion to internal data structures, and embedding +into generated/compiled application data. + +The modified data are not original, unmodified datasets produced by the +Japan Meteorological Agency. + +The JMA source page also identifies source map data from the Geospatial +Information Authority of Japan (GSI) and the corresponding approval information. +See the JMA source page above for those details. + +Use of the JMA content is subject to PDL 1.0 and the additional information +specified in the JMA website terms of use. + +--- + +## Ministry of Land, Infrastructure, Transport and Tourism (MLIT), Japan + +### Dataset + +National Land Numerical Information: + +* Lake Data (湖沼データ), dataset code W09 + +Source: + +`https://nlftp.mlit.go.jp/ksj/gml/datalist/KsjTmplt-W09-v2_2.html` + +Terms applicable to this dataset: + +National Land Numerical Information Download Service Content Terms of Use +(旧国土情報利用約款準拠版) + +`https://nlftp.mlit.go.jp/ksj/other/agreement_02.html` + +### License classification + +The W09 Lake Data are classified by MLIT as: + +> 商用可 (commercial use permitted) + +Under the applicable terms, commercial use and redistribution of copies are +permitted provided that the source and modifications are indicated. + +### Attribution and modifications + +Source: National Land Numerical Information (Lake Data W09), +Ministry of Land, Infrastructure, Transport and Tourism of Japan. + +The data used by EEW Renderer have been modified by the EEWBot project. +Modifications may include simplification, filtering, removal of attributes, +geometry processing, triangulation, conversion to internal data structures, +and embedding into generated/compiled application data. + +The modified data are not original, unmodified data distributed by MLIT. + +### Accuracy and usage limitations + +The National Land Numerical Information terms specify limitations that also +apply when the data or derived data are redistributed. + +In particular: + +* the data may contain spatial and temporal errors; +* some information may be missing or outdated; +* users are responsible for determining whether the data are suitable for + their intended purpose; +* the data are not intended for navigation, public surveying, certification, + or other applications requiring high positional or temporal accuracy; and +* MLIT and the original data providers assume no responsibility for damages + resulting from use of the data. + +Users receiving data derived from W09 should also review the applicable MLIT +terms of use linked above. + +This notice, or equivalent information preserving the applicable license, +provenance, modification notice, and usage limitations, should be retained +when redistributing data derived from W09. + +--- + +## No endorsement + +References to Natural Earth, the Japan Meteorological Agency, the Geospatial +Information Authority of Japan, and the Ministry of Land, Infrastructure, +Transport and Tourism are provided solely for attribution and provenance. + +Modifications made by the EEWBot project have not been reviewed or endorsed +by those organizations. diff --git a/asset-preprocessor/src/lib.rs b/asset-preprocessor/src/lib.rs index f5c81cf..99fe7e0 100644 --- a/asset-preprocessor/src/lib.rs +++ b/asset-preprocessor/src/lib.rs @@ -2,3 +2,4 @@ mod math; pub mod parse_lake_shapefile; pub mod parse_shapefile; pub mod parse_tsunami_shapefile; +pub mod parse_world_shapefile; diff --git a/asset-preprocessor/src/parse_shapefile.rs b/asset-preprocessor/src/parse_shapefile.rs index dea354a..166bd82 100644 --- a/asset-preprocessor/src/parse_shapefile.rs +++ b/asset-preprocessor/src/parse_shapefile.rs @@ -212,10 +212,10 @@ pub fn read( HashMap>, // area_bounding_box HashMap>, // area_centers Vec<(f32, f32)>, // vertex_buffer - Vec, // map_indices - Vec>, // area_lines - Vec>, // pref_lines - Vec<(f32, usize)>, // scale_level_map + [Vec; InsetRegion::COUNT], // map_indices (main / okinawa / ogasawara) + Vec>, // area_lines + Vec>, // pref_lines + Vec<(f32, usize)>, // scale_level_map ) { let shapefile = Shapefile::new( "../assets/shapefile/earthquake_detailed/earthquake_detailed_simplified.shp", @@ -234,13 +234,17 @@ pub fn read( .map(|area_rings| (area_rings.area_code, area_rings.bounding_box)) .collect(); - let map_indices = shapefile - .entries - .iter() - .flat_map(|area_rings| &area_rings.rings) - .flat_map(|r| r.triangulate()) - .map(|p| vertex_buffer.insert(p.into()) as u32) - .collect(); + let mut map_indices: [Vec; InsetRegion::COUNT] = Default::default(); + for area_rings in &shapefile.entries { + let region = classify_saibunkuiki(area_rings.area_code.0); + map_indices[region.index()].extend( + area_rings + .rings + .iter() + .flat_map(|r| r.triangulate()) + .map(|p| vertex_buffer.insert(p.into()) as u32), + ); + } let references = PointReferences::tally_of(&shapefile, area_code__pref_code); diff --git a/asset-preprocessor/src/parse_tsunami_shapefile.rs b/asset-preprocessor/src/parse_tsunami_shapefile.rs index e79eaec..87b23fd 100644 --- a/asset-preprocessor/src/parse_tsunami_shapefile.rs +++ b/asset-preprocessor/src/parse_tsunami_shapefile.rs @@ -7,7 +7,7 @@ use shapefile::dbase::{FieldValue, Record}; use shapefile::{Shape, ShapeReader}; use crate::math::*; -use renderer_types::codes; +use renderer_types::{classify_tsunami_area, codes, InsetRegion}; struct TsunamiAreaCodeBuffer { area_code_to_internal_code: HashMap, @@ -145,9 +145,9 @@ Please follow: } pub fn read() -> ( - Vec<(f32, f32, u16)>, // vertices - Vec, // indices - HashMap, // tsunami_area_code_to_internal_code + Vec<(f32, f32, u16)>, // vertices + [Vec; InsetRegion::COUNT], // indices (main / okinawa / ogasawara) + HashMap, // tsunami_area_code_to_internal_code ) { let shapefile = Shapefile::new( "../assets/shapefile/tsunami_forecast/tsunami_forecast_simplified.shp", @@ -155,10 +155,13 @@ pub fn read() -> ( ); let mut vertex_buffer = VertexBuffer::new(); - let mut lines = Vec::new(); + let mut lines: [Vec; InsetRegion::COUNT] = Default::default(); let mut tsunami_area_code_buffer = TsunamiAreaCodeBuffer::new(); for e in shapefile.entries { + let region = classify_tsunami_area(e.tsunami_area_code.0); + let lines = &mut lines[region.index()]; + for line in e.lines { let line: Vec = line .vertices diff --git a/asset-preprocessor/src/parse_world_shapefile.rs b/asset-preprocessor/src/parse_world_shapefile.rs new file mode 100644 index 0000000..fa0ef24 --- /dev/null +++ b/asset-preprocessor/src/parse_world_shapefile.rs @@ -0,0 +1,136 @@ +#![allow(clippy::type_complexity)] + +use std::collections::HashMap; + +use shapefile::{Shape, ShapeReader}; + +use crate::math::*; + +const WEB_MERCATOR_MAX_LATITUD: f32 = 85.051_13; + +struct VertexBuffer { + buffer: Vec<(Of32, Of32)>, + dict: HashMap<(Of32, Of32), usize>, +} + +impl VertexBuffer { + fn new() -> Self { + Self { + buffer: Default::default(), + dict: Default::default(), + } + } + + fn insert(&mut self, v: (Of32, Of32)) -> usize { + match self.dict.get(&v) { + Some(index) => *index, + None => { + self.buffer.push(v); + let index = self.buffer.len() - 1; + self.dict.insert(v, index); + index + } + } + } + + fn into_buffer(self) -> Vec<(f32, f32)> { + self.buffer.into_iter().map(|(x, y)| (x.0, y.0)).collect() + } +} + +/// 極点での発散を避けるため、球面Web Mercatorで一般的に使用される打ち切り緯度 ±85.05113° を、レンダラーの描画上限として採用する。 +/// 経度はそのまま返す。 +fn clamp_mercator_latitude(point: Point) -> Point { + let latitude = point + .latitude + .0 + .clamp(-WEB_MERCATOR_MAX_LATITUD, WEB_MERCATOR_MAX_LATITUD); + + Point::new(Of32::from(latitude), point.longitude) +} + +struct WorldRings { + rings: Vec, +} + +impl WorldRings { + fn from_shape(shape: Shape) -> Self { + let Shape::Polygon(polygon) = shape else { + panic!("world shapefile contains a non-Polygon shape"); + }; + + let rings: Vec<_> = polygon + .rings() + .iter() + .map(|ring| Ring::from(ring.points().to_vec())) + .collect(); + + Self { rings } + } +} + +struct Shapefile { + entries: Vec, +} + +impl Shapefile { + fn new() -> Self { + let shp_file = std::fs::File::open("../assets/shapefile/world/world.shp"); + + let Ok(shp_file) = shp_file else { + panic!( + r#"EEWBot Renderer requirements is not satisfied. + +World shape file is not found. + - assets/shapefile/world/world.shp + +Please follow: + https://github.com/EEWBot/eew-renderer/wiki#shapefile-%E5%85%A5%E6%89%8B%E5%85%88"# + ) + }; + + let mut shape_reader = ShapeReader::new(shp_file).unwrap(); + + let entries = shape_reader + .iter_shapes() + .map(|shape| shape.expect("Failed to read a shape from world shapefile")) + .map(WorldRings::from_shape) + .collect(); + + Self { entries } + } +} + +pub fn read() -> ( + Vec<(f32, f32)>, // vertices + Vec, // indices +) { + let shapefile = Shapefile::new(); + + let mut vertex_buffer = VertexBuffer::new(); + + let world_indices: Vec = shapefile + .entries + .iter() + .flat_map(|world_rings| &world_rings.rings) + .flat_map(|ring| ring.triangulate()) + .map(clamp_mercator_latitude) + .map(|point| { + u32::try_from(vertex_buffer.insert(point.into())) + .expect("world shapefile has too many vertices") + }) + .collect(); + + let world_vertices = vertex_buffer.into_buffer(); + + assert!( + !world_vertices.is_empty(), + "world shapefile produced no vertices" + ); + assert!( + !world_indices.is_empty(), + "world shapefile produced no indices" + ); + + (world_vertices, world_indices) +} diff --git a/assets/shapefile/earthquake_detailed/README.md b/assets/shapefile/earthquake_detailed/README.md index fd98664..de9ae17 100644 --- a/assets/shapefile/earthquake_detailed/README.md +++ b/assets/shapefile/earthquake_detailed/README.md @@ -1,3 +1,13 @@ -https://github.com/EEWBot/eew-renderer/wiki に従い、以下のファイルを作成する - - `assets/shapefile/earthquake_detailed/earthquake_detailed_simplified.shp` - - `assets/shapefile/earthquake_detailed/earthquake_detailed_simplified.dbf` +# earthquake_detailed + +## 出典 + +気象庁「予報区等GISデータの一覧」/地震情報/細分区域 +- https://www.data.jma.go.jp/developer/gis.html + +ライセンス・出典の詳細は [THIRD_PARTY_NOTICES.md](../../../THIRD_PARTY_NOTICES.md) を参照してください。 + +## 加工 + +同梱しているファイルは EEWBot により簡略化・属性処理・ジオメトリ処理等の加工を行ったものであり、気象庁が配布するデータそのものではありません。 +作成方法は https://github.com/EEWBot/eew-renderer/wiki を参照してください。 diff --git a/assets/shapefile/earthquake_detailed/earthquake_detailed_simplified.dbf b/assets/shapefile/earthquake_detailed/earthquake_detailed_simplified.dbf new file mode 100644 index 0000000..fff712d Binary files /dev/null and b/assets/shapefile/earthquake_detailed/earthquake_detailed_simplified.dbf differ diff --git a/assets/shapefile/earthquake_detailed/earthquake_detailed_simplified.shp b/assets/shapefile/earthquake_detailed/earthquake_detailed_simplified.shp new file mode 100644 index 0000000..8f8e118 Binary files /dev/null and b/assets/shapefile/earthquake_detailed/earthquake_detailed_simplified.shp differ diff --git a/assets/shapefile/earthquake_detailed/earthquake_detailed_simplified.shx b/assets/shapefile/earthquake_detailed/earthquake_detailed_simplified.shx new file mode 100644 index 0000000..0fe45af Binary files /dev/null and b/assets/shapefile/earthquake_detailed/earthquake_detailed_simplified.shx differ diff --git a/assets/shapefile/lake_reduced/README.md b/assets/shapefile/lake_reduced/README.md new file mode 100644 index 0000000..471d36c --- /dev/null +++ b/assets/shapefile/lake_reduced/README.md @@ -0,0 +1,13 @@ +# lake_reduced + +## 出典 + +国土数値情報「湖沼データ」(W09)/国土交通省 +- https://nlftp.mlit.go.jp/ksj/gml/datalist/KsjTmplt-W09-v2_2.html + +ライセンス・出典の詳細は [THIRD_PARTY_NOTICES.md](../../../THIRD_PARTY_NOTICES.md) を参照してください。 + +## 加工 + +同梱しているファイルは EEWBot により簡略化・属性削除・ジオメトリ処理等の加工を行ったものであり、国土交通省が配布するデータそのものではありません。 +作成方法は https://github.com/EEWBot/eew-renderer/wiki を参照してください。 diff --git a/assets/shapefile/lake_reduced/lake_reduced_simplified.dbf b/assets/shapefile/lake_reduced/lake_reduced_simplified.dbf new file mode 100644 index 0000000..c88a15a Binary files /dev/null and b/assets/shapefile/lake_reduced/lake_reduced_simplified.dbf differ diff --git a/assets/shapefile/lake_reduced/lake_reduced_simplified.shp b/assets/shapefile/lake_reduced/lake_reduced_simplified.shp new file mode 100644 index 0000000..03b2572 Binary files /dev/null and b/assets/shapefile/lake_reduced/lake_reduced_simplified.shp differ diff --git a/assets/shapefile/tsunami_forecast/README.md b/assets/shapefile/tsunami_forecast/README.md index 045e669..b68d9fb 100644 --- a/assets/shapefile/tsunami_forecast/README.md +++ b/assets/shapefile/tsunami_forecast/README.md @@ -1,6 +1,13 @@ -https://www.data.jma.go.jp/developer/gis.html -からGISファイルをダウンロードし、それぞれ以下のようにリネームし、配置する。 +# tsunami_forecast -- `tsunami_forecast.dbf` -- `tsunami_forecast.shp` -- `tsunami_forecast.shx` +## 出典 + +気象庁「予報区等GISデータの一覧」/津波予報区 +- https://www.data.jma.go.jp/developer/gis.html + +ライセンス・出典の詳細は [THIRD_PARTY_NOTICES.md](../../../THIRD_PARTY_NOTICES.md) を参照してください。 + +## 加工 + +同梱しているファイルは EEWBot により簡略化・属性処理・ジオメトリ処理等の加工を行ったものであり、気象庁が配布するデータそのものではありません。 +作成方法は https://github.com/EEWBot/eew-renderer/wiki を参照してください。 diff --git a/assets/shapefile/tsunami_forecast/tsunami_forecast_simplified.dbf b/assets/shapefile/tsunami_forecast/tsunami_forecast_simplified.dbf new file mode 100644 index 0000000..d950c9f Binary files /dev/null and b/assets/shapefile/tsunami_forecast/tsunami_forecast_simplified.dbf differ diff --git a/assets/shapefile/tsunami_forecast/tsunami_forecast_simplified.shp b/assets/shapefile/tsunami_forecast/tsunami_forecast_simplified.shp new file mode 100644 index 0000000..d3c30cf Binary files /dev/null and b/assets/shapefile/tsunami_forecast/tsunami_forecast_simplified.shp differ diff --git a/assets/shapefile/tsunami_forecast/tsunami_forecast_simplified.shx b/assets/shapefile/tsunami_forecast/tsunami_forecast_simplified.shx new file mode 100644 index 0000000..e03254b Binary files /dev/null and b/assets/shapefile/tsunami_forecast/tsunami_forecast_simplified.shx differ diff --git a/assets/shapefile/world/README.md b/assets/shapefile/world/README.md new file mode 100644 index 0000000..6490916 --- /dev/null +++ b/assets/shapefile/world/README.md @@ -0,0 +1,13 @@ +# world + +## 出典 + +Natural Earth 1:10m Cultural Vectors / Admin 0 – Countries +- https://www.naturalearthdata.com/downloads/10m-cultural-vectors/ + +ライセンス・出典の詳細は [THIRD_PARTY_NOTICES.md](../../../THIRD_PARTY_NOTICES.md) を参照してください。 + +## 加工 + +同梱しているファイルは EEWBot により簡略化・変換等の加工を行ったものであり、Natural Earth が配布するデータそのものではありません。 +作成方法は https://github.com/EEWBot/eew-renderer/wiki を参照してください。 diff --git a/assets/shapefile/world/world.shp b/assets/shapefile/world/world.shp new file mode 100644 index 0000000..4d6e070 Binary files /dev/null and b/assets/shapefile/world/world.shp differ diff --git a/init-virtualgl.sh b/init-virtualgl.sh deleted file mode 100755 index 61fd7a2..0000000 --- a/init-virtualgl.sh +++ /dev/null @@ -1,22 +0,0 @@ -#!/bin/bash - -# depName=VirtualGL/virtualgl -VIRTUALGL_VERSION="3.1.4" - -apt-get install -y wget - -case "$(uname -m)" in - x86_64) ARCH="amd64" ;; - aarch64) ARCH="arm64" ;; - *) echo "Unsupported architecture: $(uname -m)" >&2; exit 1;; -esac - -wget \ - "https://github.com/VirtualGL/virtualgl/releases/download/${VIRTUALGL_VERSION}/virtualgl_${VIRTUALGL_VERSION}_${ARCH}.deb" \ - -O "/virtualgl_${VIRTUALGL_VERSION}_${ARCH}.deb" - -apt-get install -y "/virtualgl_${VIRTUALGL_VERSION}_${ARCH}.deb" - -rm "/virtualgl_${VIRTUALGL_VERSION}_${ARCH}.deb" - -apt-get purge -y wget diff --git a/renderer-assets/build.rs b/renderer-assets/build.rs index da7c4f0..76778ce 100644 --- a/renderer-assets/build.rs +++ b/renderer-assets/build.rs @@ -6,7 +6,9 @@ use const_gen::*; use ordered_float::NotNan; mod station_codes_parser; -use asset_preprocessor::{parse_lake_shapefile, parse_shapefile, parse_tsunami_shapefile}; +use asset_preprocessor::{ + parse_lake_shapefile, parse_shapefile, parse_tsunami_shapefile, parse_world_shapefile, +}; use renderer_types::{BoundingBox, GeoDegree, Vertex}; fn main() { @@ -15,6 +17,8 @@ fn main() { let (lake_vertices, lake_indices) = parse_lake_shapefile::read(); + let (world_vertices, world_indices) = parse_world_shapefile::read(); + let s = std::fs::read_to_string("../assets/intensity_stations.json").unwrap(); #[allow(non_snake_case)] @@ -77,6 +81,8 @@ fn main() { const_declaration!(SCALE_LEVEL_MAP = scale_level_map), const_declaration!(LAKE_VERTICES = lake_vertices), const_declaration!(LAKE_INDICES = lake_indices), + const_declaration!(WORLD_VERTICES = world_vertices), + const_declaration!(WORLD_INDICES = world_indices), const_declaration!(TSUNAMI_VERTICES = tsunami_vertices), const_declaration!(TSUNAMI_INDICES = tsunami_indices), const_declaration!(TSUNAMI_AREA_CODE_TO_INTERNAL_CODE = tsunami_area_code_to_internal_code), diff --git a/renderer-assets/src/lib.rs b/renderer-assets/src/lib.rs index 21ae8c8..248649d 100644 --- a/renderer-assets/src/lib.rs +++ b/renderer-assets/src/lib.rs @@ -9,7 +9,7 @@ pub struct QueryInterface; pub struct Geometries { pub vertices: &'static [(f32, f32)], - pub map_triangles: &'static [u32], + pub map_triangles: [&'static [u32]; InsetRegion::COUNT], pub area_lines: &'static [&'static [u32]], pub pref_lines: &'static [&'static [u32]], } @@ -19,9 +19,14 @@ pub struct LakeGeometries { pub indices: &'static [u32], } +pub struct WorldGeometries { + pub vertices: &'static [(f32, f32)], + pub indices: &'static [u32], +} + pub struct TsunamiGeometries { pub vertices: &'static [(f32, f32, u16)], - pub indices: &'static [u32], + pub indices: [&'static [u32]; InsetRegion::COUNT], } impl QueryInterface { @@ -41,6 +46,13 @@ impl QueryInterface { } } + pub fn world_geometries() -> WorldGeometries { + WorldGeometries { + vertices: WORLD_VERTICES, + indices: WORLD_INDICES, + } + } + pub fn tsunami_geometries() -> TsunamiGeometries { TsunamiGeometries { vertices: TSUNAMI_VERTICES, diff --git a/renderer-macros/Cargo.toml b/renderer-macros/Cargo.toml new file mode 100644 index 0000000..16a2bc2 --- /dev/null +++ b/renderer-macros/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "renderer-macros" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[lib] +proc-macro = true + +[dependencies] +syn = "3" +quote = "1" diff --git a/renderer-macros/src/lib.rs b/renderer-macros/src/lib.rs new file mode 100644 index 0000000..cbfc110 --- /dev/null +++ b/renderer-macros/src/lib.rs @@ -0,0 +1,75 @@ +use proc_macro::TokenStream; +use syn::{parse_macro_input, LitStr, LitFloat, Token}; +use syn::parse::{Parse, ParseStream}; +use quote::quote; + +struct RgbArgs { + hex: LitStr, +} + +impl Parse for RgbArgs { + fn parse(input: ParseStream) -> syn::Result { + Ok(RgbArgs { + hex: input.parse()?, + }) + } +} + +#[proc_macro] +pub fn rgb(input: TokenStream) -> TokenStream { + let RgbArgs { hex, .. } = parse_macro_input!(input as RgbArgs); + + let hex_str = hex.value(); + let hex_str = hex_str.trim_start_matches('#'); + let r = u8::from_str_radix(&hex_str[0..2], 16).unwrap(); + let g = u8::from_str_radix(&hex_str[2..4], 16).unwrap(); + let b = u8::from_str_radix(&hex_str[4..6], 16).unwrap(); + + let expanded = quote! { + [ + #r as f32 / 255.0, + #g as f32 / 255.0, + #b as f32 / 255.0, + ] + }; + + expanded.into() +} + +struct RgbaArgs { + hex: LitStr, + _comma: Token![,], + alpha: LitFloat, +} + +impl Parse for RgbaArgs { + fn parse(input: ParseStream) -> syn::Result { + Ok(RgbaArgs { + hex: input.parse()?, + _comma: input.parse()?, + alpha: input.parse()?, + }) + } +} + +#[proc_macro] +pub fn rgba(input: TokenStream) -> TokenStream { + let RgbaArgs { hex, alpha, .. } = parse_macro_input!(input as RgbaArgs); + + let hex_str = hex.value(); + let hex_str = hex_str.trim_start_matches('#'); + let r = u8::from_str_radix(&hex_str[0..2], 16).unwrap(); + let g = u8::from_str_radix(&hex_str[2..4], 16).unwrap(); + let b = u8::from_str_radix(&hex_str[4..6], 16).unwrap(); + + let expanded = quote! { + [ + #r as f32 / 255.0, + #g as f32 / 255.0, + #b as f32 / 255.0, + #alpha, + ] + }; + + expanded.into() +} diff --git a/renderer-types/src/lib.rs b/renderer-types/src/lib.rs index c813f60..f348699 100644 --- a/renderer-types/src/lib.rs +++ b/renderer-types/src/lib.rs @@ -13,3 +13,6 @@ mod coord_types; pub use coord_types::*; pub mod codes; + +pub mod region; +pub use region::*; diff --git a/renderer-types/src/region.rs b/renderer-types/src/region.rs new file mode 100644 index 0000000..b8660ef --- /dev/null +++ b/renderer-types/src/region.rs @@ -0,0 +1,61 @@ +use crate::{BoundingBox, GeoDegree, Vertex}; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum InsetRegion { + Main, + Okinawa, + Ogasawara, +} + +impl InsetRegion { + pub const COUNT: usize = 3; + + pub const fn index(self) -> usize { + self as usize + } +} + +/// 772: 奄美群島・トカラ列島 / 800: 沖縄本島地方 / 801: 大東島地方 / 802: 宮古島・八重山地方 +pub const TSUNAMI_AREA_CODES_OKINAWA: &[u32] = &[772, 800, 801, 802]; + +/// 321: 小笠原諸島 +pub const TSUNAMI_AREA_CODES_OGASAWARA: &[u32] = &[321]; + +/// 774: 鹿児島県十島村 / 778: 鹿児島県奄美北部 / 779: 鹿児島県奄美南部 +/// 800: 沖縄県本島北部 / 801: 沖縄県本島中南部 / 802: 沖縄県久米島 / 803: 沖縄県大東島 +/// 804: 沖縄県宮古島 / 805: 沖縄県石垣島 / 806: 沖縄県与那国島 / 807: 沖縄県西表島 +pub const SAIBUNKUIKI_CODES_OKINAWA: &[u32] = + &[774, 778, 779, 800, 801, 802, 803, 804, 805, 806, 807]; + +/// 359: 東京都小笠原 +pub const SAIBUNKUIKI_CODES_OGASAWARA: &[u32] = &[359]; + +fn classify(code: u32, okinawa: &[u32], ogasawara: &[u32]) -> InsetRegion { + if okinawa.contains(&code) { + InsetRegion::Okinawa + } else if ogasawara.contains(&code) { + InsetRegion::Ogasawara + } else { + InsetRegion::Main + } +} + +pub fn classify_tsunami_area(code: u32) -> InsetRegion { + classify( + code, + TSUNAMI_AREA_CODES_OKINAWA, + TSUNAMI_AREA_CODES_OGASAWARA, + ) +} + +pub fn classify_saibunkuiki(code: u32) -> InsetRegion { + classify(code, SAIBUNKUIKI_CODES_OKINAWA, SAIBUNKUIKI_CODES_OGASAWARA) +} + +/// ((与那国島西, 波照間島南), (北大東島東, 口之島北)) +pub const OKINAWA_VIEW_BBOX: BoundingBox = + BoundingBox::new(Vertex::new(122.5, 23.9), Vertex::new(132.2, 30.1)); + +/// ((西ノ島西, 南硫黄島南), (東, 聟島列島北)) +pub const OGASAWARA_VIEW_BBOX: BoundingBox = + BoundingBox::new(Vertex::new(140.2, 23.9), Vertex::new(142.5, 27.9)); diff --git a/renderer/Cargo.toml b/renderer/Cargo.toml index 098b345..326f7e0 100644 --- a/renderer/Cargo.toml +++ b/renderer/Cargo.toml @@ -31,6 +31,7 @@ prost = "0.14.0" rand = "0.10.0" renderer-assets = { path = "../renderer-assets/" } renderer-types = { path = "../renderer-types/" } +renderer-macros = { path = "../renderer-macros/" } rusttype = { version = "0.9.3", features = ["gpu_cache"] } sha1 = "0.11.0" strum = { version = "0.28.0", features = ["derive"] } diff --git a/renderer/src/frame_context.rs b/renderer/src/frame_context.rs index 65d427b..02b2b18 100644 --- a/renderer/src/frame_context.rs +++ b/renderer/src/frame_context.rs @@ -105,6 +105,27 @@ pub enum FramePayload { TsunamiSecond(TsunamiSecondPayload), } +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct MapLayerConfig { + pub world: bool, + pub saibunkuiki_line: bool, +} + +impl FramePayload { + pub fn map_layers(&self) -> MapLayerConfig { + match self { + FramePayload::Earthquake(_) => MapLayerConfig { + world: true, + saibunkuiki_line: true, + }, + FramePayload::TsunamiFirst(_) | FramePayload::TsunamiSecond(_) => MapLayerConfig { + world: false, + saibunkuiki_line: false, + }, + } + } +} + #[derive(Debug)] pub struct FrameContext { pub payload: FramePayload, diff --git a/renderer/src/main.rs b/renderer/src/main.rs index f3236ac..01990bc 100644 --- a/renderer/src/main.rs +++ b/renderer/src/main.rs @@ -42,6 +42,12 @@ struct Cli { #[clap(long, env)] #[clap(default_value_t = 512)] image_cache_capacity: u64, + + #[clap(long, env, default_value_t = false)] + headless: bool, + + #[clap(long, env, default_value_t = 0)] + egl_device_index: usize, } #[tokio::main] @@ -57,18 +63,23 @@ async fn main() -> Result<(), Box> { "Minimum Response Interval: {}", cli.minimum_response_interval ); + tracing::info!("Headless: {}", cli.headless); if cli.security_rules.bypass_hmac { tracing::warn!("[SECURITY NOTICE] BYPASS HMAC MODE!"); tracing::warn!("[SECURITY NOTICE] DO NOT USE THIS OPTION IN PRODUCTION!!"); } - let (webe_tx, webe_rx) = tokio::sync::oneshot::channel::>(); let (tx, rx) = tokio::sync::mpsc::channel::(16); + let (headless, egl_device_index) = (cli.headless, cli.egl_device_index); + + let listener = tokio::net::TcpListener::bind(&cli.listen).await.unwrap(); + tracing::info!("Listening on {}", cli.listen); + tokio::spawn(async move { let e = web::run( - cli.listen, + listener, tx, &cli.hmac_key, &cli.instance_name, @@ -79,16 +90,13 @@ async fn main() -> Result<(), Box> { ) .await; - webe_tx.send(e).unwrap() + tracing::error!("UNRECOVERABLE ERROR (Web): {e:?}"); + std::process::exit(1); }); - tokio::select! { - e = worker::run(rx) => { - tracing::error!("UNRECOVERABLE ERROR (Worker): {e:?}"); - } - e = webe_rx => { - tracing::error!("UNRECOVERABLE ERROR (Web): {e:?}"); - } + if let Err(e) = worker::run(rx, headless, egl_device_index).await { + tracing::error!("UNRECOVERABLE ERROR (Worker): {e:?}"); + std::process::exit(1); } Ok(()) diff --git a/renderer/src/web/mod.rs b/renderer/src/web/mod.rs index 7309edc..bd83ce0 100644 --- a/renderer/src/web/mod.rs +++ b/renderer/src/web/mod.rs @@ -2,7 +2,7 @@ use std::net::SocketAddr; use std::sync::Arc; use std::time::{Duration, Instant}; -use anyhow::{Context, Result}; +use anyhow::Result; use axum::{ extract::{Request, State}, http::{header::CONTENT_TYPE, HeaderName, HeaderValue, StatusCode}, @@ -360,7 +360,7 @@ async fn root_handler(State(app): State, ClientIp(_client_ip): ClientI #[allow(clippy::too_many_arguments)] pub async fn run( - listen: SocketAddr, + listener: tokio::net::TcpListener, request_channel: tokio::sync::mpsc::Sender, hmac_key: &str, instance_name: &str, @@ -391,12 +391,6 @@ pub async fn run( }) .layer(client_ip_source.into_extension()); - let listener = tokio::net::TcpListener::bind(listen) - .await - .with_context(|| format!("Failed to bind address {listen}"))?; - - tracing::info!("Listening on {listen}"); - axum::serve( listener, app.into_make_service_with_connect_info::(), diff --git a/renderer/src/worker/camera.rs b/renderer/src/worker/camera.rs new file mode 100644 index 0000000..e6b826e --- /dev/null +++ b/renderer/src/worker/camera.rs @@ -0,0 +1,41 @@ +use renderer_types::{BoundingBox, GeoDegree, Mercator, Size, Vertex}; + +const MAXIMUM_SCALE: f32 = 100.0; + +pub struct Camera { + pub offset: Vertex, + pub scale: f32, + pub image_size: Size, +} + +impl Camera { + pub fn fit(view_bbox: &BoundingBox, image_size: Size) -> Camera { + let rendering_bbox = BoundingBox::from_vertices_float( + &view_bbox + .gl_vertices() + .iter() + .map(|v| v.to_mercator()) + .collect::>(), + ); + + Camera { + offset: -rendering_bbox.center(), + scale: calculate_map_scale(rendering_bbox, image_size), + image_size, + } + } + + pub fn with_margin(self, factor: f32) -> Camera { + Camera { + scale: self.scale / factor, + ..self + } + } +} + +fn calculate_map_scale(bounding_box: BoundingBox, image_size: Size) -> f32 { + let x_scale = 1.0 / bounding_box.size().x(); + let y_scale = 1.0 / bounding_box.size().y() * image_size.aspect_ratio(); + + f32::min(f32::min(x_scale, y_scale) * 2.0, MAXIMUM_SCALE) +} diff --git a/renderer/src/worker/drawer_epicenter.rs b/renderer/src/worker/drawer_epicenter.rs index 0813c99..b246d29 100644 --- a/renderer/src/worker/drawer_epicenter.rs +++ b/renderer/src/worker/drawer_epicenter.rs @@ -4,47 +4,60 @@ use crate::worker::FrameContext; use glium::backend::Facade; use glium::index::{NoIndices, PrimitiveType}; use glium::{Surface, VertexBuffer}; +use renderer_types::{GeoDegree, Vertex}; use std::ops::DerefMut; -pub fn draw( - frame_context: &FrameContext, - rendering_context: &C, -) { - let facade = frame_context.facade; - let resources = frame_context.resources; - let aspect_ratio = frame_context.image_size.aspect_ratio(); - let offset = frame_context.offset; - let scale = frame_context.scale; - let draw_parameters = frame_context.draw_parameters; - - if rendering_context.epicenter().is_empty() { - return; +pub fn create_vertex_buffer( + facade: &F, + epicenters: &[Vertex], +) -> Option> { + if epicenters.is_empty() { + return None; } - let vb = rendering_context - .epicenter() + let vertices = epicenters .iter() .map(|epicenter| EpicenterVertex { position: (*epicenter).into(), }) .collect::>(); - let vb = VertexBuffer::dynamic(facade, &vb).unwrap(); + + Some(VertexBuffer::dynamic(facade, &vertices).unwrap()) +} + +pub fn draw_vertex_buffer( + frame_context: &FrameContext, + vertex_buffer: &VertexBuffer, + icon_ratio_in_y_axis: f32, +) { + let resources = frame_context.resources; resources .shader .epicenter .draw( frame_context.surface.borrow_mut().deref_mut(), - &vb, + vertex_buffer, NoIndices(PrimitiveType::Points), &EpicenterUniform { - aspect_ratio, - offset: offset.into(), - zoom: scale, - icon_ratio_in_y_axis: super::ICON_RATIO_IN_Y_AXIS, + aspect_ratio: frame_context.camera.image_size.aspect_ratio(), + offset: frame_context.camera.offset.into(), + zoom: frame_context.camera.scale, + icon_ratio_in_y_axis, texture_sampler: &resources.texture.epicenter, }, - draw_parameters, + frame_context.premultiplied_draw_parameters, ) .unwrap(); } + +pub fn draw( + frame_context: &FrameContext, + rendering_context: &C, +) { + if let Some(vertex_buffer) = + create_vertex_buffer(frame_context.facade, rendering_context.epicenter()) + { + draw_vertex_buffer(frame_context, &vertex_buffer, super::ICON_RATIO_IN_Y_AXIS); + } +} diff --git a/renderer/src/worker/drawer_inset_frame.rs b/renderer/src/worker/drawer_inset_frame.rs new file mode 100644 index 0000000..ce22910 --- /dev/null +++ b/renderer/src/worker/drawer_inset_frame.rs @@ -0,0 +1,203 @@ +use crate::worker::fonts::{Font, Offset, Origin}; +use crate::worker::inset::BorderSides; +use crate::worker::notch::ResolvedNotch; +use crate::worker::vertex::{ShapeUniform, ShapeVertex}; +use crate::worker::FrameContext; +use glium::backend::Facade; +use glium::draw_parameters::{StencilOperation, StencilTest}; +use glium::index::{NoIndices, PrimitiveType}; +use glium::Surface; +use rusttype::Scale; +use std::ops::DerefMut; + +fn push_quad(vertices: &mut Vec, x0: f32, y0: f32, x1: f32, y1: f32) { + let quad = [[x0, y0], [x1, y0], [x1, y1], [x0, y0], [x1, y1], [x0, y1]]; + vertices.extend(quad.map(|position| ShapeVertex { position })); +} + +fn border_vertices(width_x: f32, width_y: f32, sides: &BorderSides) -> Vec { + let inner_left = -1.0 + width_x; + let inner_right = 1.0 - width_x; + let inner_bottom = -1.0 + width_y; + let inner_top = 1.0 - width_y; + + let side_bottom = if sides.bottom { inner_bottom } else { -1.0 }; + let side_top = if sides.top { inner_top } else { 1.0 }; + + let mut vertices = Vec::with_capacity(30); + if sides.top { + push_quad(&mut vertices, -1.0, inner_top, 1.0, 1.0); + } + if sides.bottom { + push_quad(&mut vertices, -1.0, -1.0, 1.0, inner_bottom); + } + if sides.left { + push_quad(&mut vertices, -1.0, side_bottom, inner_left, side_top); + } + if sides.right { + push_quad(&mut vertices, inner_right, side_bottom, 1.0, side_top); + } + vertices +} + +pub fn draw_stencil_mask( + frame_context: &FrameContext, + notch: &ResolvedNotch, +) { + let vertices: Vec<_> = notch + .kept_polygon + .as_slice() + .iter() + .map(|&position| ShapeVertex { position }) + .collect(); + + let buffer = &frame_context.resources.inset.notch_mask_vertex_buffer; + let slice = buffer.slice(0..vertices.len()).unwrap(); + slice.write(&vertices); + + let mut draw_parameters = frame_context.draw_parameters.clone(); + draw_parameters.color_mask = (false, false, false, false); + draw_parameters.stencil = + crate::worker::stencil_params(StencilTest::AlwaysPass, StencilOperation::Replace); + + frame_context + .resources + .shader + .shape + .draw( + frame_context.surface.borrow_mut().deref_mut(), + slice, + NoIndices(PrimitiveType::TriangleFan), + &ShapeUniform { + color: [0.0, 0.0, 0.0, 0.0], + }, + &draw_parameters, + ) + .unwrap(); +} + +pub fn draw_background( + frame_context: &FrameContext, +) { + frame_context + .resources + .shader + .shape + .draw( + frame_context.surface.borrow_mut().deref_mut(), + &frame_context.resources.inset.background_vertex_buffer, + NoIndices(PrimitiveType::TriangleStrip), + &ShapeUniform { + color: frame_context.theme.inset_background_color, + }, + frame_context.draw_parameters, + ) + .unwrap(); +} + +fn push_notch_border( + vertices: &mut Vec, + notch: &ResolvedNotch, + width_x: f32, + width_y: f32, +) { + let [ax, ay] = notch.a; + let [bx, by] = notch.b; + + let dir = [(bx - ax) / width_x, (by - ay) / width_y]; + let length = f32::hypot(dir[0], dir[1]); + let normal = [-dir[1] / length, dir[0] / length]; + + let mut offset = [normal[0] * width_x, normal[1] * width_y]; + if notch.side([ax + offset[0], ay + offset[1]]).signum() != notch.keep_sign() { + offset = [-offset[0], -offset[1]]; + } + + let inner_a = [ax + offset[0], ay + offset[1]]; + let inner_b = [bx + offset[0], by + offset[1]]; + + let quad = [notch.a, notch.b, inner_b, notch.a, inner_b, inner_a]; + vertices.extend(quad.map(|position| ShapeVertex { position })); +} + +fn label_offset_x(notch: Option<&ResolvedNotch>, viewport_width: f32) -> i32 { + let Some(notch) = notch else { + return 0; + }; + const EPSILON: f32 = 1e-3; + let (min, max, count) = notch + .kept_polygon + .as_slice() + .iter() + .filter(|[_, y]| *y <= -1.0 + EPSILON) + .fold( + (f32::INFINITY, f32::NEG_INFINITY, 0usize), + |(min, max, count), [x, _]| (min.min(*x), max.max(*x), count + 1), + ); + if count < 2 { + return 0; + } + + ((min + max) / 2.0 * viewport_width / 2.0).round() as i32 +} + +pub fn draw_border_and_label( + frame_context: &FrameContext, + sides: &BorderSides, + notch: Option<&ResolvedNotch>, + label: &str, +) { + let theme = frame_context.theme; + let image_size = frame_context.camera.image_size.to_f32(); + + let width_x = 2.0 * theme.inset_border_width / image_size.x(); + let width_y = 2.0 * theme.inset_border_width / image_size.y(); + + let mut vertices = border_vertices(width_x, width_y, sides); + if let Some(notch) = notch { + push_notch_border(&mut vertices, notch, width_x, width_y); + } + + if !vertices.is_empty() { + let buffer = &frame_context.resources.inset.border_vertex_buffer; + let slice = buffer.slice(0..vertices.len()).unwrap(); + slice.write(&vertices); + + frame_context + .resources + .shader + .shape + .draw( + frame_context.surface.borrow_mut().deref_mut(), + slice, + NoIndices(PrimitiveType::TrianglesList), + &ShapeUniform { + color: theme.inset_border_color, + }, + frame_context.draw_parameters, + ) + .unwrap(); + } + + frame_context + .font_manager + .borrow_mut() + .deref_mut() + .draw_text( + label, + Font::BizUDPGothicBold, + theme.inset_label_color, + Scale::uniform(theme.inset_label_font_size), + Offset::new( + Origin::CenterDown, + Origin::CenterDown, + label_offset_x(notch, image_size.x()), + theme.inset_label_offset_y, + ), + frame_context.camera.image_size.into(), + frame_context.resources, + frame_context.facade, + frame_context.surface.borrow_mut().deref_mut(), + frame_context.draw_parameters, + ); +} diff --git a/renderer/src/worker/drawer_intensity_icon.rs b/renderer/src/worker/drawer_intensity_icon.rs index c045b08..7566ef9 100644 --- a/renderer/src/worker/drawer_intensity_icon.rs +++ b/renderer/src/worker/drawer_intensity_icon.rs @@ -31,10 +31,9 @@ pub fn draw_all( ) { let facade = frame_context.facade; let resources = frame_context.resources; - let aspect_ratio = frame_context.image_size.aspect_ratio(); - let offset = frame_context.offset; - let scale = frame_context.scale; - let draw_parameters = frame_context.draw_parameters; + let aspect_ratio = frame_context.camera.image_size.aspect_ratio(); + let offset = frame_context.camera.offset; + let scale = frame_context.camera.scale; let per_icon_data: Vec<_> = earthquake_payload .area_intensities @@ -70,7 +69,7 @@ pub fn draw_all( icon_ratio_in_y_axis: super::ICON_RATIO_IN_Y_AXIS, texture_sampler: &resources.texture.intensity, }, - draw_parameters, + frame_context.premultiplied_draw_parameters, ) .unwrap(); } diff --git a/renderer/src/worker/drawer_map.rs b/renderer/src/worker/drawer_map.rs index 9c28540..2be2efb 100644 --- a/renderer/src/worker/drawer_map.rs +++ b/renderer/src/worker/drawer_map.rs @@ -1,20 +1,44 @@ +use crate::frame_context::MapLayerConfig; use crate::worker::vertex::{BorderLineUniform, MapUniform}; use crate::worker::FrameContext; use glium::backend::Facade; use glium::Surface; +use renderer_types::InsetRegion; use std::ops::DerefMut; pub fn draw( frame_context: &FrameContext, - has_saibunkuiki: bool, + layers: MapLayerConfig, + region: Option, ) { let theme = frame_context.theme; let params = frame_context.draw_parameters; let resources = frame_context.resources; - let scale = frame_context.scale; - let aspect_ratio = frame_context.image_size.aspect_ratio(); - let offset = frame_context.offset.into(); - let image_size: [f32; 2] = frame_context.image_size.to_f32().into(); + let scale = frame_context.camera.scale; + let aspect_ratio = frame_context.camera.image_size.aspect_ratio(); + let offset = frame_context.camera.offset.into(); + let image_size: [f32; 2] = frame_context.camera.image_size.to_f32().into(); + + let map_uniform = MapUniform { + aspect_ratio, + offset, + zoom: scale, + color: theme.ground_color, + }; + + if layers.world { + resources + .shader + .map + .draw( + frame_context.surface.borrow_mut().deref_mut(), + &resources.world.vertex, + &resources.world.index, + &map_uniform, + params, + ) + .unwrap(); + } resources .shader @@ -22,39 +46,37 @@ pub fn draw( .draw( frame_context.surface.borrow_mut().deref_mut(), &resources.buffer.map_vertex, - &resources.buffer.map, - &MapUniform { - aspect_ratio, - offset, - zoom: scale, - color: theme.ground_color, - }, + resources.buffer.get_map_slice(region), + &map_uniform, params, ) .unwrap(); - resources - .shader - .map - .draw( - frame_context.surface.borrow_mut().deref_mut(), - &resources.lake.vertex, - &resources.lake.index, - &MapUniform { - aspect_ratio, - offset, - zoom: scale, - color: [ - theme.clear_color[0], - theme.clear_color[1], - theme.clear_color[2], - ], - }, - params, - ) - .unwrap(); + // lakeと県境線は本土のみに存在するため、Main以外のinset描画時はskip + let draws_mainland = region.is_none_or(|region| region == InsetRegion::Main); - if has_saibunkuiki { + if draws_mainland { + resources + .shader + .map + .draw( + frame_context.surface.borrow_mut().deref_mut(), + &resources.lake.vertex, + &resources.lake.index, + &MapUniform { + color: [ + theme.clear_color[0], + theme.clear_color[1], + theme.clear_color[2], + ], + ..map_uniform + }, + params, + ) + .unwrap(); + } + + if layers.saibunkuiki_line { resources .shader .border_line @@ -74,21 +96,23 @@ pub fn draw( .unwrap(); } - resources - .shader - .border_line - .draw( - frame_context.surface.borrow_mut().deref_mut(), - &resources.buffer.map_vertex, - resources.buffer.get_pref_line_by_scale(scale).unwrap(), - &BorderLineUniform { - dimension: image_size, - offset, - zoom: scale, - line_width: theme.prefectural_border_width, - color: theme.prefectural_border_color, - }, - params, - ) - .unwrap(); + if draws_mainland { + resources + .shader + .border_line + .draw( + frame_context.surface.borrow_mut().deref_mut(), + &resources.buffer.map_vertex, + resources.buffer.get_pref_line_by_scale(scale).unwrap(), + &BorderLineUniform { + dimension: image_size, + offset, + zoom: scale, + line_width: theme.prefectural_border_width, + color: theme.prefectural_border_color, + }, + params, + ) + .unwrap(); + } } diff --git a/renderer/src/worker/drawer_overlay.rs b/renderer/src/worker/drawer_overlay.rs index 3b01976..44c4c48 100644 --- a/renderer/src/worker/drawer_overlay.rs +++ b/renderer/src/worker/drawer_overlay.rs @@ -23,8 +23,8 @@ pub fn draw( let draw_parameters = frame_context.draw_parameters; let theme = frame_context.theme; - let rights_position = calculate_rights_notation_position(frame_context.image_size); - let watermark_position = calculate_watermark_position(frame_context.image_size); + let rights_position = calculate_rights_notation_position(frame_context.camera.image_size); + let watermark_position = calculate_watermark_position(frame_context.camera.image_size); let vertices = [ TexturedVertex { @@ -76,7 +76,7 @@ pub fn draw( &TexturedUniform { texture_sampler: &resources.texture.overlay, }, - draw_parameters, + frame_context.premultiplied_draw_parameters, ) .unwrap(); @@ -96,7 +96,7 @@ pub fn draw( theme.occurrence_time_color, Scale::uniform(20.0), // TODO: calculate from dimension Offset::new(Origin::RightDown, Origin::RightDown, -20, -20), - frame_context.image_size.into(), + frame_context.camera.image_size.into(), resources, facade, frame_context.surface.borrow_mut().deref_mut(), diff --git a/renderer/src/worker/drawer_tsunami_legends.rs b/renderer/src/worker/drawer_tsunami_legends.rs index 4abb247..80fb67d 100644 --- a/renderer/src/worker/drawer_tsunami_legends.rs +++ b/renderer/src/worker/drawer_tsunami_legends.rs @@ -36,7 +36,8 @@ where .iter() .enumerate() .for_each(|(i, forecast_level)| { - let (shape, text_origin) = calculate_legend_position(frame_context.image_size, i); + let (shape, text_origin) = + calculate_legend_position(frame_context.camera.image_size, i); let shape = VertexBuffer::dynamic(facade, &shape).unwrap(); let color = match forecast_level { @@ -75,7 +76,7 @@ where text_origin.0, text_origin.1, ), - frame_context.image_size.into(), + frame_context.camera.image_size.into(), resources, facade, frame_context.surface.borrow_mut().deref_mut(), @@ -91,10 +92,10 @@ fn calculate_legend_position( index: usize, ) -> ([ShapeVertex; 4], (i32, i32)) { let image_size = image_size.to_f32(); - let text_origin = (-300, -240 - 26 * index as i32); + let text_origin = (-300, -76 - 26 * index as i32); let x_origin = 1.0 - 300.0 / (image_size.x() / 2.0); - let y_origin = -1.0 + (240.0 + 26.0 * index as f32) / (image_size.y() / 2.0); + let y_origin = -1.0 + (76.0 + 26.0 * index as f32) / (image_size.y() / 2.0); let shape_text_gap = 15.0 / (image_size.x() / 2.0); let shape_shape_gap = 10.4 / (image_size.y() / 2.0); let shape_width = 46.8 / (image_size.x() / 2.0); diff --git a/renderer/src/worker/drawer_tsunami_line.rs b/renderer/src/worker/drawer_tsunami_line.rs index 488f7d2..7911ed4 100644 --- a/renderer/src/worker/drawer_tsunami_line.rs +++ b/renderer/src/worker/drawer_tsunami_line.rs @@ -6,24 +6,18 @@ use glium::texture::{ }; use glium::Surface; use renderer_assets::QueryInterface; +use renderer_types::InsetRegion; use std::borrow::Cow; use std::ops::DerefMut; -pub fn draw( - frame_context: &FrameContext, +pub fn build_levels_texture( + facade: &F, tsunami_payload: &T, -) where +) -> UnsignedTexture1d +where T: crate::frame_context::HasTsunamiForecastLevels, { - let facade = frame_context.facade; - let resources = frame_context.resources; - let offset = frame_context.offset; - let scale = frame_context.scale; - let draw_parameters = frame_context.draw_parameters; - let theme = frame_context.theme; - let area_code_count = QueryInterface::tsunami_area_code_count(); - // println!("AreaCodeCount: {area_code_count}"); let mut levels = vec![0_u8; area_code_count]; @@ -38,19 +32,31 @@ pub fn draw( }) }); - // println!("{:?}", levels); let levels = RawImage1d { data: Cow::from(&levels), width: levels.len() as u32, format: ClientFormat::U8, }; - let levels = UnsignedTexture1d::with_format( + + UnsignedTexture1d::with_format( facade, levels, UncompressedUintFormat::U8, MipmapsOption::NoMipmap, ) - .unwrap(); + .unwrap() +} + +pub fn draw( + frame_context: &FrameContext, + region: InsetRegion, + levels: &UnsignedTexture1d, +) { + let resources = frame_context.resources; + let offset = frame_context.camera.offset; + let scale = frame_context.camera.scale; + let draw_parameters = frame_context.draw_parameters; + let theme = frame_context.theme; resources .shader @@ -58,9 +64,12 @@ pub fn draw( .draw( frame_context.surface.borrow_mut().deref_mut(), &frame_context.resources.buffer.tsunami_vertex, - &frame_context.resources.buffer.tsunami_indices, + frame_context + .resources + .buffer + .get_tsunami_indices_by_region(region), &TsunamiUniform { - dimension: frame_context.image_size.to_f32().into(), + dimension: frame_context.camera.image_size.to_f32().into(), offset: offset.into(), zoom: scale, colors: theme.tsunami_colors, diff --git a/renderer/src/worker/fonts.rs b/renderer/src/worker/fonts.rs index cb3c694..55a6e8c 100644 --- a/renderer/src/worker/fonts.rs +++ b/renderer/src/worker/fonts.rs @@ -234,6 +234,7 @@ pub enum Origin { RightUp, RightDown, Center, + CenterDown, } #[derive(Copy, Clone, Debug)] @@ -270,6 +271,7 @@ impl Offset { Origin::RightUp => (x + image_dimension.0, y), Origin::RightDown => (x + image_dimension.0, y + image_dimension.1), Origin::Center => (x + image_dimension.0 / 2.0, y + image_dimension.1 / 2.0), + Origin::CenterDown => (x + image_dimension.0 / 2.0, y + image_dimension.1), }; let text_size = (text_size.0, text_size.1); @@ -279,6 +281,7 @@ impl Offset { Origin::RightUp => (offset.0 - text_size.0, offset.1), Origin::RightDown => (offset.0 - text_size.0, offset.1 - text_size.1), Origin::Center => (offset.0 - text_size.0 / 2.0, offset.1 - text_size.1 / 2.0), + Origin::CenterDown => (offset.0 - text_size.0 / 2.0, offset.1 - text_size.1), }; point(offset.0, offset.1 + ascent) diff --git a/renderer/src/worker/headless.rs b/renderer/src/worker/headless.rs new file mode 100644 index 0000000..db9ab70 --- /dev/null +++ b/renderer/src/worker/headless.rs @@ -0,0 +1,123 @@ +use std::error::Error; +use std::ffi::CString; +use std::num::NonZeroU32; +use std::os::raw::c_void; +use std::rc::Rc; + +use glium::backend::{Backend, Context}; +use glium::glutin::api::egl::context::PossiblyCurrentContext; +use glium::glutin::api::egl::device::Device; +use glium::glutin::api::egl::display::Display; +use glium::glutin::api::egl::surface::Surface; +use glium::glutin::config::{ConfigSurfaceTypes, ConfigTemplateBuilder}; +use glium::glutin::context::{ + ContextApi, ContextAttributesBuilder, NotCurrentGlContext, PossiblyCurrentGlContext, Version, +}; +use glium::glutin::display::GlDisplay; +use glium::glutin::surface::{PbufferSurface, SurfaceAttributesBuilder}; +use glium::SwapBuffersError; + +struct HeadlessBackend { + /// surfaceless なコンテキストを作れた場合は `None`。 + surface: Option>, + context: PossiblyCurrentContext, + display: Display, +} + +unsafe impl Backend for HeadlessBackend { + fn swap_buffers(&self) -> Result<(), SwapBuffersError> { + Ok(()) + } + + unsafe fn get_proc_address(&self, symbol: &str) -> *const c_void { + let symbol = CString::new(symbol).unwrap(); + self.display.get_proc_address(&symbol) + } + + fn get_framebuffer_dimensions(&self) -> (u32, u32) { + (1, 1) + } + + fn resize(&self, _: (u32, u32)) {} + + fn is_current(&self) -> bool { + self.context.is_current() + } + + unsafe fn make_current(&self) { + match &self.surface { + Some(surface) => self.context.make_current(surface).unwrap(), + None => self.context.make_current_surfaceless().unwrap(), + } + } +} + +pub fn create(device_index: usize) -> Result, Box> { + let devices: Vec = Device::query_devices() + .map_err(|e| format!("Failed to enumerate EGL devices (is libEGL installed?): {e}"))? + .collect(); + + if devices.is_empty() { + return Err("No EGL device is available".into()); + } + + for (i, device) in devices.iter().enumerate() { + tracing::info!( + "EGL Device #{i}: {} ({})", + device.name().unwrap_or("[unknown]"), + device.vendor().unwrap_or("[unknown]"), + ); + } + + let device = devices.get(device_index).ok_or_else(|| { + format!( + "EGL device #{device_index} is not available ({} device(s) found)", + devices.len() + ) + })?; + + let display = unsafe { Display::with_device(device, None) } + .map_err(|e| format!("Failed to create an EGL display from the device: {e}"))?; + + let template = ConfigTemplateBuilder::new() + .with_surface_type(ConfigSurfaceTypes::PBUFFER) + .build(); + + let config = unsafe { display.find_configs(template)? } + .next() + .ok_or("No EGL config is available")?; + + let attributes = ContextAttributesBuilder::new() + .with_context_api(ContextApi::OpenGl(Some(Version::new(4, 5)))) + .build(None); + + let context = unsafe { display.create_context(&config, &attributes)? }; + + let (context, surface) = match context.make_current_surfaceless() { + Ok(context) => { + tracing::info!("EGL Surface: surfaceless"); + (context, None) + } + Err(e) => { + tracing::info!("EGL Surface: 1x1 pbuffer (surfaceless is unavailable: {e})"); + + let context = unsafe { display.create_context(&config, &attributes)? }; + + let attributes = SurfaceAttributesBuilder::::new() + .build(NonZeroU32::new(1).unwrap(), NonZeroU32::new(1).unwrap()); + + let surface = unsafe { display.create_pbuffer_surface(&config, &attributes)? }; + let context = context.make_current(&surface)?; + + (context, Some(surface)) + } + }; + + let backend = HeadlessBackend { + surface, + context, + display, + }; + + Ok(unsafe { Context::new(backend, true, Default::default()) }?) +} diff --git a/renderer/src/worker/inset.rs b/renderer/src/worker/inset.rs new file mode 100644 index 0000000..071565b --- /dev/null +++ b/renderer/src/worker/inset.rs @@ -0,0 +1,81 @@ +use super::camera::Camera; +use glium::Rect; +use renderer_types::{ + BoundingBox, GeoDegree, InsetRegion, Size, Vertex, OGASAWARA_VIEW_BBOX, OKINAWA_VIEW_BBOX, +}; + +pub struct BorderSides { + pub top: bool, + pub bottom: bool, + pub left: bool, + pub right: bool, +} + +impl BorderSides { + pub const ALL: Self = Self { + top: true, + bottom: true, + left: true, + right: true, + }; +} + +pub struct NotchLine { + pub from: Vertex, + pub to: Vertex, +} + +pub struct InsetPass { + pub region: InsetRegion, + pub view_bbox: BoundingBox, + pub viewport: Rect, + pub border_sides: BorderSides, + pub notch: Option, + pub label: &'static str, +} + +pub const OKINAWA_INSET: InsetPass = InsetPass { + region: InsetRegion::Okinawa, + view_bbox: OKINAWA_VIEW_BBOX, + viewport: Rect { + left: 0, + bottom: 321, + width: 447, + height: 447, + }, + border_sides: BorderSides { + top: false, + bottom: true, + left: false, + right: true, + }, + // 大東諸島の南東を通る斜め線 + notch: Some(NotchLine { + from: Vertex::new(132.2, 25.9), + to: Vertex::new(129.4, 23.9), + }), + label: "沖縄", +}; + +pub const OGASAWARA_INSET: InsetPass = InsetPass { + region: InsetRegion::Ogasawara, + view_bbox: OGASAWARA_VIEW_BBOX, + viewport: Rect { + left: 750, + bottom: 180, + width: 180, + height: 260, + }, + border_sides: BorderSides::ALL, + notch: None, + label: "小笠原", +}; + +pub const ALL_INSETS: [&InsetPass; 2] = [&OKINAWA_INSET, &OGASAWARA_INSET]; + +impl InsetPass { + pub fn camera(&self) -> Camera { + let image_size = Size::new(self.viewport.width, self.viewport.height); + Camera::fit(&self.view_bbox, image_size) + } +} diff --git a/renderer/src/worker/mod.rs b/renderer/src/worker/mod.rs index ee5083c..0d7d85c 100644 --- a/renderer/src/worker/mod.rs +++ b/renderer/src/worker/mod.rs @@ -1,18 +1,21 @@ -use crate::frame_context::FramePayload; +use crate::frame_context::{FramePayload, HasEpicenter, HasTime, HasTsunamiForecastLevels}; use crate::model::Message; +use crate::worker::camera::Camera; use crate::worker::fonts::FontManager; use crate::worker::theme::Theme; use glium::backend::Facade; use glium::glutin::surface::{GlSurface, SwapInterval}; use glium::{ - draw_parameters::{Blend, LinearBlendingFactor}, - framebuffer::SimpleFrameBuffer, + draw_parameters::{Blend, LinearBlendingFactor, Stencil, StencilOperation, StencilTest}, + framebuffer::{SimpleFrameBuffer, StencilRenderBuffer}, glutin::{ config::ConfigTemplateBuilder, context::{ContextAttributesBuilder, NotCurrentGlContext}, display::{GetGlDisplay, GlDisplay}, surface::{SurfaceAttributesBuilder, WindowSurface}, }, + texture::StencilFormat, + uniforms::MagnifySamplerFilter, BlendingFunction, Display, DrawParameters, Surface, Texture2d, }; use glutin_winit::DisplayBuilder; @@ -27,37 +30,61 @@ use tokio::sync::mpsc; use winit::application::ApplicationHandler; use winit::event::{StartCause, WindowEvent}; use winit::event_loop::ActiveEventLoop; -use winit::window::WindowId; +use winit::window::{Window, WindowId}; use winit::{raw_window_handle::HasWindowHandle, window::WindowAttributes}; +mod camera; mod drawer_epicenter; +mod drawer_inset_frame; mod drawer_intensity_icon; mod drawer_map; mod drawer_overlay; mod drawer_tsunami_legends; mod drawer_tsunami_line; mod fonts; +#[cfg(not(any(target_os = "macos", target_os = "ios")))] +mod headless; pub mod image_buffer; +mod inset; +mod notch; mod resources; mod shader; mod theme; mod vertex; const DIMENSION: (u32, u32) = (1024, 768); -const MAXIMUM_SCALE: f32 = 100.0; -const SCALE_FACTOR: f32 = 1.2; +const MAP_MARGIN_FACTOR: f32 = 1.2; const ICON_RATIO_IN_Y_AXIS: f32 = 0.05; -pub async fn run(mut rx: mpsc::Receiver) -> Result<(), Box> { +pub async fn run( + rx: mpsc::Receiver, + headless: bool, + egl_device_index: usize, +) -> Result<(), Box> { + if headless { + run_headless(rx, egl_device_index).await + } else { + run_windowed(rx).await + } +} + +async fn run_windowed(mut rx: mpsc::Receiver) -> Result<(), Box> { let event_loop = winit::event_loop::EventLoop::::with_user_event().build()?; let proxy = event_loop.create_proxy(); tokio::spawn(async move { - loop { - let message = rx.recv().await.unwrap(); + while let Some(message) = rx.recv().await { proxy.send_event(message).unwrap(); } + + // rx.recv() failing directly indicates that the Web thread has terminated abnormally, + // and the worker thread should keep waiting for the program’s error-handling routine, + // which is the correct behavior. + tokio::time::sleep(std::time::Duration::from_secs(1)).await; + + tracing::error!("An abnormal termination of the web thread was detected!"); + std::process::exit(1); }); event_loop.run_app(&mut App::default()).unwrap(); @@ -65,23 +92,97 @@ pub async fn run(mut rx: mpsc::Receiver) -> Result<(), Box> Ok(()) } -pub struct FrameContext<'a, 'b, F: ?Sized + Facade, S: ?Sized + Surface> { +async fn run_headless( + mut rx: mpsc::Receiver, + egl_device_index: usize, +) -> Result<(), Box> { + let context = create_headless_context(egl_device_index)?; + + log_gl_info(&context); + + let resources = resources::Resources::load(&context); + let mut font_manager = FontManager::new(&context); + let render_target = RenderTarget::new(&context); + + while let Some(message) = rx.recv().await { + render_frame( + &context, + &render_target, + &resources, + &mut font_manager, + message, + ); + } + + // rx.recv() failing directly indicates that the Web thread has terminated abnormally, + // and the worker thread should keep waiting for the program’s error-handling routine, + // which is the correct behavior. + tokio::time::sleep(std::time::Duration::from_secs(1)).await; + + tracing::error!("An abnormal termination of the web thread was detected!"); + std::process::exit(1); +} + +#[cfg(not(any(target_os = "macos", target_os = "ios")))] +fn create_headless_context( + egl_device_index: usize, +) -> Result, Box> { + headless::create(egl_device_index) +} + +#[cfg(any(target_os = "macos", target_os = "ios"))] +fn create_headless_context(_: usize) -> Result, Box> { + Err("Headless mode requires EGL, which is not available on this platform".into()) +} + +fn log_gl_info(facade: &F) { + let context = facade.get_context(); + + tracing::info!("GL_VENDOR: {}", context.get_opengl_vendor_string()); + tracing::info!("GL_RENDERER: {}", context.get_opengl_renderer_string()); + tracing::info!("GL_VERSION: {}", context.get_opengl_version_string()); +} + +const STRAIGHT_ALPHA_BLEND: Blend = Blend { + color: BlendingFunction::Addition { + source: LinearBlendingFactor::SourceAlpha, + destination: LinearBlendingFactor::OneMinusSourceAlpha, + }, + alpha: BlendingFunction::Max, + constant_value: (0.0, 0.0, 0.0, 0.0), +}; + +const PREMULTIPLIED_ALPHA_BLEND: Blend = Blend { + color: BlendingFunction::Addition { + source: LinearBlendingFactor::One, + destination: LinearBlendingFactor::OneMinusSourceAlpha, + }, + alpha: BlendingFunction::Addition { + source: LinearBlendingFactor::One, + destination: LinearBlendingFactor::OneMinusSourceAlpha, + }, + constant_value: (0.0, 0.0, 0.0, 0.0), +}; + +pub struct FrameContext<'a, 'b, 'c, F: ?Sized + Facade, S: ?Sized + Surface> { pub facade: &'a F, pub surface: Rc>, - pub image_size: Size, // TODO: Themeに移動する pub theme: &'a Theme, pub resources: &'a resources::Resources<'a>, pub font_manager: Rc>>, - pub draw_parameters: &'a DrawParameters<'a>, - pub scale: f32, - pub offset: Vertex, + pub draw_parameters: &'c DrawParameters<'c>, + + pub premultiplied_draw_parameters: &'c DrawParameters<'c>, + pub camera: Camera, } #[derive(Default)] struct App<'a> { display: Option>, + window: Option, resources: Option>, font_manager: Option>, + render_target: Option, } impl ApplicationHandler for App<'_> { @@ -90,20 +191,16 @@ impl ApplicationHandler for App<'_> { return; } - let display = create_gl_context(event_loop); - - let gl_vendor = display.get_opengl_vendor_string(); - let gl_renderer = display.get_opengl_renderer_string(); - let gl_version = display.get_opengl_version_string(); + let (display, window) = create_gl_context(event_loop); - tracing::info!("GL_VENDOR: {gl_vendor}"); - tracing::info!("GL_RENDERER: {gl_renderer}"); - tracing::info!("GL_VERSION: {gl_version}"); + log_gl_info(&display); let resources = resources::Resources::load(&display); let font_manager = FontManager::new(&display); + self.render_target = Some(RenderTarget::new(&display)); self.display = Some(display); + self.window = Some(window); self.resources = Some(resources); self.font_manager = Some(font_manager); } @@ -111,116 +208,172 @@ impl ApplicationHandler for App<'_> { fn resumed(&mut self, _: &ActiveEventLoop) {} fn user_event(&mut self, _: &ActiveEventLoop, event: Message) { - let Message::FrameRequest((request_frame_context, response_socket)) = event; - - let start_at = std::time::Instant::now(); - let display = self.display.as_ref().unwrap(); + let render_target = self.render_target.as_ref().unwrap(); let resources = self.resources.as_ref().unwrap(); let font_manager = self.font_manager.as_mut().unwrap(); - let font_manager = Rc::new(RefCell::new(font_manager)); + render_frame(display, render_target, resources, font_manager, event); + present(display, &render_target.texture); + } + + fn window_event(&mut self, event_loop: &ActiveEventLoop, _: WindowId, event: WindowEvent) { + match event { + WindowEvent::CloseRequested => event_loop.exit(), + WindowEvent::Resized(size) => { + if let Some(display) = self.display.as_ref() { + display.resize((size.width, size.height)); + } + } + WindowEvent::RedrawRequested => { + if let (Some(display), Some(render_target)) = + (self.display.as_ref(), self.render_target.as_ref()) + { + present(display, &render_target.texture); + } + } + _ => {} + } + } +} + +struct RenderTarget { + texture: Texture2d, + stencil_buffer: StencilRenderBuffer, +} + +impl RenderTarget { + fn new(facade: &F) -> Self { let image_size = Size::from(DIMENSION); - let bounding_box = calculate_bounding_box(&request_frame_context.payload); + let texture = Texture2d::empty(facade, image_size.x(), image_size.y()).unwrap(); + let stencil_buffer = + StencilRenderBuffer::new(facade, StencilFormat::I8, image_size.x(), image_size.y()) + .unwrap(); - let rendering_bbox = BoundingBox::from_vertices_float( - &bounding_box - .gl_vertices() - .iter() - .map(|v| v.to_mercator()) - .collect::>(), - ); - let offset = -rendering_bbox.center(); - let scale = calculate_map_scale(rendering_bbox, image_size); - - let draw_parameters = DrawParameters { - multisampling: false, - blend: Blend { - color: BlendingFunction::Addition { - source: LinearBlendingFactor::SourceAlpha, - destination: LinearBlendingFactor::OneMinusSourceAlpha, - }, - alpha: BlendingFunction::Max, - constant_value: (0.0, 0.0, 0.0, 0.0), - }, - ..Default::default() - }; + Self { + texture, + stencil_buffer, + } + } - let t_before_alloc = Instant::now(); + fn frame_buffer(&self, facade: &F) -> SimpleFrameBuffer<'_> { + SimpleFrameBuffer::with_stencil_buffer(facade, &self.texture, &self.stencil_buffer).unwrap() + } +} - let texture = Texture2d::empty(display, image_size.x(), image_size.y()).unwrap(); - let frame_buffer = SimpleFrameBuffer::new(display, &texture).unwrap(); - let frame_buffer = Rc::new(RefCell::new(frame_buffer)); +fn render_frame( + facade: &F, + target: &RenderTarget, + resources: &resources::Resources<'_>, + font_manager: &mut FontManager<'_>, + event: Message, +) { + let Message::FrameRequest((request_frame_context, response_socket)) = event; - let t_before_render = Instant::now(); + let start_at = std::time::Instant::now(); - let frame_context = FrameContext { - facade: display, - surface: frame_buffer.clone(), - image_size, - theme: &theme::DEFAULT, - resources, - font_manager, - draw_parameters: &draw_parameters, - scale, - offset, - }; + let font_manager = Rc::new(RefCell::new(font_manager)); + + let image_size = Size::from(DIMENSION); + + let bounding_box = calculate_bounding_box(&request_frame_context.payload); + + let camera = camera::Camera::fit(&bounding_box, image_size).with_margin(MAP_MARGIN_FACTOR); + + let draw_parameters = DrawParameters { + multisampling: false, + blend: STRAIGHT_ALPHA_BLEND, + ..Default::default() + }; + + let premultiplied_draw_parameters = DrawParameters { + blend: PREMULTIPLIED_ALPHA_BLEND, + ..draw_parameters.clone() + }; + + let t_before_alloc = Instant::now(); + + let frame_buffer = Rc::new(RefCell::new(target.frame_buffer(facade))); + + let t_before_render = Instant::now(); + + let frame_context = FrameContext { + facade, + surface: frame_buffer.clone(), + theme: &theme::DEFAULT, + resources, + font_manager, + draw_parameters: &draw_parameters, + premultiplied_draw_parameters: &premultiplied_draw_parameters, + camera, + }; - let clear_color = frame_context.theme.clear_color; - frame_buffer.borrow_mut().clear_color( + let clear_color = frame_context.theme.clear_color; + frame_buffer.borrow_mut().clear( + None, + Some(( clear_color[0], clear_color[1], clear_color[2], clear_color[3], - ); + )), + false, + None, + Some(0), + ); - match &request_frame_context.payload { - FramePayload::Earthquake(earthquake) => { - drawer_map::draw(&frame_context, true); - drawer_intensity_icon::draw_all(&frame_context, earthquake); - drawer_epicenter::draw(&frame_context, earthquake); - drawer_overlay::draw(&frame_context, earthquake); - } - FramePayload::TsunamiFirst(tsunami) => { - drawer_map::draw(&frame_context, false); - drawer_tsunami_line::draw(&frame_context, tsunami); - drawer_tsunami_legends::draw(&frame_context, tsunami); - drawer_epicenter::draw(&frame_context, tsunami); - drawer_overlay::draw(&frame_context, tsunami); - } - FramePayload::TsunamiSecond(tsunami) => { - drawer_map::draw(&frame_context, false); - drawer_tsunami_legends::draw(&frame_context, tsunami); - drawer_epicenter::draw(&frame_context, tsunami); - drawer_overlay::draw(&frame_context, tsunami); - } + let map_layers = request_frame_context.payload.map_layers(); + + match &request_frame_context.payload { + FramePayload::Earthquake(earthquake) => { + drawer_map::draw(&frame_context, map_layers, None); + drawer_intensity_icon::draw_all(&frame_context, earthquake); + drawer_epicenter::draw(&frame_context, earthquake); + drawer_overlay::draw(&frame_context, earthquake); + } + FramePayload::TsunamiFirst(tsunami) => { + draw_tsunami_frame(&frame_context, tsunami, map_layers, true); + } + FramePayload::TsunamiSecond(tsunami) => { + draw_tsunami_frame(&frame_context, tsunami, map_layers, false); } + } - let t_before_bufcpy = Instant::now(); + let t_before_bufcpy = Instant::now(); - let image: RGBAImageData = texture.read(); + let image: RGBAImageData = target.texture.read(); - let t_done = Instant::now(); + let t_done = Instant::now(); - tracing::info!( - "Init: {:?} Alloc: {:?} Render: {:?} BufCpy: {:?} ({})", - t_before_alloc - start_at, - t_before_render - t_before_alloc, - t_before_bufcpy - t_before_render, - t_done - t_before_bufcpy, - request_frame_context.request_identity, - ); + tracing::info!( + "Init: {:?} Alloc: {:?} Render: {:?} BufCpy: {:?} ({})", + t_before_alloc - start_at, + t_before_render - t_before_alloc, + t_before_bufcpy - t_before_render, + t_done - t_before_bufcpy, + request_frame_context.request_identity, + ); - let _ = response_socket.send(Ok(image)); - } + let _ = response_socket.send(Ok(image)); +} - fn window_event(&mut self, _: &ActiveEventLoop, _: WindowId, _: WindowEvent) {} +fn present(display: &Display, texture: &Texture2d) { + let frame = display.draw(); + texture + .as_surface() + .fill(&frame, MagnifySamplerFilter::Nearest); + if let Err(err) = frame.finish() { + tracing::warn!("Failed to present frame: {err:?}"); + } } -fn create_gl_context(event_loop: &ActiveEventLoop) -> Display { - let display_builder = - DisplayBuilder::new().with_window_attributes(Some(WindowAttributes::default())); +fn create_gl_context(event_loop: &ActiveEventLoop) -> (Display, Window) { + let window_attributes = WindowAttributes::default() + .with_title("eew-renderer") + .with_inner_size(winit::dpi::PhysicalSize::new(DIMENSION.0, DIMENSION.1)) + .with_resizable(false); + let display_builder = DisplayBuilder::new().with_window_attributes(Some(window_attributes)); let (window, gl_config) = display_builder .build(event_loop, ConfigTemplateBuilder::new(), |mut configs| { @@ -230,10 +383,11 @@ fn create_gl_context(event_loop: &ActiveEventLoop) -> Display { let window = window.unwrap(); + let size = window.inner_size(); let attributes = SurfaceAttributesBuilder::::new().build( window.window_handle().unwrap().as_raw(), - NonZeroU32::new(1).unwrap(), - NonZeroU32::new(1).unwrap(), + NonZeroU32::new(size.width).unwrap_or(NonZeroU32::new(1).unwrap()), + NonZeroU32::new(size.height).unwrap_or(NonZeroU32::new(1).unwrap()), ); let surface = unsafe { @@ -259,7 +413,10 @@ fn create_gl_context(event_loop: &ActiveEventLoop) -> Display { .set_swap_interval(¤t_context, SwapInterval::DontWait) .unwrap(); - Display::from_context_surface(current_context, surface).unwrap() + ( + Display::from_context_surface(current_context, surface).unwrap(), + window, + ) } /// マップの描画範囲を決定する。 @@ -300,14 +457,108 @@ pub fn calculate_bounding_box(payload: &FramePayload) -> BoundingBox bbox } FramePayload::TsunamiFirst(_) | FramePayload::TsunamiSecond(_) => { - BoundingBox::new(Vertex::new(122.9, 24.0), Vertex::new(148.9, 45.5)) + BoundingBox::new(Vertex::new(128.3, 30.0), Vertex::new(148.9, 45.5)) } } } -fn calculate_map_scale(bounding_box: BoundingBox, image_size: Size) -> f32 { - let x_scale = 1.0 / bounding_box.size().x(); - let y_scale = 1.0 / bounding_box.size().y() * image_size.aspect_ratio(); +fn draw_tsunami_frame( + frame_context: &FrameContext, + payload: &T, + map_layers: crate::frame_context::MapLayerConfig, + with_lines: bool, +) where + T: HasEpicenter + HasTime + HasTsunamiForecastLevels, +{ + let facade = frame_context.facade; + + let epicenter_buffer = drawer_epicenter::create_vertex_buffer(facade, payload.epicenter()); + let levels = with_lines.then(|| drawer_tsunami_line::build_levels_texture(facade, payload)); + + drawer_map::draw(frame_context, map_layers, Some(InsetRegion::Main)); + if let Some(levels) = &levels { + drawer_tsunami_line::draw(frame_context, InsetRegion::Main, levels); + } + drawer_tsunami_legends::draw(frame_context, payload); + if let Some(buffer) = &epicenter_buffer { + drawer_epicenter::draw_vertex_buffer(frame_context, buffer, ICON_RATIO_IN_Y_AXIS); + } + draw_insets( + frame_context, + epicenter_buffer.as_ref(), + map_layers, + levels.as_ref(), + ); + drawer_overlay::draw(frame_context, payload); +} + +fn draw_insets( + base: &FrameContext, + epicenter_buffer: Option<&glium::VertexBuffer>, + layers: crate::frame_context::MapLayerConfig, + levels: Option<&glium::texture::UnsignedTexture1d>, +) { + for inset in inset::ALL_INSETS { + let camera = inset.camera(); + let notch = inset.notch.as_ref().map(|n| notch::resolve(&camera, n)); + + let mut draw_parameters = base.draw_parameters.clone(); + draw_parameters.viewport = Some(inset.viewport); + if notch.is_some() { + draw_parameters.stencil = + stencil_params(StencilTest::IfEqual { mask: 0xff }, StencilOperation::Keep); + } + + let premultiplied_draw_parameters = DrawParameters { + blend: PREMULTIPLIED_ALPHA_BLEND, + ..draw_parameters.clone() + }; - f32::min(f32::min(x_scale, y_scale) * 2.0, MAXIMUM_SCALE) / SCALE_FACTOR + let frame_context = FrameContext { + facade: base.facade, + surface: base.surface.clone(), + theme: base.theme, + resources: base.resources, + font_manager: base.font_manager.clone(), + draw_parameters: &draw_parameters, + premultiplied_draw_parameters: &premultiplied_draw_parameters, + camera, + }; + + if let Some(notch) = ¬ch { + drawer_inset_frame::draw_stencil_mask(&frame_context, notch); + } + drawer_inset_frame::draw_background(&frame_context); + drawer_map::draw(&frame_context, layers, Some(inset.region)); + if let Some(levels) = levels { + drawer_tsunami_line::draw(&frame_context, inset.region, levels); + } + if let Some(buffer) = epicenter_buffer { + let icon_ratio = ICON_RATIO_IN_Y_AXIS + * (base.camera.image_size.y() as f32 / inset.viewport.height as f32); + drawer_epicenter::draw_vertex_buffer(&frame_context, buffer, icon_ratio); + } + drawer_inset_frame::draw_border_and_label( + &frame_context, + &inset.border_sides, + notch.as_ref(), + inset.label, + ); + } +} + +fn stencil_params(test: StencilTest, operation: StencilOperation) -> Stencil { + Stencil { + test_clockwise: test, + reference_value_clockwise: 1, + fail_operation_clockwise: operation, + pass_depth_fail_operation_clockwise: operation, + depth_pass_operation_clockwise: operation, + test_counter_clockwise: test, + reference_value_counter_clockwise: 1, + fail_operation_counter_clockwise: operation, + pass_depth_fail_operation_counter_clockwise: operation, + depth_pass_operation_counter_clockwise: operation, + ..Default::default() + } } diff --git a/renderer/src/worker/notch.rs b/renderer/src/worker/notch.rs new file mode 100644 index 0000000..43a0c8d --- /dev/null +++ b/renderer/src/worker/notch.rs @@ -0,0 +1,109 @@ +use super::camera::Camera; +use super::inset::NotchLine; +use renderer_types::{GeoDegree, Vertex}; + +pub struct ResolvedNotch { + pub a: [f32; 2], + pub b: [f32; 2], + pub kept_polygon: KeptPolygon, +} + +impl ResolvedNotch { + pub fn side(&self, p: [f32; 2]) -> f32 { + (self.b[0] - self.a[0]) * (p[1] - self.a[1]) - (self.b[1] - self.a[1]) * (p[0] - self.a[0]) + } + + pub fn keep_sign(&self) -> f32 { + self.side([0.0, 0.0]).signum() + } +} + +pub struct KeptPolygon { + points: [[f32; 2]; 5], + len: usize, +} + +impl KeptPolygon { + pub fn as_slice(&self) -> &[[f32; 2]] { + &self.points[..self.len] + } + + fn push(&mut self, p: [f32; 2]) { + self.points[self.len] = p; + self.len += 1; + } +} + +fn to_ndc(camera: &Camera, geo: Vertex) -> [f32; 2] { + geo.to_mercator() + .to_screen(camera.offset, camera.scale) + .into() +} + +fn clip_line_to_square(origin: [f32; 2], dir: [f32; 2]) -> ([f32; 2], [f32; 2]) { + let mut t_enter = f32::NEG_INFINITY; + let mut t_exit = f32::INFINITY; + for axis in 0..2 { + let (o, d) = (origin[axis], dir[axis]); + if d.abs() <= f32::EPSILON { + continue; + } + let t0 = (-1.0 - o) / d; + let t1 = (1.0 - o) / d; + t_enter = t_enter.max(t0.min(t1)); + t_exit = t_exit.min(t0.max(t1)); + } + debug_assert!(t_enter.is_finite() && t_exit.is_finite() && t_enter < t_exit); + ( + [origin[0] + t_enter * dir[0], origin[1] + t_enter * dir[1]], + [origin[0] + t_exit * dir[0], origin[1] + t_exit * dir[1]], + ) +} + +pub fn resolve(camera: &Camera, notch: &NotchLine) -> ResolvedNotch { + let a = to_ndc(camera, notch.from); + let b = to_ndc(camera, notch.to); + let dir = [b[0] - a[0], b[1] - a[1]]; + let (a, b) = clip_line_to_square(a, dir); + let mut resolved = ResolvedNotch { + a, + b, + kept_polygon: KeptPolygon { + points: [[0.0, 0.0]; 5], + len: 0, + }, + }; + resolved.kept_polygon = kept_polygon(&resolved); + resolved +} + +fn kept_polygon(notch: &ResolvedNotch) -> KeptPolygon { + let square = [[-1.0, -1.0], [1.0, -1.0], [1.0, 1.0], [-1.0, 1.0]]; + + let keep_sign = notch.keep_sign(); + + let mut result = KeptPolygon { + points: [[0.0, 0.0]; 5], + len: 0, + }; + for i in 0..square.len() { + let current = square[i]; + let next = square[(i + 1) % square.len()]; + let current_inside = notch.side(current) * keep_sign >= 0.0; + let next_inside = notch.side(next) * keep_sign >= 0.0; + + if current_inside { + result.push(current); + } + if current_inside != next_inside { + let d_current = notch.side(current); + let d_next = notch.side(next); + let t = d_current / (d_current - d_next); + result.push([ + current[0] + t * (next[0] - current[0]), + current[1] + t * (next[1] - current[1]), + ]); + } + } + result +} diff --git a/renderer/src/worker/resources.rs b/renderer/src/worker/resources.rs index 2ea1c08..24b997b 100644 --- a/renderer/src/worker/resources.rs +++ b/renderer/src/worker/resources.rs @@ -8,14 +8,18 @@ use crate::worker::shader::ShaderProgram; use glium::backend::Facade; use glium::index::PrimitiveType; use glium::texture::{MipmapsOption, RawImage2d, UncompressedFloatFormat}; +use glium::uniforms::{AsUniformValue, UniformValue}; use glium::{IndexBuffer, Texture2d, VertexBuffer}; +use renderer_types::InsetRegion; #[derive(Debug)] pub struct Resources<'a> { pub shader: Shader<'a>, pub buffer: Buffer, pub lake: Lake, + pub world: World, pub texture: Texture, + pub inset: Inset, } impl Resources<'_> { @@ -23,13 +27,17 @@ impl Resources<'_> { let shader = Shader::load(facade); let buffer = Buffer::load(facade); let lake = Lake::load(facade); + let world = World::load(facade); let texture = Texture::load(facade); + let inset = Inset::load(facade); Self { shader, buffer, lake, + world, texture, + inset, } } } @@ -39,9 +47,10 @@ pub struct Buffer { pub map_vertex: VertexBuffer, area_line: Vec>, pref_line: Vec>, - pub map: IndexBuffer, + map: IndexBuffer, + map_ranges: [std::ops::Range; InsetRegion::COUNT], pub tsunami_vertex: VertexBuffer, - pub tsunami_indices: IndexBuffer, + tsunami_indices: [IndexBuffer; InsetRegion::COUNT], } impl Buffer { @@ -59,8 +68,18 @@ impl Buffer { let vertex = VertexBuffer::new(facade, &vertices).unwrap(); - let map = - IndexBuffer::new(facade, PrimitiveType::TrianglesList, geom.map_triangles).unwrap(); + let mut cursor = 0; + let map_ranges = std::array::from_fn(|i| { + let start = cursor; + cursor += geom.map_triangles[i].len(); + start..cursor + }); + let map = IndexBuffer::new( + facade, + PrimitiveType::TrianglesList, + &geom.map_triangles.concat(), + ) + .unwrap(); let area_line: Vec<_> = geom .area_lines @@ -83,12 +102,14 @@ impl Buffer { }) .collect::>(); let tsunami_vertex = VertexBuffer::immutable(facade, &tsunami_vertex).unwrap(); - let tsunami_indices = - IndexBuffer::immutable(facade, PrimitiveType::LineStrip, tsunami_geom.indices).unwrap(); + let tsunami_indices = tsunami_geom + .indices + .map(|i| IndexBuffer::immutable(facade, PrimitiveType::LineStrip, i).unwrap()); Buffer { map_vertex: vertex, map, + map_ranges, area_line, pref_line, tsunami_vertex, @@ -96,6 +117,21 @@ impl Buffer { } } + pub fn get_map_slice( + &self, + region: Option, + ) -> glium::index::IndexBufferSlice<'_, u32> { + let range = match region { + Some(region) => self.map_ranges[region.index()].clone(), + None => 0..self.map.len(), + }; + self.map.slice(range).unwrap() + } + + pub fn get_tsunami_indices_by_region(&self, region: InsetRegion) -> &IndexBuffer { + &self.tsunami_indices[region.index()] + } + pub fn get_area_line_by_scale(&self, scale: f32) -> Option<&IndexBuffer> { let i = renderer_assets::QueryInterface::query_lod_level_by_scale(scale)?; self.area_line.get(i) @@ -133,6 +169,32 @@ impl Lake { } } +#[derive(Debug)] +pub struct World { + pub vertex: VertexBuffer, + pub index: IndexBuffer, +} + +impl World { + fn load(facade: &F) -> Self { + let geom = renderer_assets::QueryInterface::world_geometries(); + + let vertex: Vec<_> = geom + .vertices + .iter() + .map(|v| MapVertex { + position: [v.0, v.1], + }) + .collect(); + let vertex = VertexBuffer::immutable(facade, &vertex).unwrap(); + + let index = + IndexBuffer::immutable(facade, PrimitiveType::TrianglesList, geom.indices).unwrap(); + + World { vertex, index } + } +} + #[derive(Debug)] pub struct Shader<'a> { pub border_line: ShaderProgram, @@ -140,7 +202,7 @@ pub struct Shader<'a> { pub intensity_icon: ShaderProgram, IntensityIconVertex>, pub map: ShaderProgram, pub shape: ShaderProgram, - pub tsunami: ShaderProgram, + pub tsunami: ShaderProgram, TsunamiVertex>, pub text: ShaderProgram, TextVertex>, pub textured: ShaderProgram, TexturedVertex>, } @@ -224,31 +286,60 @@ impl Shader<'_> { } } +/// ロード時に RGB×A をpremultiply済みの Texture2d #[derive(Debug)] -pub struct Texture { - pub intensity: Texture2d, - pub epicenter: Texture2d, - pub overlay: Texture2d, +pub(crate) struct PremultipliedTexture2d(Texture2d); + +#[inline(always)] +fn mul_alpha(c: u8, a: u16) -> u8 { + let t = c as u16 * a + 128; + ((t + (t >> 8)) >> 8) as u8 } -impl Texture { - fn load(facade: &F) -> Self { - use image::ImageFormat; +impl PremultipliedTexture2d { + fn load_png(facade: &F, buf: &[u8]) -> Self { + let image = image::load_from_memory_with_format(buf, image::ImageFormat::Png).unwrap(); + let mut image = image.into_rgba8(); + + for pixel in image.pixels_mut() { + let a = pixel[3] as u16; - let load_png = |buf: &[u8]| -> Texture2d { - let image = image::load_from_memory_with_format(buf, ImageFormat::Png).unwrap(); - let image = image.as_rgba8().unwrap(); - let dimension = image.dimensions(); - let image = RawImage2d::from_raw_rgba_reversed(image.as_raw(), dimension); + pixel[0] = mul_alpha(pixel[0], a); + pixel[1] = mul_alpha(pixel[1], a); + pixel[2] = mul_alpha(pixel[2], a); + } + let dimension = image.dimensions(); + let image = RawImage2d::from_raw_rgba_reversed(image.as_raw(), dimension); + + Self( Texture2d::with_format( facade, image, UncompressedFloatFormat::U8U8U8U8, MipmapsOption::NoMipmap, ) - .unwrap() - }; + .unwrap(), + ) + } +} + +impl AsUniformValue for &PremultipliedTexture2d { + fn as_uniform_value(&self) -> UniformValue<'_> { + self.0.as_uniform_value() + } +} + +#[derive(Debug)] +pub struct Texture { + pub intensity: PremultipliedTexture2d, + pub epicenter: PremultipliedTexture2d, + pub overlay: PremultipliedTexture2d, +} + +impl Texture { + fn load(facade: &F) -> Self { + let load_png = |buf: &[u8]| PremultipliedTexture2d::load_png(facade, buf); let intensity = load_png(include_bytes!("../../../assets/image/intensity.png")); let epicenter = load_png(include_bytes!("../../../assets/image/epicenter.png")); @@ -261,3 +352,43 @@ impl Texture { } } } + +#[derive(Debug)] +pub struct Inset { + pub border_vertex_buffer: VertexBuffer, + pub background_vertex_buffer: VertexBuffer, + pub notch_mask_vertex_buffer: VertexBuffer, +} + +impl Inset { + fn load(facade: &F) -> Self { + let border_vertex_buffer = VertexBuffer::empty_dynamic(facade, 30).unwrap(); + + let notch_mask_vertex_buffer = VertexBuffer::empty_dynamic(facade, 5).unwrap(); + + let background_vertex_buffer = VertexBuffer::immutable( + facade, + &[ + ShapeVertex { + position: [-1.0, -1.0], + }, + ShapeVertex { + position: [1.0, -1.0], + }, + ShapeVertex { + position: [-1.0, 1.0], + }, + ShapeVertex { + position: [1.0, 1.0], + }, + ], + ) + .unwrap(); + + Self { + border_vertex_buffer, + background_vertex_buffer, + notch_mask_vertex_buffer, + } + } +} diff --git a/renderer/src/worker/shader.rs b/renderer/src/worker/shader.rs index cfa6e93..41d9386 100644 --- a/renderer/src/worker/shader.rs +++ b/renderer/src/worker/shader.rs @@ -1,10 +1,16 @@ use glium::backend::Facade; use glium::index::IndicesSource; use glium::uniforms::Uniforms; +use glium::vertex::{MultiVerticesSource, VertexBufferSlice}; use glium::{DrawError, DrawParameters, Program, Surface, Vertex, VertexBuffer}; use std::fmt::{Debug, Display, Formatter}; use std::marker::PhantomData; +pub trait VertexSource<'a, V: Vertex>: MultiVerticesSource<'a> {} + +impl<'a, V: Vertex> VertexSource<'a, V> for &'a VertexBuffer {} +impl<'a, V: Vertex> VertexSource<'a, V> for VertexBufferSlice<'a, V> {} + #[derive(Debug)] pub struct ShaderProgram { program: Program, @@ -32,10 +38,10 @@ impl ShaderProgram { Self::from_program(program) } - pub fn draw<'a, S: ?Sized + Surface, I: Into>>( + pub fn draw<'a, 'b, S: ?Sized + Surface, I: Into>, B: VertexSource<'b, V>>( &self, surface: &mut S, - vertex_buffer: &VertexBuffer, + vertex_buffer: B, index_buffer: I, uniform: &U, draw_parameters: &DrawParameters, diff --git a/renderer/src/worker/theme.rs b/renderer/src/worker/theme.rs index deac5a2..d53df33 100644 --- a/renderer/src/worker/theme.rs +++ b/renderer/src/worker/theme.rs @@ -1,5 +1,6 @@ #![allow(clippy::eq_op)] use crate::worker::vertex::TsunamiLineColors; +use renderer_macros::{rgb, rgba}; pub struct Theme { pub clear_color: [f32; 4], @@ -12,42 +13,35 @@ pub struct Theme { pub tsunami_width: f32, pub tsunami_legend_color: [f32; 4], pub occurrence_time_color: [f32; 4], + pub inset_background_color: [f32; 4], + pub inset_border_color: [f32; 4], + pub inset_border_width: f32, + pub inset_label_color: [f32; 4], + pub inset_label_font_size: f32, + pub inset_label_offset_y: i32, } pub const DEFAULT: Theme = Theme { #[allow(clippy::eq_op)] - clear_color: [130.0 / 255.0, 188.0 / 255.0, 255.0 / 255.0, 1.0], - ground_color: [222.0 / 255.0, 226.0 / 255.0, 229.0 / 255.0], - prefectural_border_color: [148.0 / 255.0, 151.0 / 255.0, 153.0 / 255.0], + clear_color: rgba!("#82BCFF", 1.0), + ground_color: rgb!("#DEE2E5"), + prefectural_border_color: rgb!("#949799"), prefectural_border_width: 2.0, - area_border_color: [148.0 / 255.0, 151.0 / 255.0, 153.0 / 255.0], + area_border_color: rgb!("#949799"), area_border_width: 1.0, tsunami_colors: TsunamiLineColors { - forecast: [0.0 / 255.0, 191.0 / 255.0, 255.0 / 255.0], - advisory: [250.0 / 255.0, 245.0 / 255.0, 0.0 / 255.0], - warning: [255.0 / 255.0, 40.0 / 255.0, 0.0 / 255.0], - major_warning: [200.0 / 255.0, 0.0 / 255.0, 255.0 / 255.0], + forecast: rgb!("#087FFF"), + advisory: rgb!("#FAF500"), + warning: rgb!("#FF2800"), + major_warning: rgb!("#C800FF"), }, tsunami_width: 3.0, - tsunami_legend_color: [0.0, 0.0, 0.0, 0.8], - occurrence_time_color: [0.0, 0.0, 0.0, 0.63], -}; - -#[allow(dead_code)] -pub const DARK_DEMO: Theme = Theme { - clear_color: [0.1, 0.12, 0.15, 1.0], - ground_color: [0.35, 0.35, 0.35], - prefectural_border_color: [0.75, 0.75, 0.75], - prefectural_border_width: 5.0, - area_border_color: [0.6, 0.6, 0.6], - area_border_width: 2.0, - tsunami_colors: TsunamiLineColors { - forecast: [0.0 / 255.0, 191.0 / 255.0, 255.0 / 255.0], - advisory: [250.0 / 255.0, 245.0 / 255.0, 0.0 / 255.0], - warning: [255.0 / 255.0, 40.0 / 255.0, 0.0 / 255.0], - major_warning: [200.0 / 255.0, 0.0 / 255.0, 255.0 / 255.0], - }, - tsunami_width: 8.0, - tsunami_legend_color: [0.0, 0.0, 0.0, 0.8], - occurrence_time_color: [1.0, 1.0, 1.0, 0.63], + tsunami_legend_color: rgba!("#000000", 0.8), + occurrence_time_color: rgba!("#000000", 0.63), + inset_background_color: rgba!("#82BCFF", 1.0), + inset_border_color: rgba!("#969BA0", 1.0), + inset_border_width: 3.0, + inset_label_color: rgba!("#000000", 0.8), + inset_label_font_size: 22.0, + inset_label_offset_y: -6, }; diff --git a/renderer/src/worker/vertex.rs b/renderer/src/worker/vertex.rs index ba819e9..d8c453a 100644 --- a/renderer/src/worker/vertex.rs +++ b/renderer/src/worker/vertex.rs @@ -1,3 +1,4 @@ +use super::resources::PremultipliedTexture2d; use glium::texture::UnsignedTexture1d; use glium::uniforms::{ AsUniformValue, ImageUnitAccess, ImageUnitBehavior, ImageUnitFormat, Sampler, UniformValue, @@ -36,7 +37,7 @@ pub struct EpicenterUniform<'a> { pub offset: [f32; 2], pub zoom: f32, pub icon_ratio_in_y_axis: f32, - pub texture_sampler: &'a Texture2d, + pub texture_sampler: &'a PremultipliedTexture2d, } impl Uniforms for EpicenterUniform<'_> { @@ -65,7 +66,7 @@ pub struct IntensityIconUniform<'a> { pub offset: [f32; 2], pub zoom: f32, pub icon_ratio_in_y_axis: f32, - pub texture_sampler: &'a Texture2d, + pub texture_sampler: &'a PremultipliedTexture2d, } impl Uniforms for IntensityIconUniform<'_> { @@ -129,16 +130,16 @@ pub struct TsunamiVertex { implement_vertex!(TsunamiVertex, position, code); #[derive(Debug)] -pub struct TsunamiUniform { +pub struct TsunamiUniform<'a> { pub dimension: [f32; 2], pub offset: [f32; 2], pub zoom: f32, pub colors: TsunamiLineColors, - pub levels: UnsignedTexture1d, + pub levels: &'a UnsignedTexture1d, pub line_width: f32, } -impl Uniforms for TsunamiUniform { +impl Uniforms for TsunamiUniform<'_> { fn visit_values<'a, Fn: FnMut(&str, UniformValue<'a>)>(&'a self, mut visitor: Fn) { visitor("dimension", self.dimension.as_uniform_value()); visitor("offset", self.offset.as_uniform_value()); @@ -156,7 +157,7 @@ impl Uniforms for TsunamiUniform { behavior.format = ImageUnitFormat::R8UI; visitor( "levels", - UniformValue::UnsignedImage1d(&self.levels, Some(behavior)), + UniformValue::UnsignedImage1d(self.levels, Some(behavior)), ); visitor("line_width", self.line_width.as_uniform_value()); // meow @@ -207,7 +208,7 @@ implement_vertex!(TexturedVertex, position, uv); #[derive(Debug)] pub struct TexturedUniform<'a> { - pub texture_sampler: &'a Texture2d, + pub texture_sampler: &'a PremultipliedTexture2d, } impl Uniforms for TexturedUniform<'_> { diff --git a/renovate.json b/renovate.json index e0c2212..b4e446b 100644 --- a/renovate.json +++ b/renovate.json @@ -6,7 +6,7 @@ ], "regexManagers": [ { - "fileMatch": [".*\\.yml", ".*\\.sh"], + "fileMatch": [".*\\.yml"], "matchStrings": ["depName=(?.*?)?\\s.*?_VERSION=\"(?.*?)\""], "versioningTemplate": "docker", "datasourceTemplate": "github-releases" diff --git a/rust-toolchain b/rust-toolchain index 7243604..6422209 100644 --- a/rust-toolchain +++ b/rust-toolchain @@ -1,2 +1,2 @@ [toolchain] -channel = "1.97.1" +channel = "1.98.1"