rjw_uktides/
lib.rs

1//! rjw-uktides is a small library to help fetch and parse tide predictions data from the UK
2//! Hydrographic Office [EasyTide] service.
3//!
4//! Tide predictions can be obtained for about 700 locations around Great Britain, Ireland, the
5//! Channel Islands, and the Isle of Man. The data includes the predicted times of high and low
6//! tides, so it's perfect for planning your next trip to the beach (please don't use it for
7//! navigation).
8//!
9//! EasyTide is a publicly available web application that uses two unauthenticated JSON endpoints
10//! to look up tide stations and tide predictions for those stations. No API key is needed.
11//!
12//! [EasyTide]: https://easytide.admiralty.co.uk/
13//!
14//! This library does not perform network IO itself, instead there are two pairs of functions that construct
15//! the appropriate URLs for you to query with your preferred HTTP client, and parse the returned
16//! JSON data:
17//!
18//! - [`stations_list_url()`] returns the URL that gives a list of tidal stations, which can then
19//!   be parsed with [`stations_from_reader()`].
20//! - [`tide_predictions_url()`] constructs a station-specific URL that gives tide prediction data,
21//!   which can then be parsed with [`tides_from_reader()`].
22//!
23//! # Example usage
24//!
25//! Here's a full usage example using [`ureq`] to perform the GET request.
26//!
27//! [`ureq`]: https://docs.rs/ureq/latest/ureq/
28//!
29//! ```
30//! # fn main() -> anyhow::Result<()> {
31//! # use rjw_uktides::{Station, TidePredictions};
32//! // Fetch the list of all stations.
33//! let url = rjw_uktides::stations_list_url();
34//! let body = ureq::get(url.as_str()).call()?.into_body().into_reader();
35//! let stations: Vec<Station> = rjw_uktides::stations_from_reader(body)?;
36//!
37//! // Fetch tide predictions for the first station
38//! let url = rjw_uktides::tide_predictions_url(&stations[0].id);
39//! let body = ureq::get(url.as_str()).call()?.into_body().into_reader();
40//! let predictions: TidePredictions = rjw_uktides::tides_from_reader(body)?;
41//!
42//! // Print the times of the high and low tides
43//! for event in predictions.tidal_event_list {
44//!     println!("{}    {}", event.date_time, event.event_type);
45//! }
46//! # Ok(())
47//! # }
48//! ```
49//!
50//! Typically you'll already know the station you want predictions for. Look up its ID, either from
51//! the list of stations or from the value of the `PortID` query parameter on the EasyTide website.
52//! For instance, [Sandown] on the Isle of Wight has an ID of "0053" (note that these are not numeric
53//! IDs). We can look it up directly by creating a [`StationId`] from that string:
54//!
55//! [Sandown]: https://easytide.admiralty.co.uk/?PortID=0053
56//!
57//! ```
58//! # fn main() -> anyhow::Result<()> {
59//! # use rjw_uktides::{StationId, tide_predictions_url, tides_from_reader};
60//! let sandown: StationId = "0053".into();
61//! let url = tide_predictions_url(&sandown);
62//! let body = ureq::get(url.as_str()).call()?.into_body().into_reader();
63//! let sandown_predictions = tides_from_reader(body)?;
64//! # Ok(())
65//! # }
66//!
67//! ```
68//!
69//! # Main types
70//!
71//! The main structs of interest are:
72//!
73//! - [`TidePredictions`], which includes all of the prediction data, notably the times of high
74//!   and low tides over the next few days;
75//! - [`StationId`], which you need to use to obtain those predictions; and
76//! - [`Station`], which contains more details about a particular tidal station.
77//!
78//!
79//! # Feature flags
80//!
81//! If you only need to use this as a library, you can disable the CLI binary and its
82//! dependencies by [disabling default features][def].
83//!
84//! [def]: https://doc.rust-lang.org/cargo/reference/features.html#the-default-feature
85
86mod error;
87mod parse;
88mod types;
89
90use std::io::Read;
91
92use url::Url;
93
94pub use crate::error::Error;
95pub use crate::types::{
96    Coordinates, Country, DecimalDegrees, LunarPhase, LunarPhaseType, Metres, Station, StationId,
97    TidalEvent, TidalEventType, TidalHeightOccurence, TidePredictions,
98};
99
100/// Stations list HTTP endpoint
101const STATIONS_URL: &str = "https://easytide.admiralty.co.uk/Home/GetStations";
102/// Station-specific tide predictions HTTP endpoint (requires `stationID` query parameter)
103const TIDES_URL: &str = "https://easytide.admiralty.co.uk/Home/GetPredictionData";
104
105/// Get the URL for information on all available stations.
106pub fn stations_list_url() -> Url {
107    STATIONS_URL
108        .parse()
109        .expect("Station list URL is known to be valid")
110}
111
112/// Parse a tide stations list from the reader.
113///
114/// `rdr` should provide JSON sourced from the URL returned by [`stations_list_url()`].
115///
116/// An error is returned if any relevant part of the JSON cannot be parsed as expected.
117pub fn stations_from_reader(rdr: impl Read) -> Result<Vec<Station>, Error> {
118    serde_json::from_reader(rdr)
119        .map(|sd: crate::parse::StationsData| sd.features)
120        .map_err(Error::Parse)
121}
122
123/// Construct a tide-prediction URL for the given station.
124pub fn tide_predictions_url(station: &StationId) -> Url {
125    Url::parse_with_params(TIDES_URL, &[("stationID", &station.0)])
126        .expect("Tide predictions URL is known to be valid")
127}
128
129/// Parse tide predictions for a specific station from the reader.
130///
131/// `rdr` should provide JSON sourced from the URL returned by [`tide_predictions_url()`].
132///
133/// An error is returned if any relevant part of the JSON cannot be parsed as expected.
134pub fn tides_from_reader(rdr: impl Read) -> Result<TidePredictions, Error> {
135    serde_json::from_reader(rdr).map_err(Error::Parse)
136}