Skip to main content

use_projection/
lib.rs

1#![forbid(unsafe_code)]
2#![doc = include_str!("../README.md")]
3
4use core::{fmt, str::FromStr};
5use std::error::Error;
6
7fn non_empty_text(value: impl AsRef<str>) -> Result<String, ProjectionTextError> {
8    let trimmed = value.as_ref().trim();
9
10    if trimmed.is_empty() {
11        Err(ProjectionTextError::Empty)
12    } else {
13        Ok(trimmed.to_string())
14    }
15}
16
17fn normalized_token(value: &str) -> String {
18    value.trim().to_ascii_lowercase().replace(['_', ' '], "-")
19}
20
21#[derive(Clone, Copy, Debug, Eq, PartialEq)]
22pub enum ProjectionTextError {
23    Empty,
24}
25
26impl fmt::Display for ProjectionTextError {
27    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
28        match self {
29            Self::Empty => formatter.write_str("projection text cannot be empty"),
30        }
31    }
32}
33
34impl Error for ProjectionTextError {}
35
36#[derive(Clone, Copy, Debug, Eq, PartialEq)]
37pub enum ProjectionKindParseError {
38    Empty,
39}
40
41impl fmt::Display for ProjectionKindParseError {
42    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
43        match self {
44            Self::Empty => formatter.write_str("projection kind cannot be empty"),
45        }
46    }
47}
48
49impl Error for ProjectionKindParseError {}
50
51#[derive(Clone, Copy, Debug, Eq, PartialEq)]
52pub enum ProjectionParameterError {
53    EmptyKey,
54    EmptyValue,
55}
56
57impl fmt::Display for ProjectionParameterError {
58    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
59        match self {
60            Self::EmptyKey => formatter.write_str("projection parameter key cannot be empty"),
61            Self::EmptyValue => formatter.write_str("projection parameter value cannot be empty"),
62        }
63    }
64}
65
66impl Error for ProjectionParameterError {}
67
68#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
69pub struct ProjectionName(String);
70
71impl ProjectionName {
72    /// Creates a projection name from non-empty text.
73    ///
74    /// # Errors
75    ///
76    /// Returns [`ProjectionTextError::Empty`] when the trimmed value is empty.
77    pub fn new(value: impl AsRef<str>) -> Result<Self, ProjectionTextError> {
78        non_empty_text(value).map(Self)
79    }
80
81    #[must_use]
82    pub fn as_str(&self) -> &str {
83        &self.0
84    }
85
86    #[must_use]
87    pub fn into_string(self) -> String {
88        self.0
89    }
90}
91
92impl AsRef<str> for ProjectionName {
93    fn as_ref(&self) -> &str {
94        self.as_str()
95    }
96}
97
98impl fmt::Display for ProjectionName {
99    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
100        formatter.write_str(self.as_str())
101    }
102}
103
104impl FromStr for ProjectionName {
105    type Err = ProjectionTextError;
106
107    fn from_str(value: &str) -> Result<Self, Self::Err> {
108        Self::new(value)
109    }
110}
111
112#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
113pub enum ProjectionKind {
114    Mercator,
115    WebMercator,
116    TransverseMercator,
117    LambertConformalConic,
118    AlbersEqualArea,
119    Equirectangular,
120    Orthographic,
121    Stereographic,
122    Unknown,
123    Custom(String),
124}
125
126impl fmt::Display for ProjectionKind {
127    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
128        match self {
129            Self::Mercator => formatter.write_str("mercator"),
130            Self::WebMercator => formatter.write_str("web-mercator"),
131            Self::TransverseMercator => formatter.write_str("transverse-mercator"),
132            Self::LambertConformalConic => formatter.write_str("lambert-conformal-conic"),
133            Self::AlbersEqualArea => formatter.write_str("albers-equal-area"),
134            Self::Equirectangular => formatter.write_str("equirectangular"),
135            Self::Orthographic => formatter.write_str("orthographic"),
136            Self::Stereographic => formatter.write_str("stereographic"),
137            Self::Unknown => formatter.write_str("unknown"),
138            Self::Custom(value) => formatter.write_str(value),
139        }
140    }
141}
142
143impl FromStr for ProjectionKind {
144    type Err = ProjectionKindParseError;
145
146    fn from_str(value: &str) -> Result<Self, Self::Err> {
147        let trimmed = value.trim();
148
149        if trimmed.is_empty() {
150            return Err(ProjectionKindParseError::Empty);
151        }
152
153        Ok(match normalized_token(trimmed).as_str() {
154            "mercator" => Self::Mercator,
155            "web-mercator" => Self::WebMercator,
156            "transverse-mercator" => Self::TransverseMercator,
157            "lambert-conformal-conic" => Self::LambertConformalConic,
158            "albers-equal-area" => Self::AlbersEqualArea,
159            "equirectangular" => Self::Equirectangular,
160            "orthographic" => Self::Orthographic,
161            "stereographic" => Self::Stereographic,
162            "unknown" => Self::Unknown,
163            _ => Self::Custom(trimmed.to_string()),
164        })
165    }
166}
167
168#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
169pub struct ProjectionParameter {
170    key: String,
171    value: String,
172}
173
174impl ProjectionParameter {
175    /// Creates a projection parameter from non-empty key and value text.
176    ///
177    /// # Errors
178    ///
179    /// Returns [`ProjectionParameterError::EmptyKey`] when the trimmed key is empty.
180    /// Returns [`ProjectionParameterError::EmptyValue`] when the trimmed value is empty.
181    pub fn new(
182        key: impl AsRef<str>,
183        value: impl AsRef<str>,
184    ) -> Result<Self, ProjectionParameterError> {
185        let key = key.as_ref().trim();
186        let value = value.as_ref().trim();
187
188        if key.is_empty() {
189            return Err(ProjectionParameterError::EmptyKey);
190        }
191
192        if value.is_empty() {
193            return Err(ProjectionParameterError::EmptyValue);
194        }
195
196        Ok(Self {
197            key: key.to_string(),
198            value: value.to_string(),
199        })
200    }
201
202    #[must_use]
203    pub fn key(&self) -> &str {
204        &self.key
205    }
206
207    #[must_use]
208    pub fn value(&self) -> &str {
209        &self.value
210    }
211}
212
213impl fmt::Display for ProjectionParameter {
214    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
215        write!(formatter, "{}={}", self.key, self.value)
216    }
217}
218
219#[cfg(test)]
220mod tests {
221    use super::{
222        ProjectionKind, ProjectionKindParseError, ProjectionName, ProjectionParameter,
223        ProjectionParameterError, ProjectionTextError,
224    };
225
226    #[test]
227    fn valid_projection_name() -> Result<(), ProjectionTextError> {
228        let projection_name = ProjectionName::new("WGS 84 / World Mercator")?;
229
230        assert_eq!(projection_name.as_str(), "WGS 84 / World Mercator");
231        Ok(())
232    }
233
234    #[test]
235    fn empty_projection_name_rejected() {
236        assert_eq!(ProjectionName::new("   "), Err(ProjectionTextError::Empty));
237    }
238
239    #[test]
240    fn projection_kind_display_parse() -> Result<(), ProjectionKindParseError> {
241        assert_eq!(ProjectionKind::WebMercator.to_string(), "web-mercator");
242        assert_eq!(
243            "lambert conformal conic".parse::<ProjectionKind>()?,
244            ProjectionKind::LambertConformalConic
245        );
246        Ok(())
247    }
248
249    #[test]
250    fn custom_projection_kind() -> Result<(), ProjectionKindParseError> {
251        assert_eq!(
252            "azimuthal-equidistant".parse::<ProjectionKind>()?,
253            ProjectionKind::Custom(String::from("azimuthal-equidistant"))
254        );
255        Ok(())
256    }
257
258    #[test]
259    fn projection_parameter_construction() -> Result<(), ProjectionParameterError> {
260        let parameter = ProjectionParameter::new("central-meridian", "0")?;
261
262        assert_eq!(parameter.key(), "central-meridian");
263        assert_eq!(parameter.value(), "0");
264        assert_eq!(parameter.to_string(), "central-meridian=0");
265        Ok(())
266    }
267}