Skip to main content

mvt/
encoder.rs

1// encoder.rs
2//
3// Copyright (c) 2019-2026  Minnesota Department of Transportation
4//
5//! Encoder for Mapbox Vector Tile (MVT) geometry.
6//!
7use crate::error::{Error, Result};
8use pointy::{BBox, Float, Pt, Seg, Transform};
9
10/// Path commands
11#[derive(Copy, Clone, Debug, Eq, PartialEq)]
12enum Command {
13    /// Move to new position
14    MoveTo = 1,
15
16    /// Line to new position
17    LineTo = 2,
18
19    /// Close current path
20    ClosePath = 7,
21}
22
23/// Integer command
24#[derive(Copy, Clone, Debug, Eq, PartialEq)]
25struct CommandInt {
26    /// Path command
27    id: Command,
28
29    /// Command count
30    count: u32,
31}
32
33/// Integer parameter
34#[derive(Copy, Clone, Debug, Eq, PartialEq)]
35struct ParamInt {
36    /// Parameter value
37    value: i32,
38}
39
40/// Geometry types for [Features](struct.Feature.html).
41#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
42pub enum GeomType {
43    /// Point or Multipoint
44    #[default]
45    Point,
46
47    /// Linestring or Multilinestring
48    Linestring,
49
50    /// Polygon or Multipolygon
51    Polygon,
52}
53
54/// Encoder for [Feature](struct.Feature.html) geometry.
55///
56/// This can consist of Point, Linestring or Polygon data.
57///
58/// # Example
59/// ```
60/// # use mvt::{Error, GeomEncoder, GeomType};
61/// # use pointy::Transform;
62/// # fn main() -> Result<(), Error> {
63/// let geom_data = GeomEncoder::new(GeomType::Point)
64///     .point(0.0, 0.0)?
65///     .point(10.0, 0.0)?
66///     .encode()?;
67/// # Ok(()) }
68/// ```
69#[derive(Default)]
70pub struct GeomEncoder<F>
71where
72    F: Float,
73{
74    /// Geometry type
75    geom_tp: GeomType,
76
77    /// X,Y position at end of linestring/polygon geometry
78    xy_end: Option<Pt<F>>,
79
80    /// Transform to MVT coordinates
81    transform: Transform<F>,
82
83    /// Bounding box
84    bbox: BBox<F>,
85
86    /// Minimum X value
87    x_min: i32,
88
89    /// Maximum X value
90    x_max: i32,
91
92    /// Minimum Y value
93    y_min: i32,
94
95    /// Maximum Y value
96    y_max: i32,
97
98    /// Previous tile point
99    pt0: Option<(i32, i32)>,
100
101    /// Current tile point
102    pt1: Option<(i32, i32)>,
103
104    /// Command offset
105    cmd_offset: usize,
106
107    /// Count of geometry data
108    count: u32,
109
110    /// Encoded geometry data
111    data: Vec<u32>,
112}
113
114/// Validated geometry data for [Feature](struct.Feature.html)s.
115///
116/// Use [GeomEncoder](struct.GeomEncoder.html) to encode.
117///
118/// # Example
119/// ```
120/// # use mvt::{Error, GeomEncoder, GeomType};
121/// # use pointy::Transform;
122/// # fn main() -> Result<(), Error> {
123/// let geom_data = GeomEncoder::new(GeomType::Point)
124///     .point(0.0, 0.0)?
125///     .point(10.0, 0.0)?
126///     .encode()?;
127/// # Ok(()) }
128/// ```
129pub struct GeomData {
130    /// Geometry type
131    geom_tp: GeomType,
132
133    /// Encoded geometry data
134    data: Vec<u32>,
135}
136
137impl CommandInt {
138    /// Create a new integer command
139    fn new(id: Command, count: u32) -> Self {
140        debug_assert!(count <= 0x1FFF_FFFF);
141        CommandInt { id, count }
142    }
143
144    /// Encode command
145    fn encode(&self) -> u32 {
146        ((self.id as u32) & 0x7) | (self.count << 3)
147    }
148
149    /// Decode command
150    fn decode(code: u32) -> Self {
151        let id = match code & 0x7 {
152            1 => Command::MoveTo,
153            2 => Command::LineTo,
154            7 => Command::ClosePath,
155            _ => panic!("Invalid code: {code}"),
156        };
157        let count = code >> 3;
158        CommandInt { id, count }
159    }
160}
161
162impl ParamInt {
163    /// Create a new integer parameter
164    fn new(value: i32) -> Self {
165        ParamInt { value }
166    }
167
168    /// Encode the parameter
169    fn encode(&self) -> u32 {
170        ((self.value << 1) ^ (self.value >> 31)) as u32
171    }
172}
173
174impl<F> GeomEncoder<F>
175where
176    F: Float,
177{
178    /// Create a new geometry encoder.
179    ///
180    /// * `geom_tp` Geometry type.
181    pub fn new(geom_tp: GeomType) -> Self {
182        GeomEncoder {
183            geom_tp,
184            x_min: i32::MIN,
185            x_max: i32::MAX,
186            y_min: i32::MIN,
187            y_max: i32::MAX,
188            ..Default::default()
189        }
190    }
191
192    /// Adjust min/max values
193    fn adjust_minmax(mut self) -> Self {
194        if self.bbox != BBox::default() {
195            let p = self.transform * (self.bbox.x_min(), self.bbox.y_min());
196            let x0 = p.x.round().to_i32().unwrap_or(i32::MIN);
197            let y0 = p.y.round().to_i32().unwrap_or(i32::MIN);
198            let p = self.transform * (self.bbox.x_max(), self.bbox.y_max());
199            let x1 = p.x.round().to_i32().unwrap_or(i32::MAX);
200            let y1 = p.y.round().to_i32().unwrap_or(i32::MAX);
201            self.x_min = x0.min(x1);
202            self.y_min = y0.min(y1);
203            self.x_max = x0.max(x1);
204            self.y_max = y0.max(y1);
205        }
206        self
207    }
208
209    /// Add a bounding box
210    pub fn bbox(mut self, bbox: BBox<F>) -> Self {
211        self.bbox = bbox;
212        self.adjust_minmax()
213    }
214
215    /// Add a transform
216    pub fn transform(mut self, transform: Transform<F>) -> Self {
217        self.transform = transform;
218        self.adjust_minmax()
219    }
220
221    /// Push a Command
222    fn push_command(&mut self, cmd: Command) {
223        log::trace!("push_command: {cmd:?}");
224        self.cmd_offset = self.data.len();
225        self.data.push(CommandInt::new(cmd, 1).encode());
226    }
227
228    /// Set count of the most recent Command.
229    fn set_command_count(&mut self, count: u32) {
230        let off = self.cmd_offset;
231        let mut cmd = CommandInt::decode(self.data[off]);
232        cmd.count = count;
233        self.data[off] = cmd.encode();
234    }
235
236    /// Push one point with relative coörindates.
237    fn push_point(&mut self, x: i32, y: i32) {
238        log::trace!("push_point: {x},{y}");
239        self.pt0 = self.pt1;
240        let (px, py) = self.pt0.unwrap_or((0, 0));
241        self.data.push(ParamInt::new(x.saturating_sub(px)).encode());
242        self.data.push(ParamInt::new(y.saturating_sub(py)).encode());
243        self.pt1 = Some((x, y));
244        self.count += 1;
245    }
246
247    /// Pop most recent point.
248    fn pop_point(&mut self) {
249        log::trace!("pop_point");
250        self.data.pop();
251        self.data.pop();
252        self.pt1 = self.pt0;
253        self.count -= 1;
254    }
255
256    /// Add a point, taking ownership (for method chaining).
257    pub fn point(mut self, x: F, y: F) -> Result<Self> {
258        self.add_point(x, y)?;
259        Ok(self)
260    }
261
262    /// Add a point.
263    pub fn add_point(&mut self, x: F, y: F) -> Result<()> {
264        self.add_boundary_points(x, y)?;
265        self.add_tile_point(x, y)
266    }
267
268    /// Add one or two boundary points (if needed).
269    fn add_boundary_points(&mut self, x: F, y: F) -> Result<()> {
270        if let Some(pxy) = self.xy_end {
271            let xy = Pt::from((x, y));
272            let seg = Seg::new(pxy, xy);
273            if let Some(seg) = seg.clip(self.bbox) {
274                if seg.p0 != pxy {
275                    self.add_tile_point(seg.p0.x, seg.p0.y)?;
276                }
277                if seg.p1 != xy {
278                    self.add_tile_point(seg.p1.x, seg.p1.y)?;
279                }
280            }
281        }
282        match self.geom_tp {
283            GeomType::Linestring | GeomType::Polygon => {
284                self.xy_end = Some(Pt::from((x, y)));
285            }
286            _ => (),
287        }
288        Ok(())
289    }
290
291    /// Add a tile point.
292    fn add_tile_point(&mut self, x: F, y: F) -> Result<()> {
293        let pt = self.make_point(x, y)?;
294        if let Some((px, py)) = self.pt1
295            && pt.0 == px
296            && pt.1 == py
297        {
298            if self.count == 0 {
299                // If the first point of a line in a multilinestring (or
300                // multipolygon) is the same as the last of the previous line,
301                // we skip the MoveTo command and increase the count so the
302                // next point correctly gets a LineTo.
303                self.count += 1;
304            } else {
305                // Redundant points other than the first are unexpected, and
306                // entirely skipped.
307                log::trace!("redundant point: {px},{py}");
308            }
309            return Ok(());
310        }
311        match self.geom_tp {
312            GeomType::Point => {
313                if self.count == 0 {
314                    self.push_command(Command::MoveTo);
315                }
316            }
317            GeomType::Linestring => match self.count {
318                0 => self.push_command(Command::MoveTo),
319                1 => self.push_command(Command::LineTo),
320                _ => (),
321            },
322            GeomType::Polygon => {
323                match self.count {
324                    0 => self.push_command(Command::MoveTo),
325                    1 => self.push_command(Command::LineTo),
326                    _ => (),
327                }
328                if self.count >= 2 && self.should_simplify_point(pt.0, pt.1) {
329                    self.pop_point();
330                }
331            }
332        }
333        self.push_point(pt.0, pt.1);
334        Ok(())
335    }
336
337    /// Make point with tile coörindates.
338    fn make_point(&self, x: F, y: F) -> Result<(i32, i32)> {
339        let p = self.transform * (x, y);
340        let mut x = p.x.round().to_i32().ok_or(Error::InvalidValue())?;
341        let mut y = p.y.round().to_i32().ok_or(Error::InvalidValue())?;
342        x = x.clamp(self.x_min, self.x_max);
343        y = y.clamp(self.y_min, self.y_max);
344        Ok((x, y))
345    }
346
347    /// Check if point should be simplified.
348    fn should_simplify_point(&self, x: i32, y: i32) -> bool {
349        if let (Some((p0x, p0y)), Some((p1x, p1y))) = (self.pt0, self.pt1) {
350            if p0x == p1x && p1x == x {
351                return (p0y < p1y && p1y < y) || (p0y > p1y && p1y > y);
352            }
353            if p0y == p1y && p1y == y {
354                return (p0x < p1x && p1x < x) || (p0x > p1x && p1x > x);
355            }
356        }
357        false
358    }
359
360    /// Complete the current geometry (for multilinestring / multipolygon).
361    pub fn complete_geom(&mut self) -> Result<()> {
362        // FIXME: return Error::InvalidGeometry
363        //        if "MUST" rules in the spec are violated
364        match self.geom_tp {
365            GeomType::Point => {
366                self.set_command_count(self.count);
367                // early return skips geometry reset
368                return Ok(());
369            }
370            GeomType::Linestring => {
371                if self.count > 1 {
372                    self.set_command_count(self.count - 1);
373                }
374            }
375            GeomType::Polygon => {
376                if self.count > 1 {
377                    self.set_command_count(self.count - 1);
378                    self.push_command(Command::ClosePath);
379                }
380            }
381        }
382        // reset linestring / polygon geometry state
383        self.count = 0;
384        self.xy_end = None;
385        self.pt0 = None;
386        Ok(())
387    }
388
389    /// Complete the current geometry (for multilinestring / multipolygon).
390    pub fn complete(mut self) -> Result<Self> {
391        self.complete_geom()?;
392        Ok(self)
393    }
394
395    /// Encode the geometry data, consuming the encoder.
396    pub fn encode(mut self) -> Result<GeomData> {
397        // FIXME: return Error::InvalidGeometry
398        //        if "MUST" rules in the spec are violated
399        self = self.complete()?;
400        Ok(GeomData::new(self.geom_tp, self.data))
401    }
402}
403
404impl GeomData {
405    /// Create new geometry data.
406    ///
407    /// * `geom_tp` Geometry type.
408    /// * `data` Validated geometry.
409    fn new(geom_tp: GeomType, data: Vec<u32>) -> Self {
410        GeomData { geom_tp, data }
411    }
412
413    /// Get the geometry type
414    pub(crate) fn geom_type(&self) -> GeomType {
415        self.geom_tp
416    }
417
418    /// Check if data is empty
419    pub fn is_empty(&self) -> bool {
420        self.data.is_empty()
421    }
422
423    /// Get length of data
424    pub fn len(&self) -> usize {
425        self.data.len()
426    }
427
428    /// Get the geometry data
429    pub(crate) fn into_vec(self) -> Vec<u32> {
430        self.data
431    }
432}
433
434#[cfg(test)]
435mod test {
436    use super::*;
437
438    // Examples from MVT spec:
439    #[test]
440    fn test_point() {
441        let v = GeomEncoder::new(GeomType::Point)
442            .point(25.0, 17.0)
443            .unwrap()
444            .encode()
445            .unwrap()
446            .into_vec();
447        assert_eq!(v, vec!(9, 50, 34));
448    }
449
450    #[test]
451    fn test_multipoint() {
452        let v = GeomEncoder::new(GeomType::Point)
453            .point(5.0, 7.0)
454            .unwrap()
455            .point(3.0, 2.0)
456            .unwrap()
457            .encode()
458            .unwrap()
459            .into_vec();
460        assert_eq!(v, vec!(17, 10, 14, 3, 9));
461    }
462
463    #[test]
464    fn test_linestring() {
465        let v = GeomEncoder::new(GeomType::Linestring)
466            .point(2.0, 2.0)
467            .unwrap()
468            .point(2.0, 10.0)
469            .unwrap()
470            .point(10.0, 10.0)
471            .unwrap()
472            .encode()
473            .unwrap()
474            .into_vec();
475        assert_eq!(v, vec!(9, 4, 4, 18, 0, 16, 16, 0));
476    }
477
478    #[test]
479    fn test_multilinestring() {
480        let v = GeomEncoder::new(GeomType::Linestring)
481            .point(2.0, 2.0)
482            .unwrap()
483            .point(2.0, 10.0)
484            .unwrap()
485            .point(10.0, 10.0)
486            .unwrap()
487            .complete()
488            .unwrap()
489            .point(1.0, 1.0)
490            .unwrap()
491            .point(3.0, 5.0)
492            .unwrap()
493            .encode()
494            .unwrap()
495            .into_vec();
496        assert_eq!(v, vec!(9, 4, 4, 18, 0, 16, 16, 0, 9, 17, 17, 10, 4, 8));
497    }
498
499    #[test]
500    fn test_multilinestring_with_redundant_points() {
501        let v = GeomEncoder::new(GeomType::Linestring)
502            .point(2.0, 2.0)
503            .unwrap()
504            .point(2.0, 2.0)
505            .unwrap()
506            .point(10.0, 10.0)
507            .unwrap()
508            .complete()
509            .unwrap()
510            .point(10.0, 10.0)
511            .unwrap()
512            .point(13.0, 15.0)
513            .unwrap()
514            .point(10.0, 10.0)
515            .unwrap()
516            .complete()
517            .unwrap()
518            .point(2.0, 2.0)
519            .unwrap()
520            .point(10.0, 10.0)
521            .unwrap()
522            .encode()
523            .unwrap()
524            .into_vec();
525        assert_eq!(
526            v,
527            vec!(9, 4, 4, 10, 16, 16, 18, 6, 10, 5, 9, 9, 15, 15, 10, 16, 16)
528        );
529    }
530
531    #[test]
532    fn test_polygon() {
533        let v = GeomEncoder::new(GeomType::Polygon)
534            .point(3.0, 6.0)
535            .unwrap()
536            .point(8.0, 12.0)
537            .unwrap()
538            .point(20.0, 34.0)
539            .unwrap()
540            .encode()
541            .unwrap()
542            .into_vec();
543        assert_eq!(v, vec!(9, 6, 12, 18, 10, 12, 24, 44, 15));
544    }
545
546    #[test]
547    fn test_multipolygon() {
548        let v = GeomEncoder::new(GeomType::Polygon)
549            // positive area => exterior ring
550            .point(0.0, 0.0)
551            .unwrap()
552            .point(10.0, 0.0)
553            .unwrap()
554            .point(10.0, 10.0)
555            .unwrap()
556            .point(0.0, 10.0)
557            .unwrap()
558            .complete()
559            .unwrap()
560            // positive area => exterior ring
561            .point(11.0, 11.0)
562            .unwrap()
563            .point(20.0, 11.0)
564            .unwrap()
565            .point(20.0, 20.0)
566            .unwrap()
567            .point(11.0, 20.0)
568            .unwrap()
569            .complete()
570            .unwrap()
571            // negative area => interior ring
572            .point(13.0, 13.0)
573            .unwrap()
574            .point(13.0, 17.0)
575            .unwrap()
576            .point(17.0, 17.0)
577            .unwrap()
578            .point(17.0, 13.0)
579            .unwrap()
580            .encode()
581            .unwrap()
582            .into_vec();
583        assert_eq!(
584            v,
585            vec!(
586                9, 0, 0, 26, 20, 0, 0, 20, 19, 0, 15, 9, 22, 2, 26, 18, 0, 0,
587                18, 17, 0, 15, 9, 4, 13, 26, 0, 8, 8, 0, 0, 7, 15
588            )
589        );
590    }
591}