1#[derive(Debug, Clone, Default, PartialEq, Eq)]
8#[non_exhaustive]
9pub enum Crs {
10 #[default]
12 Unknown,
13 Epsg(u32),
15 Wkt(String),
17 Projjson(String),
19}
20
21impl Crs {
22 pub fn is_unknown(&self) -> bool {
23 matches!(self, Crs::Unknown)
24 }
25
26 pub fn epsg_code(&self) -> Option<u32> {
40 match self {
41 Crs::Epsg(n) => Some(*n),
42 Crs::Wkt(s) => extract_trailing_epsg(s).or_else(|| epsg_from_wkt_name(s)),
43 Crs::Unknown | Crs::Projjson(_) => None,
44 }
45 }
46
47 pub fn to_projjson(&self) -> Option<String> {
60 let code = self.epsg_code()?;
61 Some(format!(
62 r#"{{"$schema":"https://proj.org/schemas/v0.7/projjson.schema.json","id":{{"authority":"EPSG","code":{code}}}}}"#
63 ))
64 }
65}
66
67fn extract_trailing_epsg(wkt: &str) -> Option<u32> {
72 let candidates = [
74 find_clause_value(wkt, "AUTHORITY[\"EPSG\",\""),
75 find_clause_value(wkt, "ID[\"EPSG\","),
76 ];
77 candidates.into_iter().flatten().last()
78}
79
80fn find_clause_value(wkt: &str, opener: &str) -> Option<u32> {
81 let pos = wkt.rfind(opener)?;
84 let rest = &wkt[pos + opener.len()..];
85 let end = rest.find(['"', ']', ',', ' ']).unwrap_or(rest.len());
86 rest[..end].parse::<u32>().ok()
87}
88
89fn epsg_from_wkt_name(wkt: &str) -> Option<u32> {
96 let name = extract_outer_crs_name(wkt)?;
97 epsg_for_common_name(&name)
98}
99
100fn extract_outer_crs_name(wkt: &str) -> Option<String> {
101 let openers = ["PROJCS[\"", "PROJCRS[\"", "GEOGCS[\"", "GEOGCRS[\""];
103 let opener_pos = openers
104 .iter()
105 .filter_map(|op| wkt.find(op).map(|p| (p + op.len(), op)))
106 .min_by_key(|(p, _)| *p)?;
107 let start = opener_pos.0;
108 let rest = &wkt[start..];
109 let end = rest.find('"')?;
110 Some(rest[..end].to_string())
111}
112
113fn epsg_for_common_name(name: &str) -> Option<u32> {
114 let normalized = name.trim().replace('_', " ").to_ascii_uppercase();
116 Some(match normalized.as_str() {
117 "GDA2020" | "GCS GDA 2020" | "GDA 2020" => 7844,
119 "GDA94" | "GCS GDA 1994" | "GDA 1994" => 4283,
121 "WGS 84" | "WGS84" | "WGS 1984" | "GCS WGS 1984" => 4326,
123 "NAD83" | "NAD 83" | "GCS NORTH AMERICAN 1983" => 4269,
125 "NAD27" | "NAD 27" | "GCS NORTH AMERICAN 1927" => 4267,
127 "WGS 84 / PSEUDO-MERCATOR" | "WGS 1984 WEB MERCATOR AUXILIARY SPHERE" | "WEB MERCATOR" => {
129 3857
130 }
131 "OSGB 1936 / BRITISH NATIONAL GRID"
133 | "BRITISH NATIONAL GRID"
134 | "OSGB36 / BRITISH NATIONAL GRID" => 27700,
135 _ => return None,
136 })
137}
138
139#[cfg(test)]
140mod tests {
141 use super::*;
142
143 #[test]
144 fn epsg_code_from_epsg_variant() {
145 assert_eq!(Crs::Epsg(4326).epsg_code(), Some(4326));
146 assert_eq!(Crs::Epsg(7844).epsg_code(), Some(7844));
147 }
148
149 #[test]
150 fn epsg_code_from_wkt_authority_clause() {
151 let wkt = r#"GEOGCS["WGS 84",DATUM["WGS_1984",SPHEROID["WGS 84",6378137,298.257223563,AUTHORITY["EPSG","7030"]],AUTHORITY["EPSG","6326"]],PRIMEM["Greenwich",0,AUTHORITY["EPSG","8901"]],UNIT["degree",0.0174532925199433,AUTHORITY["EPSG","9122"]],AUTHORITY["EPSG","4326"]]"#;
152 assert_eq!(Crs::Wkt(wkt.into()).epsg_code(), Some(4326));
153 }
154
155 #[test]
156 fn epsg_code_from_wkt2_id_clause() {
157 let wkt = r#"GEOGCRS["GDA2020",DATUM["GDA2020"],PRIMEM["Greenwich",0],ID["EPSG",7844]]"#;
158 assert_eq!(Crs::Wkt(wkt.into()).epsg_code(), Some(7844));
159 }
160
161 #[test]
162 fn epsg_code_none_for_unknown_or_projjson() {
163 assert_eq!(Crs::Unknown.epsg_code(), None);
164 assert_eq!(Crs::Projjson("{}".into()).epsg_code(), None);
165 assert_eq!(Crs::Wkt("LOCAL_CS[\"custom\"]".into()).epsg_code(), None);
167 }
168
169 #[test]
170 fn epsg_code_from_wkt_name_when_no_authority() {
171 let wkt = r#"GEOGCS["GDA2020",DATUM["GDA2020",SPHEROID["GRS_1980",6378137.0,298.257222101]],PRIMEM["Greenwich",0.0],UNIT["Degree",0.0174532925199433]]"#;
173 assert_eq!(Crs::Wkt(wkt.into()).epsg_code(), Some(7844));
174
175 let wkt = r#"GEOGCS["GCS_WGS_1984",DATUM["D_WGS_1984",SPHEROID["WGS_1984",6378137.0,298.257223563]],PRIMEM["Greenwich",0.0],UNIT["Degree",0.0174532925199433]]"#;
177 assert_eq!(Crs::Wkt(wkt.into()).epsg_code(), Some(4326));
178
179 let wkt = r#"GEOGCS["GCS_GDA_1994",DATUM["D_GDA_1994",SPHEROID["GRS_1980",6378137.0,298.257222101]],PRIMEM["Greenwich",0.0],UNIT["Degree",0.0174532925199433]]"#;
181 assert_eq!(Crs::Wkt(wkt.into()).epsg_code(), Some(4283));
182 }
183
184 #[test]
185 fn authority_takes_precedence_over_name_lookup() {
186 let wkt = r#"GEOGCS["GDA2020",DATUM["GDA2020"],AUTHORITY["EPSG","9999"]]"#;
188 assert_eq!(Crs::Wkt(wkt.into()).epsg_code(), Some(9999));
189 }
190
191 #[test]
192 fn projjson_minimal_form() {
193 let s = Crs::Epsg(4326).to_projjson().unwrap();
194 assert!(s.contains("\"authority\":\"EPSG\""));
195 assert!(s.contains("\"code\":4326"));
196 assert!(s.contains("$schema"));
197 }
198}