1#[derive(Debug, Clone, Default, PartialEq, Eq)]
5pub enum Crs {
6 #[default]
8 Unknown,
9 Epsg(u32),
11 Wkt(String),
13 Projjson(String),
15}
16
17impl Crs {
18 pub fn is_unknown(&self) -> bool {
19 matches!(self, Crs::Unknown)
20 }
21
22 pub fn epsg_code(&self) -> Option<u32> {
36 match self {
37 Crs::Epsg(n) => Some(*n),
38 Crs::Wkt(s) => extract_trailing_epsg(s).or_else(|| epsg_from_wkt_name(s)),
39 Crs::Unknown | Crs::Projjson(_) => None,
40 }
41 }
42
43 pub fn to_projjson(&self) -> Option<String> {
56 let code = self.epsg_code()?;
57 Some(format!(
58 r#"{{"$schema":"https://proj.org/schemas/v0.7/projjson.schema.json","id":{{"authority":"EPSG","code":{code}}}}}"#
59 ))
60 }
61}
62
63fn extract_trailing_epsg(wkt: &str) -> Option<u32> {
68 let candidates = [
70 find_clause_value(wkt, "AUTHORITY[\"EPSG\",\""),
71 find_clause_value(wkt, "ID[\"EPSG\","),
72 ];
73 candidates.into_iter().flatten().last()
74}
75
76fn find_clause_value(wkt: &str, opener: &str) -> Option<u32> {
77 let pos = wkt.rfind(opener)?;
80 let rest = &wkt[pos + opener.len()..];
81 let end = rest.find(['"', ']', ',', ' ']).unwrap_or(rest.len());
82 rest[..end].parse::<u32>().ok()
83}
84
85fn epsg_from_wkt_name(wkt: &str) -> Option<u32> {
92 let name = extract_outer_crs_name(wkt)?;
93 epsg_for_common_name(&name)
94}
95
96fn extract_outer_crs_name(wkt: &str) -> Option<String> {
97 let openers = ["PROJCS[\"", "PROJCRS[\"", "GEOGCS[\"", "GEOGCRS[\""];
99 let opener_pos = openers
100 .iter()
101 .filter_map(|op| wkt.find(op).map(|p| (p + op.len(), op)))
102 .min_by_key(|(p, _)| *p)?;
103 let start = opener_pos.0;
104 let rest = &wkt[start..];
105 let end = rest.find('"')?;
106 Some(rest[..end].to_string())
107}
108
109fn epsg_for_common_name(name: &str) -> Option<u32> {
110 let normalized = name.trim().replace('_', " ").to_ascii_uppercase();
112 Some(match normalized.as_str() {
113 "GDA2020" | "GCS GDA 2020" | "GDA 2020" => 7844,
115 "GDA94" | "GCS GDA 1994" | "GDA 1994" => 4283,
117 "WGS 84" | "WGS84" | "WGS 1984" | "GCS WGS 1984" => 4326,
119 "NAD83" | "NAD 83" | "GCS NORTH AMERICAN 1983" => 4269,
121 "NAD27" | "NAD 27" | "GCS NORTH AMERICAN 1927" => 4267,
123 "WGS 84 / PSEUDO-MERCATOR" | "WGS 1984 WEB MERCATOR AUXILIARY SPHERE" | "WEB MERCATOR" => {
125 3857
126 }
127 "OSGB 1936 / BRITISH NATIONAL GRID"
129 | "BRITISH NATIONAL GRID"
130 | "OSGB36 / BRITISH NATIONAL GRID" => 27700,
131 _ => return None,
132 })
133}
134
135#[cfg(test)]
136mod tests {
137 use super::*;
138
139 #[test]
140 fn epsg_code_from_epsg_variant() {
141 assert_eq!(Crs::Epsg(4326).epsg_code(), Some(4326));
142 assert_eq!(Crs::Epsg(7844).epsg_code(), Some(7844));
143 }
144
145 #[test]
146 fn epsg_code_from_wkt_authority_clause() {
147 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"]]"#;
148 assert_eq!(Crs::Wkt(wkt.into()).epsg_code(), Some(4326));
149 }
150
151 #[test]
152 fn epsg_code_from_wkt2_id_clause() {
153 let wkt = r#"GEOGCRS["GDA2020",DATUM["GDA2020"],PRIMEM["Greenwich",0],ID["EPSG",7844]]"#;
154 assert_eq!(Crs::Wkt(wkt.into()).epsg_code(), Some(7844));
155 }
156
157 #[test]
158 fn epsg_code_none_for_unknown_or_projjson() {
159 assert_eq!(Crs::Unknown.epsg_code(), None);
160 assert_eq!(Crs::Projjson("{}".into()).epsg_code(), None);
161 assert_eq!(Crs::Wkt("LOCAL_CS[\"custom\"]".into()).epsg_code(), None);
163 }
164
165 #[test]
166 fn epsg_code_from_wkt_name_when_no_authority() {
167 let wkt = r#"GEOGCS["GDA2020",DATUM["GDA2020",SPHEROID["GRS_1980",6378137.0,298.257222101]],PRIMEM["Greenwich",0.0],UNIT["Degree",0.0174532925199433]]"#;
169 assert_eq!(Crs::Wkt(wkt.into()).epsg_code(), Some(7844));
170
171 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]]"#;
173 assert_eq!(Crs::Wkt(wkt.into()).epsg_code(), Some(4326));
174
175 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]]"#;
177 assert_eq!(Crs::Wkt(wkt.into()).epsg_code(), Some(4283));
178 }
179
180 #[test]
181 fn authority_takes_precedence_over_name_lookup() {
182 let wkt = r#"GEOGCS["GDA2020",DATUM["GDA2020"],AUTHORITY["EPSG","9999"]]"#;
184 assert_eq!(Crs::Wkt(wkt.into()).epsg_code(), Some(9999));
185 }
186
187 #[test]
188 fn projjson_minimal_form() {
189 let s = Crs::Epsg(4326).to_projjson().unwrap();
190 assert!(s.contains("\"authority\":\"EPSG\""));
191 assert!(s.contains("\"code\":4326"));
192 assert!(s.contains("$schema"));
193 }
194}