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 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 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}