Compare commits
60 Commits
Author | SHA1 | Date |
---|---|---|
Paul van Tilburg | 88c59cdb1f | |
Paul van Tilburg | cad766b520 | |
Paul van Tilburg | e62699c102 | |
Paul van Tilburg | f32f67dbf4 | |
Paul van Tilburg | d1e43a7aa7 | |
Paul van Tilburg | c2450267e0 | |
Paul van Tilburg | 087ecf00f1 | |
Paul van Tilburg | f8ea25c516 | |
Paul van Tilburg | f830d34464 | |
Paul van Tilburg | ff10cc19e8 | |
Paul van Tilburg | 1211fea46a | |
Paul van Tilburg | 182521aab7 | |
Paul van Tilburg | dadf5d3147 | |
Paul van Tilburg | 4b506541f3 | |
Paul van Tilburg | 47e28a7098 | |
Paul van Tilburg | 07e0701106 | |
Paul van Tilburg | 91d5500c86 | |
Paul van Tilburg | 9b3c11ee76 | |
Paul van Tilburg | 27e1ac726c | |
Paul van Tilburg | 3047cf74c2 | |
Paul van Tilburg | 44474aa545 | |
Paul van Tilburg | 50b0e94839 | |
Paul van Tilburg | 1010311403 | |
Paul van Tilburg | d16699636b | |
Paul van Tilburg | 38fb28c248 | |
Paul van Tilburg | 7c2b012e95 | |
Paul van Tilburg | ab6001f072 | |
Paul van Tilburg | 9742331f6d | |
Paul van Tilburg | 9bb9d248a8 | |
Paul van Tilburg | 37788fac1c | |
Paul van Tilburg | 112875e7ac | |
Paul van Tilburg | 1c71ca79ef | |
Paul van Tilburg | afca20c96f | |
Paul van Tilburg | 2d34eee49a | |
Paul van Tilburg | a52313ffb7 | |
Admar Schoonen | f39a3a33ee | |
Paul van Tilburg | a59b4eefe1 | |
Paul van Tilburg | 1aad3e2eb6 | |
Paul van Tilburg | 929508a9cc | |
Paul van Tilburg | 23e4f731a0 | |
Paul van Tilburg | d84440304a | |
Paul van Tilburg | a289bd9ef0 | |
Paul van Tilburg | 122f98a92d | |
Paul van Tilburg | 1426405943 | |
Paul van Tilburg | 34be96d187 | |
Paul van Tilburg | bc140a9d1e | |
Paul van Tilburg | 39c224eb90 | |
Paul van Tilburg | b517448fd7 | |
Paul van Tilburg | 3de66dbd41 | |
Paul van Tilburg | 6a04fc958f | |
Paul van Tilburg | c8b951ab7e | |
Paul van Tilburg | 32ec6b516c | |
Paul van Tilburg | a6301fa678 | |
Paul van Tilburg | f00537d5f3 | |
Paul van Tilburg | c8970fa3bb | |
Paul van Tilburg | aee3409f4a | |
Paul van Tilburg | dbdd7bef0f | |
Paul van Tilburg | d749233b24 | |
Paul van Tilburg | abb6657212 | |
Paul van Tilburg | 8b03f2162b |
|
@ -0,0 +1,45 @@
|
|||
name: "Check, lint and test using Cargo"
|
||||
|
||||
on:
|
||||
- pull_request
|
||||
- push
|
||||
- workflow_dispatch
|
||||
|
||||
jobs:
|
||||
check_lint:
|
||||
name: Check, lint and test
|
||||
runs-on: debian-latest
|
||||
steps:
|
||||
- name: Checkout sources
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Install Rust stable toolchain
|
||||
uses: https://github.com/actions-rs/toolchain@v1
|
||||
with:
|
||||
profile: minimal
|
||||
toolchain: stable
|
||||
override: true
|
||||
components: rustfmt, clippy
|
||||
|
||||
- name: Run cargo check
|
||||
uses: https://github.com/actions-rs/cargo@v1
|
||||
with:
|
||||
command: check
|
||||
|
||||
- name: Run cargo clippy
|
||||
uses: https://github.com/actions-rs/cargo@v1
|
||||
with:
|
||||
command: clippy
|
||||
args: -- -D warnings
|
||||
|
||||
- name: Run cargo fmt
|
||||
uses: https://github.com/actions-rs/cargo@v1
|
||||
with:
|
||||
command: fmt
|
||||
args: --all -- --check
|
||||
|
||||
- name: Run cargo test
|
||||
uses: https://github.com/actions-rs/cargo@v1
|
||||
with:
|
||||
command: test
|
||||
args: --all-features
|
|
@ -0,0 +1,112 @@
|
|||
name: "Release"
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- "v*"
|
||||
|
||||
jobs:
|
||||
release:
|
||||
name: "Release"
|
||||
runs-on: debian-latest
|
||||
steps:
|
||||
- name: Checkout sources
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Determine the version of the release
|
||||
run: |
|
||||
VERSION=${GITHUB_REF_NAME#v}
|
||||
echo "Releasing version: $VERSION"
|
||||
echo "VERSION=$VERSION" >> $GITHUB_ENV
|
||||
|
||||
- name: Get the release notes from the changelog
|
||||
run: |
|
||||
EOF=$(dd if=/dev/urandom bs=15 count=1 status=none | base64)
|
||||
RELEASE_NOTES=$(sed -n -e "/^## \[$VERSION\]/,/^## \[/{//"'!'"p;}" CHANGELOG.md | sed -e '1d;$d')
|
||||
echo "Release notes:"
|
||||
echo
|
||||
echo "$RELEASE_NOTES"
|
||||
echo "RELEASE_NOTES<<$EOF" >> "$GITHUB_ENV"
|
||||
echo "$RELEASE_NOTES" >> "$GITHUB_ENV"
|
||||
echo "$EOF" >> "$GITHUB_ENV"
|
||||
|
||||
- name: Install Go
|
||||
uses: actions/setup-go@v4
|
||||
with:
|
||||
go-version: '>=1.20.1'
|
||||
|
||||
- name: Release to Gitea
|
||||
uses: actions/release-action@main
|
||||
with:
|
||||
# This is available by default.
|
||||
api_key: '${{ secrets.RELEASE_TOKEN }}'
|
||||
files: FIXME
|
||||
title: 'Release ${{ env.VERSION }}'
|
||||
body: '${{ env.RELEASE_NOTES }}'
|
||||
|
||||
release-crate:
|
||||
name: "Release Rust crate"
|
||||
runs-on: debian-latest
|
||||
steps:
|
||||
- name: Checkout sources
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Install Rust stable toolchain
|
||||
uses: https://github.com/actions-rs/toolchain@v1
|
||||
with:
|
||||
profile: minimal
|
||||
toolchain: stable
|
||||
override: true
|
||||
|
||||
- name: Use sparse Cargo index for crates.io
|
||||
run: echo -e '[registries.crates-io]\nprotocol = "sparse"' >> /root/.cargo/config.toml
|
||||
|
||||
- name: Register the Gitea crate registry with Cargo
|
||||
run: echo -e '[registries.luon]\nindex = "https://git.luon.net/paul/_cargo-index.git"' >> /root/.cargo/config.toml
|
||||
|
||||
- name: Run cargo publish
|
||||
uses: https://github.com/actions-rs/cargo@v1
|
||||
env:
|
||||
# This needs to be provided for the repository; no login necessary as a result.
|
||||
CARGO_REGISTRIES_LUON_TOKEN: '${{ secrets.CARGO_TOKEN }}'
|
||||
with:
|
||||
command: publish
|
||||
args: --registry luon
|
||||
|
||||
release-deb:
|
||||
name: "Release Debian package"
|
||||
runs-on: debian-latest
|
||||
steps:
|
||||
- name: Checkout sources
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Install Rust stable toolchain
|
||||
uses: https://github.com/actions-rs/toolchain@v1
|
||||
with:
|
||||
profile: minimal
|
||||
toolchain: stable
|
||||
override: true
|
||||
|
||||
- name: Install cargo-deb
|
||||
uses: https://github.com/brndnmtthws/rust-action-cargo-binstall@v1
|
||||
with:
|
||||
packages: cargo-deb
|
||||
|
||||
- name: Run cargo-deb
|
||||
uses: https://github.com/actions-rs/cargo@v1
|
||||
with:
|
||||
command: deb
|
||||
|
||||
- name: Publish Debian package
|
||||
env:
|
||||
DEB_REPO_TOKEN: '${{ secrets.DEB_REPO_TOKEN }}'
|
||||
run: |
|
||||
curl --config <(printf "user=%s:%s" paul "${DEB_REPO_TOKEN}") \
|
||||
--upload-file target/debian/sinoptik*.deb \
|
||||
https://git.luon.net/api/packages/paul/debian/pool/bookworm/main/upload
|
112
CHANGELOG.md
112
CHANGELOG.md
|
@ -7,6 +7,109 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
|
||||
## [Unreleased]
|
||||
|
||||
## [0.2.11] - 2024-02-27
|
||||
|
||||
### Security
|
||||
|
||||
* Updated dependencies, fixes security advisories:
|
||||
* [RUSTSEC-2024-0003](https://rustsec.org/advisories/RUSTSEC-2024-0003)
|
||||
* [RUSTSEC-2023-0072](https://rustsec.org/advisories/RUSTSEC-2024-0072)
|
||||
|
||||
### Fixed
|
||||
|
||||
* Fix clippy issue
|
||||
* Tweak/fix tests; reduce required accuracy for geocoded coordinates
|
||||
|
||||
## [0.2.10] - 2023-11-03
|
||||
|
||||
### Security
|
||||
|
||||
* Update dependencies
|
||||
([RUSTSEC-2020-0071](https://rustsec.org/advisories/RUSTSEC-2020-0071.html),
|
||||
[RUSTSEC-2023-0044](https://rustsec.org/advisories/RUSTSEC-2023-0044.html))
|
||||
|
||||
### Changed
|
||||
|
||||
* Switch to Rocket 0.5 RC4
|
||||
* Update dependency on `cached`
|
||||
|
||||
### Fixed
|
||||
|
||||
* Fix clippy issues
|
||||
|
||||
## [0.2.9] - 2023-08-25
|
||||
|
||||
### Changed
|
||||
|
||||
* Update release Gitea Actions workflow; add seperate job to release Debian
|
||||
package to the new repository
|
||||
|
||||
### Security
|
||||
|
||||
* Update dependencies ([RUSTSEC-2023-0044](https://rustsec.org/advisories/RUSTSEC-2023-0044))
|
||||
|
||||
## [0.2.8] - 2023-06-05
|
||||
|
||||
### Added
|
||||
|
||||
* Print the version on lift off (#30)
|
||||
* Add a `/version` endpoint to the API (#30)
|
||||
|
||||
### Changed
|
||||
|
||||
* Update dependency on `cached`
|
||||
|
||||
### Fixed
|
||||
|
||||
* Properly attribute the PAQI metric in its description(s)
|
||||
|
||||
### Removed
|
||||
|
||||
* No longer provide a map for the PAQI metric; the map used is only for pollen
|
||||
|
||||
## [0.2.7] - 2023-05-26
|
||||
|
||||
### Fixed
|
||||
|
||||
* Switch back to the original Buienradar color scheme/maps key (#27)
|
||||
* Fix the token used to publish the crate to the Cargo package index
|
||||
|
||||
## [0.2.6] - 2023-05-24
|
||||
|
||||
### Added
|
||||
|
||||
* Add full release Gitea Actions workflow
|
||||
|
||||
### Changed
|
||||
|
||||
* Simplify Gitea Actions check, lint and test workflow
|
||||
* Improve no known map colors found error description
|
||||
|
||||
### Fixed
|
||||
|
||||
* Update coordinates of Eindhoven in tests (Nomatim changed its geocoding)
|
||||
* Increase sampling area to 31×31 pixels (#26)
|
||||
* Switch to new Buienradar color scheme/maps key (#27)
|
||||
|
||||
## [0.2.5] - 2023-03-24
|
||||
|
||||
### Added
|
||||
|
||||
* Add Gitea Actions workflow for cargo
|
||||
|
||||
### Changed
|
||||
|
||||
* Updated dependencies on `cached`, `chrono-tz` and `geocoding`
|
||||
|
||||
### Fixed
|
||||
|
||||
* Fix float comparison in tests
|
||||
* Fix clippy issues
|
||||
|
||||
### Security
|
||||
|
||||
* Update dependencies ([RUSTSEC-2023-0018](https://rustsec.org/advisories/RUSTSEC-2023-0018.html))
|
||||
|
||||
## [0.2.4] - 2022-07-05
|
||||
|
||||
### Added
|
||||
|
@ -69,7 +172,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
|
||||
Initial release.
|
||||
|
||||
[Unreleased]: https://git.luon.net/paul/sinoptik/compare/v0.2.4...HEAD
|
||||
[Unreleased]: https://git.luon.net/paul/sinoptik/compare/v0.2.11...HEAD
|
||||
[0.2.11]: https://git.luon.net/paul/sinoptik/compare/v0.2.10...v0.2.11
|
||||
[0.2.10]: https://git.luon.net/paul/sinoptik/compare/v0.2.9...v0.2.10
|
||||
[0.2.9]: https://git.luon.net/paul/sinoptik/compare/v0.2.8...v0.2.9
|
||||
[0.2.8]: https://git.luon.net/paul/sinoptik/compare/v0.2.7...v0.2.8
|
||||
[0.2.7]: https://git.luon.net/paul/sinoptik/compare/v0.2.6...v0.2.7
|
||||
[0.2.6]: https://git.luon.net/paul/sinoptik/compare/v0.2.5...v0.2.6
|
||||
[0.2.5]: https://git.luon.net/paul/sinoptik/compare/v0.2.4...v0.2.5
|
||||
[0.2.4]: https://git.luon.net/paul/sinoptik/compare/v0.2.3...v0.2.4
|
||||
[0.2.3]: https://git.luon.net/paul/sinoptik/compare/v0.2.2...v0.2.3
|
||||
[0.2.2]: https://git.luon.net/paul/sinoptik/compare/v0.2.1...v0.2.2
|
||||
|
|
File diff suppressed because it is too large
Load Diff
16
Cargo.toml
16
Cargo.toml
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "sinoptik"
|
||||
version = "0.2.4"
|
||||
version = "0.2.11"
|
||||
authors = [
|
||||
"Admar Schoonen <admar@luon.net",
|
||||
"Paul van Tilburg <paul@luon.net>"
|
||||
|
@ -12,16 +12,19 @@ repository = "https://git.luon.net/paul/sinoptik"
|
|||
license = "MIT"
|
||||
|
||||
[dependencies]
|
||||
cached = { version = "0.34.0", features = ["async"] }
|
||||
cached = { version = "0.46.0", features = ["async"] }
|
||||
chrono = "0.4.19"
|
||||
chrono-tz = "0.6.1"
|
||||
chrono-tz = "0.8.1"
|
||||
csv = "1.1.6"
|
||||
geocoding = "0.3.1"
|
||||
geocoding = "0.4.0"
|
||||
image = "0.24.1"
|
||||
reqwest = { version = "0.11.9", features = ["json"] }
|
||||
rocket = { version = "0.5.0-rc.2", features = ["json"] }
|
||||
rocket = { version = "0.5.0-rc.3", features = ["json"] }
|
||||
thiserror = "1.0.31"
|
||||
|
||||
[build-dependencies]
|
||||
vergen = { version = "8.2.1", default-features = false, features = ["build", "git", "gitcl"] }
|
||||
|
||||
[dev-dependencies]
|
||||
assert_float_eq = "1.1.3"
|
||||
assert_matches = "1.5.0"
|
||||
|
@ -42,7 +45,8 @@ Currently supported metrics are:
|
|||
* O₃ concentration (per hour, from Luchtmeetnet)
|
||||
* Particulate matter (PM10) concentration (per hour, from Luchtmeetnet)
|
||||
* Pollen (per hour, from Buienradar)
|
||||
* Pollen/air quality index (per hour, from Buienradar)
|
||||
* Pollen/air quality index (per hour, combined from Buienradar and
|
||||
Luchtmeetnet)
|
||||
* Precipitation (per 5 minutes, from Buienradar)
|
||||
* UV index (per day, from Buienradar)
|
||||
|
||||
|
|
49
README.md
49
README.md
|
@ -11,7 +11,8 @@ Currently supported metrics are:
|
|||
* O₃ concentration (per hour, from [Luchtmeetnet])
|
||||
* Particulate matter (PM10) concentration (per hour, from [Luchtmeetnet])
|
||||
* Pollen (per hour, from [Buienradar])
|
||||
* Pollen/air quality index (per hour, from [Buienradar])
|
||||
* Pollen/air quality index (per hour, combined from [Buienradar] and
|
||||
[Luchtmeetnet])
|
||||
* Precipitation (per 5 minutes, from [Buienradar])
|
||||
* UV index (per day, from [Buienradar])
|
||||
|
||||
|
@ -67,7 +68,6 @@ GET /forecast?address=Stationsplein,Utrecht&metrics[]=all
|
|||
|
||||
or directly by using its geocoded position:
|
||||
|
||||
|
||||
```http
|
||||
GET /forecast?lat=52.0902&lon=5.1114&metrics[]=all
|
||||
```
|
||||
|
@ -75,8 +75,8 @@ GET /forecast?lat=52.0902&lon=5.1114&metrics[]=all
|
|||
### Metrics
|
||||
|
||||
When querying, the metrics need to be selected. It can be one of: `AQI`, `NO2`,
|
||||
`O3`, `PAQI`, `PM10`, `pollen`, `precipitation` or `UVI`. If you use metric `all`, or
|
||||
`all` is part of the selected metrics, all metrics will be retrieved.
|
||||
`O3`, `PAQI`, `PM10`, `pollen`, `precipitation` or `UVI`. If you use metric
|
||||
`all`, or `all` is part of the selected metrics, all metrics will be retrieved.
|
||||
Note that the parameter "array" notation as well as the repeated parameter
|
||||
notation are supported. For example:
|
||||
|
||||
|
@ -86,7 +86,7 @@ GET /forecast?address=Stationsplein,Utrecht&metrics=AQI&metrics=pollen
|
|||
GET /forecast?address=Stationsplein,Utrecht&metrics=all
|
||||
```
|
||||
|
||||
### Response
|
||||
### Forecast responses
|
||||
|
||||
The response of the API is a JSON object that contains three fixed fields:
|
||||
|
||||
|
@ -160,8 +160,8 @@ selecting the maximum value for each hour:
|
|||
|
||||
#### Errors
|
||||
|
||||
If geocoding of an address is requested but fails, a not found error is returned (HTTP 404).
|
||||
with the following body (this will change in the future):
|
||||
If geocoding of an address is requested but fails, a not found error is
|
||||
returned (HTTP 404). with the following body (this will change in the future):
|
||||
|
||||
```json
|
||||
{
|
||||
|
@ -173,10 +173,10 @@ with the following body (this will change in the future):
|
|||
}
|
||||
```
|
||||
|
||||
If for any specific metric an error occurs, the list with forecast items will be absent.
|
||||
However, the `errors` field will contain the error message for each failed metric.
|
||||
For example, say Buienradar is down and precipitation forecast items can not be
|
||||
retrieved:
|
||||
If for any specific metric an error occurs, the list with forecast items will
|
||||
be absent. However, the `errors` field will contain the error message for each
|
||||
failed metric. For example, say Buienradar is down and precipitation forecast
|
||||
items can not be retrieved:
|
||||
|
||||
```json
|
||||
{
|
||||
|
@ -210,13 +210,38 @@ or directly by using its geocoded position:
|
|||
GET /map?lat=52.0902&lon=5.1114&metric=pollen
|
||||
```
|
||||
|
||||
### Response
|
||||
### Map responses
|
||||
|
||||
The response is a PNG image with a crosshair drawn on the map. If geocoding of
|
||||
an address fails or if the position is out of bounds of the map, nothing is
|
||||
returned (HTTP 404). If the maps cannot/have not been downloaded or cached yet,
|
||||
a service unavailable error is returned (HTTP 503).
|
||||
|
||||
## Version API endpoint
|
||||
|
||||
The `/version` API endpoint provides information of the current version and
|
||||
build of the service. This can be used to check if it needs to be updated.
|
||||
Again, there is no path and no query parameters, just:
|
||||
|
||||
```http
|
||||
GET /version
|
||||
```
|
||||
|
||||
### Version responses
|
||||
|
||||
The response uses the JSON format and typically looks like this:
|
||||
|
||||
```json
|
||||
{
|
||||
"version": "0.2.7",
|
||||
"timestamp": "2023-05-29T13:34:34.701323159Z",
|
||||
"git_sha": "bb5962d",
|
||||
"git_timestamp": "2023-05-29T15:32:17.000000000+02:00"
|
||||
}
|
||||
```
|
||||
|
||||
(Build and git information in example output may be out of date.)
|
||||
|
||||
## License
|
||||
|
||||
Sinoptik is licensed under the MIT license (see the `LICENSE` file or
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
use std::error::Error;
|
||||
use vergen::EmitBuilder;
|
||||
|
||||
fn main() -> Result<(), Box<dyn Error>> {
|
||||
// Generate the `cargo:` instructions to fill the appropriate environment variables.
|
||||
EmitBuilder::builder().all_build().all_git().emit()?;
|
||||
|
||||
Ok(())
|
||||
}
|
92
src/lib.rs
92
src/lib.rs
|
@ -1,9 +1,17 @@
|
|||
#![doc = include_str!("../README.md")]
|
||||
#![warn(
|
||||
clippy::all,
|
||||
missing_copy_implementations,
|
||||
missing_debug_implementations,
|
||||
rust_2018_idioms,
|
||||
rustdoc::broken_intra_doc_links
|
||||
rustdoc::broken_intra_doc_links,
|
||||
trivial_casts,
|
||||
trivial_numeric_casts,
|
||||
renamed_and_removed_lints,
|
||||
unsafe_code,
|
||||
unstable_features,
|
||||
unused_import_braces,
|
||||
unused_qualifications
|
||||
)]
|
||||
#![deny(missing_docs)]
|
||||
|
||||
|
@ -13,6 +21,7 @@ use rocket::fairing::AdHoc;
|
|||
use rocket::http::Status;
|
||||
use rocket::response::Responder;
|
||||
use rocket::serde::json::Json;
|
||||
use rocket::serde::Serialize;
|
||||
use rocket::{get, routes, Build, Request, Rocket, State};
|
||||
|
||||
use self::forecast::{forecast, Forecast, Metric};
|
||||
|
@ -60,7 +69,7 @@ pub(crate) enum Error {
|
|||
UnsupportedMetric(Metric),
|
||||
}
|
||||
|
||||
impl<'r, 'o: 'r> rocket::response::Responder<'r, 'o> for Error {
|
||||
impl<'r, 'o: 'r> Responder<'r, 'o> for Error {
|
||||
fn respond_to(self, _request: &'r Request<'_>) -> rocket::response::Result<'o> {
|
||||
eprintln!("💥 Encountered error during request: {}", self);
|
||||
|
||||
|
@ -76,13 +85,41 @@ impl<'r, 'o: 'r> rocket::response::Responder<'r, 'o> for Error {
|
|||
}
|
||||
}
|
||||
|
||||
/// Result type that defaults to [`Error`] as the default error type.
|
||||
pub(crate) type Result<T, E = Error> = std::result::Result<T, E>;
|
||||
|
||||
#[derive(Responder)]
|
||||
#[response(content_type = "image/png")]
|
||||
struct PngImageData(Vec<u8>);
|
||||
|
||||
/// Result type that defaults to [`Error`] as the default error type.
|
||||
pub(crate) type Result<T, E = Error> = std::result::Result<T, E>;
|
||||
|
||||
/// The version information as JSON response.
|
||||
#[derive(Debug, Serialize)]
|
||||
#[serde(crate = "rocket::serde")]
|
||||
struct VersionInfo {
|
||||
/// The version of the build.
|
||||
version: String,
|
||||
|
||||
/// The timestamp of the build.
|
||||
timestamp: String,
|
||||
|
||||
/// The (most recent) git SHA used for the build.
|
||||
git_sha: String,
|
||||
|
||||
/// The timestamp of the last git commit used for the build.
|
||||
git_timestamp: String,
|
||||
}
|
||||
impl VersionInfo {
|
||||
/// Retrieves the version information from the environment variables.
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
version: String::from(env!("CARGO_PKG_VERSION")),
|
||||
timestamp: String::from(env!("VERGEN_BUILD_TIMESTAMP")),
|
||||
git_sha: String::from(&env!("VERGEN_GIT_SHA")[0..7]),
|
||||
git_timestamp: String::from(env!("VERGEN_GIT_COMMIT_TIMESTAMP")),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Handler for retrieving the forecast for an address.
|
||||
#[get("/forecast?<address>&<metrics>")]
|
||||
async fn forecast_address(
|
||||
|
@ -142,6 +179,12 @@ async fn map_geo(
|
|||
image_data.map(PngImageData)
|
||||
}
|
||||
|
||||
/// Returns the version information.
|
||||
#[get("/version", format = "application/json")]
|
||||
async fn version() -> Result<Json<VersionInfo>> {
|
||||
Ok(Json(VersionInfo::new()))
|
||||
}
|
||||
|
||||
/// Sets up Rocket.
|
||||
fn rocket(maps_handle: MapsHandle) -> Rocket<Build> {
|
||||
let maps_refresher = maps::run(Arc::clone(&maps_handle));
|
||||
|
@ -149,13 +192,28 @@ fn rocket(maps_handle: MapsHandle) -> Rocket<Build> {
|
|||
rocket::build()
|
||||
.mount(
|
||||
"/",
|
||||
routes![forecast_address, forecast_geo, map_address, map_geo],
|
||||
routes![
|
||||
forecast_address,
|
||||
forecast_geo,
|
||||
map_address,
|
||||
map_geo,
|
||||
version
|
||||
],
|
||||
)
|
||||
.manage(maps_handle)
|
||||
.attach(AdHoc::on_liftoff("Maps refresher", |_| {
|
||||
Box::pin(async move {
|
||||
// We don't care about the join handle nor error results?
|
||||
let _ = rocket::tokio::spawn(maps_refresher);
|
||||
let _refresher = rocket::tokio::spawn(maps_refresher);
|
||||
})
|
||||
}))
|
||||
.attach(AdHoc::on_liftoff("Version", |_| {
|
||||
Box::pin(async move {
|
||||
let name = env!("CARGO_PKG_NAME");
|
||||
let version = env!("CARGO_PKG_VERSION");
|
||||
let git_sha = &env!("VERGEN_GIT_SHA")[0..7];
|
||||
|
||||
println!("🌁 Started {name} v{version} (git @{git_sha})");
|
||||
})
|
||||
}))
|
||||
}
|
||||
|
@ -205,8 +263,8 @@ mod tests {
|
|||
let response = client.get("/forecast?address=eindhoven").dispatch();
|
||||
assert_eq!(response.status(), Status::Ok);
|
||||
let json = response.into_json::<JsonValue>().expect("Not valid JSON");
|
||||
assert_f64_near!(json["lat"].as_f64().unwrap(), 51.4392648);
|
||||
assert_f64_near!(json["lon"].as_f64().unwrap(), 5.478633);
|
||||
assert_float_absolute_eq!(json["lat"].as_f64().unwrap(), 51.448557, 1e-5);
|
||||
assert_float_absolute_eq!(json["lon"].as_f64().unwrap(), 5.450123, 1e-5);
|
||||
assert_matches!(json["time"], JsonValue::Number(_));
|
||||
assert_matches!(json.get("AQI"), None);
|
||||
assert_matches!(json.get("NO2"), None);
|
||||
|
@ -223,8 +281,8 @@ mod tests {
|
|||
.dispatch();
|
||||
assert_eq!(response.status(), Status::Ok);
|
||||
let json = response.into_json::<JsonValue>().expect("Not valid JSON");
|
||||
assert_f64_near!(json["lat"].as_f64().unwrap(), 51.4392648);
|
||||
assert_f64_near!(json["lon"].as_f64().unwrap(), 5.478633);
|
||||
assert_float_absolute_eq!(json["lat"].as_f64().unwrap(), 51.448557, 1e-5);
|
||||
assert_float_absolute_eq!(json["lon"].as_f64().unwrap(), 5.450123, 1e-5);
|
||||
assert_matches!(json["time"], JsonValue::Number(_));
|
||||
assert_matches!(json.get("AQI"), Some(JsonValue::Array(_)));
|
||||
assert_matches!(json.get("NO2"), Some(JsonValue::Array(_)));
|
||||
|
@ -308,7 +366,7 @@ mod tests {
|
|||
|
||||
// No metric selected, don't know which map to show?
|
||||
let response = client.get("/map?address=eindhoven").dispatch();
|
||||
assert_eq!(response.status(), Status::NotFound);
|
||||
assert_eq!(response.status(), Status::UnprocessableEntity);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
@ -317,10 +375,6 @@ mod tests {
|
|||
let maps_handle_clone = Arc::clone(&maps_handle);
|
||||
let client = Client::tracked(rocket(maps_handle)).expect("Not a valid Rocket instance");
|
||||
|
||||
// No metric passed, don't know which map to show?
|
||||
let response = client.get("/map?lat=51.4&lon=5.5").dispatch();
|
||||
assert_eq!(response.status(), Status::NotFound);
|
||||
|
||||
// No maps available yet.
|
||||
let response = client.get("/map?lat=51.4&lon=5.5&metric=pollen").dispatch();
|
||||
assert_eq!(response.status(), Status::ServiceUnavailable);
|
||||
|
@ -337,8 +391,12 @@ mod tests {
|
|||
assert_eq!(response.status(), Status::Ok);
|
||||
assert_eq!(response.content_type(), Some(ContentType::PNG));
|
||||
|
||||
// ... but not if it is out of bounds.
|
||||
let response = client.get("/map?lat=0.0&lon=0.0&metric=pollen").dispatch();
|
||||
assert_eq!(response.status(), Status::NotFound);
|
||||
|
||||
// No metric passed, don't know which map to show?
|
||||
let response = client.get("/map?lat=51.4&lon=5.5").dispatch();
|
||||
assert_eq!(response.status(), Status::NotFound);
|
||||
assert_eq!(response.status(), Status::UnprocessableEntity);
|
||||
}
|
||||
}
|
||||
|
|
10
src/main.rs
10
src/main.rs
|
@ -1,9 +1,17 @@
|
|||
#![doc = include_str!("../README.md")]
|
||||
#![warn(
|
||||
clippy::all,
|
||||
missing_copy_implementations,
|
||||
missing_debug_implementations,
|
||||
rust_2018_idioms,
|
||||
rustdoc::broken_intra_doc_links
|
||||
rustdoc::broken_intra_doc_links,
|
||||
trivial_casts,
|
||||
trivial_numeric_casts,
|
||||
renamed_and_removed_lints,
|
||||
unsafe_code,
|
||||
unstable_features,
|
||||
unused_import_braces,
|
||||
unused_qualifications
|
||||
)]
|
||||
#![deny(missing_docs)]
|
||||
|
||||
|
|
35
src/maps.rs
35
src/maps.rs
|
@ -8,7 +8,7 @@ use std::f64::consts::PI;
|
|||
use std::sync::{Arc, Mutex};
|
||||
|
||||
use chrono::serde::ts_seconds;
|
||||
use chrono::{DateTime, Duration, NaiveDateTime, Utc};
|
||||
use chrono::{DateTime, Duration, NaiveDateTime, TimeZone, Utc};
|
||||
use image::{
|
||||
DynamicImage, GenericImage, GenericImageView, ImageError, ImageFormat, Pixel, Rgb, Rgba,
|
||||
};
|
||||
|
@ -47,8 +47,8 @@ pub(crate) enum Error {
|
|||
#[error("Failed to join a task: {0}")]
|
||||
Join(#[from] rocket::tokio::task::JoinError),
|
||||
|
||||
/// Found no known (map key) colors in samples.
|
||||
#[error("Found not known colors in samples")]
|
||||
/// Did not find any known (map key) colors in samples.
|
||||
#[error("Did not find any known colors in samples")]
|
||||
NoKnownColorsInSamples,
|
||||
|
||||
/// No maps found (yet).
|
||||
|
@ -78,22 +78,22 @@ type MapKeyHistogram = HashMap<Rgb<u8>, u32>;
|
|||
/// Note that the actual score starts from 1, not 0 as per this array.
|
||||
#[rustfmt::skip]
|
||||
const MAP_KEY: [[u8; 3]; 10] = [
|
||||
[0x49, 0xDA, 0x21],
|
||||
[0x30, 0xD2, 0x00],
|
||||
[0xFF, 0xF8, 0x8B],
|
||||
[0xFF, 0xF6, 0x42],
|
||||
[0xFD, 0xBB, 0x31],
|
||||
[0xFD, 0x8E, 0x24],
|
||||
[0xFC, 0x10, 0x3E],
|
||||
[0x97, 0x0A, 0x33],
|
||||
[0xA6, 0x6D, 0xBC],
|
||||
[0xB3, 0x30, 0xA1],
|
||||
[0x49, 0xDA, 0x21], // #49DA21
|
||||
[0x30, 0xD2, 0x00], // #30D200
|
||||
[0xFF, 0xF8, 0x8B], // #FFF88B
|
||||
[0xFF, 0xF6, 0x42], // #FFF642
|
||||
[0xFD, 0xBB, 0x31], // #FDBB31
|
||||
[0xFD, 0x8E, 0x24], // #FD8E24
|
||||
[0xFC, 0x10, 0x3E], // #FC103E
|
||||
[0x97, 0x0A, 0x33], // #970A33
|
||||
[0xA6, 0x6D, 0xBC], // #A66DBC
|
||||
[0xB3, 0x30, 0xA1], // #B330A1
|
||||
];
|
||||
|
||||
/// The Buienradar map sample size.
|
||||
///
|
||||
/// Determiess the number of pixels in width/height that is samples around the sampling coordinate.
|
||||
const MAP_SAMPLE_SIZE: [u32; 2] = [11, 11];
|
||||
/// Determines the number of pixels in width/height that is sampled around the sampling coordinate.
|
||||
const MAP_SAMPLE_SIZE: [u32; 2] = [31, 31];
|
||||
|
||||
/// The interval between map refreshes (in seconds).
|
||||
const REFRESH_INTERVAL: tokio::time::Duration = tokio::time::Duration::from_secs(60);
|
||||
|
@ -393,7 +393,7 @@ fn sample<I: GenericImageView<Pixel = Rgba<u8>>>(
|
|||
.expect("Maximum color is always a map key color") as u8;
|
||||
|
||||
samples.push(Sample { time, score });
|
||||
time = time + chrono::Duration::seconds(interval as i64);
|
||||
time += chrono::Duration::seconds(interval);
|
||||
offset += width;
|
||||
}
|
||||
|
||||
|
@ -449,7 +449,7 @@ async fn retrieve_image(url: Url) -> Result<RetrievedMaps> {
|
|||
.ok_or_else(|| Error::InvalidImagePath(path.to_owned()))?;
|
||||
let timestamp = NaiveDateTime::parse_from_str(timestamp_str, "%Y%m%d%H%M")?;
|
||||
|
||||
DateTime::<Utc>::from_utc(timestamp, Utc)
|
||||
Utc.from_utc_datetime(×tamp)
|
||||
};
|
||||
let bytes = response.bytes().await?;
|
||||
|
||||
|
@ -567,7 +567,6 @@ pub(crate) async fn mark_map(
|
|||
tokio::task::spawn_blocking(move || {
|
||||
let maps = maps_handle.lock().expect("Maps handle lock was poisoned");
|
||||
let image = match metric {
|
||||
Metric::PAQI => maps.pollen_mark(position),
|
||||
Metric::Pollen => maps.pollen_mark(position),
|
||||
Metric::UVI => maps.uvi_mark(position),
|
||||
_ => return Err(crate::Error::UnsupportedMetric(metric)),
|
||||
|
|
|
@ -109,7 +109,7 @@ pub(crate) async fn resolve_address(address: String) -> Result<Position> {
|
|||
let points: Vec<Point<f64>> = osm.forward(&address)?;
|
||||
|
||||
points
|
||||
.get(0)
|
||||
.first()
|
||||
.ok_or(Error::NoPositionFound)
|
||||
.map(Position::from)
|
||||
})
|
||||
|
|
|
@ -4,9 +4,8 @@
|
|||
//! and <https://www.buienradar.nl/overbuienradar/gratis-weerdata>.
|
||||
|
||||
use cached::proc_macro::cached;
|
||||
use chrono::offset::TimeZone;
|
||||
use chrono::serde::ts_seconds;
|
||||
use chrono::{DateTime, Datelike, Duration, NaiveTime, ParseError, Utc};
|
||||
use chrono::{DateTime, Datelike, Duration, NaiveTime, ParseError, TimeZone, Utc};
|
||||
use chrono_tz::Europe;
|
||||
use csv::ReaderBuilder;
|
||||
use reqwest::Url;
|
||||
|
@ -66,10 +65,10 @@ impl TryFrom<Row> for Item {
|
|||
/// time zone.
|
||||
fn parse_time(t: &str) -> Result<DateTime<Utc>, ParseError> {
|
||||
// First, get the current date in the Europe/Amsterdam time zone.
|
||||
let today = Utc::now().with_timezone(&Europe::Amsterdam).date();
|
||||
let today = Utc::now().with_timezone(&Europe::Amsterdam).date_naive();
|
||||
// Then, parse the time and interpret it relative to "today".
|
||||
let ntime = NaiveTime::parse_from_str(t, "%H:%M")?;
|
||||
let ndtime = today.naive_local().and_time(ntime);
|
||||
let ndtime = today.and_time(ntime);
|
||||
// Finally, interpret the naive date/time in the Europe/Amsterdam time zone and convert it to
|
||||
// the UTC time zone.
|
||||
let ldtime = Europe::Amsterdam.from_local_datetime(&ndtime).unwrap();
|
||||
|
@ -98,8 +97,9 @@ fn fix_items_day_boundary(items: Vec<Item>) -> Vec<Item> {
|
|||
let now = Utc::now().with_timezone(&Europe::Amsterdam);
|
||||
// Use noon on the same day as "now" as a comparison moment.
|
||||
let noon = Europe::Amsterdam
|
||||
.ymd(now.year(), now.month(), now.day())
|
||||
.and_hms(12, 0, 0);
|
||||
.with_ymd_and_hms(now.year(), now.month(), now.day(), 12, 0, 0)
|
||||
.single()
|
||||
.expect("Invalid date: input date is invalid or not unambiguous");
|
||||
|
||||
if now < noon {
|
||||
// It is still before noon, so bump timestamps after noon a day back.
|
||||
|
@ -107,7 +107,7 @@ fn fix_items_day_boundary(items: Vec<Item>) -> Vec<Item> {
|
|||
.into_iter()
|
||||
.map(|mut item| {
|
||||
if item.time > noon {
|
||||
item.time = item.time - Duration::days(1)
|
||||
item.time -= Duration::days(1)
|
||||
}
|
||||
item
|
||||
})
|
||||
|
@ -118,7 +118,7 @@ fn fix_items_day_boundary(items: Vec<Item>) -> Vec<Item> {
|
|||
.into_iter()
|
||||
.map(|mut item| {
|
||||
if item.time < noon {
|
||||
item.time = item.time + Duration::days(1)
|
||||
item.time += Duration::days(1)
|
||||
}
|
||||
item
|
||||
})
|
||||
|
|
|
@ -108,7 +108,7 @@ fn merge(
|
|||
// value.
|
||||
let items = pollen_samples
|
||||
.into_iter()
|
||||
.zip(aqi_items.into_iter())
|
||||
.zip(aqi_items)
|
||||
.map(|(pollen_sample, aqi_item)| {
|
||||
let time = pollen_sample.time;
|
||||
let value = (pollen_sample.score as f32).max(aqi_item.value);
|
||||
|
|
Loading…
Reference in New Issue