Skip to main content

geotiff_core/
geokeys.rs

1//! GeoKey directory parsing and construction (TIFF tag 34735).
2//!
3//! The GeoKey directory is stored as a TIFF SHORT array with the structure:
4//! - Header: KeyDirectoryVersion, KeyRevision, MinorRevision, NumberOfKeys
5//! - Entries: KeyID, TIFFTagLocation, Count, ValueOffset (repeated)
6//!
7//! GeoKeys reference values either inline (location=0), from the
8//! GeoDoubleParams tag (34736), or from the GeoAsciiParams tag (34737).
9
10// Well-known GeoKey IDs.
11pub 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/// A parsed GeoKey entry.
27#[derive(Debug, Clone)]
28pub struct GeoKey {
29    pub id: u16,
30    pub value: GeoKeyValue,
31}
32
33/// The value of a GeoKey.
34#[derive(Debug, Clone)]
35pub enum GeoKeyValue {
36    /// Short value stored inline.
37    Short(u16),
38    /// Double value(s) from GeoDoubleParams.
39    Double(Vec<f64>),
40    /// ASCII string from GeoAsciiParams.
41    Ascii(String),
42}
43
44/// Parsed GeoKey directory.
45#[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    /// Create an empty directory with default version (1.1.0).
55    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    /// Parse the GeoKey directory from the three GeoTIFF tags.
65    ///
66    /// - `directory`: contents of tag 34735 (SHORT array)
67    /// - `double_params`: contents of tag 34736 (DOUBLE array), may be empty
68    /// - `ascii_params`: contents of tag 34737 (ASCII), may be empty
69    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                    // Value is the offset itself (short).
94                    GeoKeyValue::Short(value_offset)
95                }
96                34736 => {
97                    // Value is in GeoDoubleParams.
98                    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                    // Value is in GeoAsciiParams.
108                    let start = value_offset as usize;
109                    let end = start + count;
110                    if end <= ascii_params.len() {
111                        let s = ascii_params[start..end]
112                            .trim_end_matches('|')
113                            .trim_end_matches('\0')
114                            .to_string();
115                        GeoKeyValue::Ascii(s)
116                    } else {
117                        continue;
118                    }
119                }
120                _ => continue,
121            };
122
123            keys.push(GeoKey { id: key_id, value });
124        }
125
126        Some(Self {
127            version,
128            major_revision,
129            minor_revision,
130            keys,
131        })
132    }
133
134    /// Look up a GeoKey by ID.
135    pub fn get(&self, id: u16) -> Option<&GeoKey> {
136        self.keys.iter().find(|k| k.id == id)
137    }
138
139    /// Get a short value for a key.
140    pub fn get_short(&self, id: u16) -> Option<u16> {
141        self.get(id).and_then(|k| match &k.value {
142            GeoKeyValue::Short(v) => Some(*v),
143            _ => None,
144        })
145    }
146
147    /// Get an ASCII value for a key.
148    pub fn get_ascii(&self, id: u16) -> Option<&str> {
149        self.get(id).and_then(|k| match &k.value {
150            GeoKeyValue::Ascii(s) => Some(s.as_str()),
151            _ => None,
152        })
153    }
154
155    /// Get double value(s) for a key.
156    pub fn get_double(&self, id: u16) -> Option<&[f64]> {
157        self.get(id).and_then(|k| match &k.value {
158            GeoKeyValue::Double(v) => Some(v.as_slice()),
159            _ => None,
160        })
161    }
162
163    /// Insert or replace a GeoKey.
164    pub fn set(&mut self, id: u16, value: GeoKeyValue) {
165        if let Some(existing) = self.keys.iter_mut().find(|k| k.id == id) {
166            existing.value = value;
167        } else {
168            self.keys.push(GeoKey { id, value });
169        }
170    }
171
172    /// Remove a GeoKey by ID.
173    pub fn remove(&mut self, id: u16) {
174        self.keys.retain(|k| k.id != id);
175    }
176
177    /// Serialize the directory into the three TIFF tag payloads.
178    ///
179    /// Returns `(directory_shorts, double_params, ascii_params)`.
180    /// Keys are sorted by ID per spec. Short values go inline (location=0),
181    /// Double values reference the double_params array (location=34736),
182    /// Ascii values reference the ascii_params string (location=34737).
183    pub fn serialize(&self) -> (Vec<u16>, Vec<f64>, String) {
184        let mut sorted_keys = self.keys.clone();
185        sorted_keys.sort_by_key(|k| k.id);
186
187        let mut directory = Vec::new();
188        let mut double_params = Vec::new();
189        let mut ascii_params = String::new();
190
191        // Header: version, major_revision, minor_revision, num_keys
192        directory.push(self.version);
193        directory.push(self.major_revision);
194        directory.push(self.minor_revision);
195        directory.push(sorted_keys.len() as u16);
196
197        for key in &sorted_keys {
198            directory.push(key.id);
199            match &key.value {
200                GeoKeyValue::Short(v) => {
201                    directory.push(0); // location: inline
202                    directory.push(1); // count
203                    directory.push(*v); // value
204                }
205                GeoKeyValue::Double(v) => {
206                    directory.push(34736); // location: GeoDoubleParams
207                    directory.push(v.len() as u16);
208                    directory.push(double_params.len() as u16); // offset
209                    double_params.extend_from_slice(v);
210                }
211                GeoKeyValue::Ascii(s) => {
212                    directory.push(34737); // location: GeoAsciiParams
213                    let ascii_with_pipe = format!("{}|", s);
214                    directory.push(ascii_with_pipe.len() as u16);
215                    directory.push(ascii_params.len() as u16); // offset
216                    ascii_params.push_str(&ascii_with_pipe);
217                }
218            }
219        }
220
221        (directory, double_params, ascii_params)
222    }
223}
224
225impl Default for GeoKeyDirectory {
226    fn default() -> Self {
227        Self::new()
228    }
229}
230
231#[cfg(test)]
232mod tests {
233    use super::*;
234
235    #[test]
236    fn parse_roundtrip() {
237        let mut dir = GeoKeyDirectory::new();
238        dir.set(GT_MODEL_TYPE, GeoKeyValue::Short(2));
239        dir.set(GEOGRAPHIC_TYPE, GeoKeyValue::Short(4326));
240        dir.set(GEOG_CITATION, GeoKeyValue::Ascii("WGS 84".into()));
241
242        let (shorts, doubles, ascii) = dir.serialize();
243        let parsed = GeoKeyDirectory::parse(&shorts, &doubles, &ascii).unwrap();
244
245        assert_eq!(parsed.get_short(GT_MODEL_TYPE), Some(2));
246        assert_eq!(parsed.get_short(GEOGRAPHIC_TYPE), Some(4326));
247        assert_eq!(parsed.get_ascii(GEOG_CITATION), Some("WGS 84"));
248    }
249
250    #[test]
251    fn set_replaces_existing() {
252        let mut dir = GeoKeyDirectory::new();
253        dir.set(GT_MODEL_TYPE, GeoKeyValue::Short(1));
254        dir.set(GT_MODEL_TYPE, GeoKeyValue::Short(2));
255        assert_eq!(dir.get_short(GT_MODEL_TYPE), Some(2));
256        assert_eq!(dir.keys.len(), 1);
257    }
258
259    #[test]
260    fn remove_key() {
261        let mut dir = GeoKeyDirectory::new();
262        dir.set(GT_MODEL_TYPE, GeoKeyValue::Short(1));
263        dir.remove(GT_MODEL_TYPE);
264        assert!(dir.get(GT_MODEL_TYPE).is_none());
265    }
266}