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