1pub const GT_MODEL_TYPE: u16 = 1024;
12pub const GT_RASTER_TYPE: u16 = 1025;
13pub const GT_CITATION: u16 = 1026;
14pub const GEODETIC_CRS_TYPE: u16 = 2048;
15pub const GEOGRAPHIC_TYPE: u16 = GEODETIC_CRS_TYPE;
16pub const GEODETIC_CITATION: u16 = 2049;
17pub const GEOG_CITATION: u16 = 2049;
18pub const GEODETIC_DATUM: u16 = 2050;
19pub const GEOG_GEODETIC_DATUM: u16 = 2050;
20pub const GEOG_ANGULAR_UNITS: u16 = 2054;
21pub const PROJECTED_CRS_TYPE: u16 = 3072;
22pub const PROJECTED_CS_TYPE: u16 = 3072;
23pub const PROJ_CITATION: u16 = 3073;
24pub const PROJECTION: u16 = 3074;
25pub const PROJ_COORD_TRANS: u16 = 3075;
26pub const PROJ_LINEAR_UNITS: u16 = 3076;
27pub const VERTICAL_CITATION: u16 = 4097;
28pub const VERTICAL_CS_TYPE: u16 = 4096;
29pub const VERTICAL_DATUM: u16 = 4098;
30pub const VERTICAL_UNITS: u16 = 4099;
31
32#[derive(Debug, Clone)]
34pub struct GeoKey {
35 pub id: u16,
36 pub value: GeoKeyValue,
37}
38
39#[derive(Debug, Clone)]
41pub enum GeoKeyValue {
42 Short(u16),
44 Double(Vec<f64>),
46 Ascii(String),
48}
49
50#[derive(Debug, Clone)]
52pub struct GeoKeyDirectory {
53 pub version: u16,
54 pub major_revision: u16,
55 pub minor_revision: u16,
56 pub keys: Vec<GeoKey>,
57}
58
59impl GeoKeyDirectory {
60 pub fn new() -> Self {
62 Self {
63 version: 1,
64 major_revision: 1,
65 minor_revision: 0,
66 keys: Vec::new(),
67 }
68 }
69
70 pub fn parse(directory: &[u16], double_params: &[f64], ascii_params: &str) -> Option<Self> {
76 if directory.len() < 4 {
77 return None;
78 }
79
80 let version = directory[0];
81 let major_revision = directory[1];
82 let minor_revision = directory[2];
83 let num_keys = directory[3] as usize;
84
85 if directory.len() < 4 + num_keys * 4 {
86 return None;
87 }
88
89 let mut keys = Vec::with_capacity(num_keys);
90 for i in 0..num_keys {
91 let base = 4 + i * 4;
92 let key_id = directory[base];
93 let location = directory[base + 1];
94 let count = directory[base + 2] as usize;
95 let value_offset = directory[base + 3];
96
97 let value = match location {
98 0 => {
99 GeoKeyValue::Short(value_offset)
101 }
102 34736 => {
103 let start = value_offset as usize;
105 let end = start + count;
106 if end <= double_params.len() {
107 GeoKeyValue::Double(double_params[start..end].to_vec())
108 } else {
109 continue;
110 }
111 }
112 34737 => {
113 let start = value_offset as usize;
115 let end = start + count;
116 if let Some(raw) = ascii_params.get(start..end) {
117 let s = raw.trim_end_matches('|').trim_end_matches('\0').to_string();
118 GeoKeyValue::Ascii(s)
119 } else {
120 continue;
121 }
122 }
123 _ => continue,
124 };
125
126 keys.push(GeoKey { id: key_id, value });
127 }
128
129 Some(Self {
130 version,
131 major_revision,
132 minor_revision,
133 keys,
134 })
135 }
136
137 pub fn get(&self, id: u16) -> Option<&GeoKey> {
139 self.keys.iter().find(|k| k.id == id)
140 }
141
142 pub fn get_short(&self, id: u16) -> Option<u16> {
144 self.get(id).and_then(|k| match &k.value {
145 GeoKeyValue::Short(v) => Some(*v),
146 _ => None,
147 })
148 }
149
150 pub fn get_ascii(&self, id: u16) -> Option<&str> {
152 self.get(id).and_then(|k| match &k.value {
153 GeoKeyValue::Ascii(s) => Some(s.as_str()),
154 _ => None,
155 })
156 }
157
158 pub fn get_double(&self, id: u16) -> Option<&[f64]> {
160 self.get(id).and_then(|k| match &k.value {
161 GeoKeyValue::Double(v) => Some(v.as_slice()),
162 _ => None,
163 })
164 }
165
166 pub fn set(&mut self, id: u16, value: GeoKeyValue) {
168 if let Some(existing) = self.keys.iter_mut().find(|k| k.id == id) {
169 existing.value = value;
170 } else {
171 self.keys.push(GeoKey { id, value });
172 }
173 }
174
175 pub fn remove(&mut self, id: u16) {
177 self.keys.retain(|k| k.id != id);
178 }
179
180 pub fn serialize(&self) -> (Vec<u16>, Vec<f64>, String) {
187 let mut sorted_keys = self.keys.clone();
188 sorted_keys.sort_by_key(|k| k.id);
189
190 let mut directory = Vec::new();
191 let mut double_params = Vec::new();
192 let mut ascii_params = String::new();
193
194 directory.push(self.version);
196 directory.push(self.major_revision);
197 directory.push(self.minor_revision);
198 directory.push(sorted_keys.len() as u16);
199
200 for key in &sorted_keys {
201 directory.push(key.id);
202 match &key.value {
203 GeoKeyValue::Short(v) => {
204 directory.push(0); directory.push(1); directory.push(*v); }
208 GeoKeyValue::Double(v) => {
209 directory.push(34736); directory.push(v.len() as u16);
211 directory.push(double_params.len() as u16); double_params.extend_from_slice(v);
213 }
214 GeoKeyValue::Ascii(s) => {
215 directory.push(34737); let ascii_with_pipe = format!("{}|", s);
217 directory.push(ascii_with_pipe.len() as u16);
218 directory.push(ascii_params.len() as u16); ascii_params.push_str(&ascii_with_pipe);
220 }
221 }
222 }
223
224 (directory, double_params, ascii_params)
225 }
226}
227
228impl Default for GeoKeyDirectory {
229 fn default() -> Self {
230 Self::new()
231 }
232}
233
234#[cfg(test)]
235mod tests {
236 use super::*;
237
238 #[test]
239 fn parse_roundtrip() {
240 let mut dir = GeoKeyDirectory::new();
241 dir.set(GT_MODEL_TYPE, GeoKeyValue::Short(2));
242 dir.set(GEOGRAPHIC_TYPE, GeoKeyValue::Short(4326));
243 dir.set(GEOG_CITATION, GeoKeyValue::Ascii("WGS 84".into()));
244
245 let (shorts, doubles, ascii) = dir.serialize();
246 let parsed = GeoKeyDirectory::parse(&shorts, &doubles, &ascii).unwrap();
247
248 assert_eq!(parsed.get_short(GT_MODEL_TYPE), Some(2));
249 assert_eq!(parsed.get_short(GEOGRAPHIC_TYPE), Some(4326));
250 assert_eq!(parsed.get_ascii(GEOG_CITATION), Some("WGS 84"));
251 }
252
253 #[test]
254 fn set_replaces_existing() {
255 let mut dir = GeoKeyDirectory::new();
256 dir.set(GT_MODEL_TYPE, GeoKeyValue::Short(1));
257 dir.set(GT_MODEL_TYPE, GeoKeyValue::Short(2));
258 assert_eq!(dir.get_short(GT_MODEL_TYPE), Some(2));
259 assert_eq!(dir.keys.len(), 1);
260 }
261
262 #[test]
263 fn remove_key() {
264 let mut dir = GeoKeyDirectory::new();
265 dir.set(GT_MODEL_TYPE, GeoKeyValue::Short(1));
266 dir.remove(GT_MODEL_TYPE);
267 assert!(dir.get(GT_MODEL_TYPE).is_none());
268 }
269
270 #[test]
271 fn parse_skips_invalid_ascii_subslice_without_panicking() {
272 let directory = [
273 1u16,
274 1,
275 0,
276 1, GEOG_CITATION,
278 34737,
279 1,
280 1, ];
282 let ascii = String::from_utf8_lossy(&[0xff, b'|']).into_owned();
283
284 let parsed = GeoKeyDirectory::parse(&directory, &[], &ascii).unwrap();
285 assert!(parsed.get_ascii(GEOG_CITATION).is_none());
286 }
287}