Skip to main content

mlt_core/encoder/
sort.rs

1//! Feature reordering for the optimizer
2
3use crate::codecs::hilbert::{hilbert_curve_params_from_bounds, hilbert_sort_key};
4use crate::codecs::morton::morton_sort_key;
5use crate::encoder::model::CurveParams;
6use crate::encoder::source::{LayerSource, Order};
7use crate::tile::TileLayer;
8
9/// Controls how features inside a layer are reordered before encoding.
10///
11/// Reordering features changes their position in every parallel column
12/// (geometry, ID, and all properties simultaneously), so the caller must
13/// opt in explicitly.
14#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, strum::EnumIter, strum::EnumCount)]
15pub enum SortStrategy {
16    /// Preserve the original feature order - no reordering is applied.
17    ///
18    /// This is the default.
19    #[default]
20    Unsorted,
21
22    /// Sort features by the Z-order (Morton) curve index of their first vertex.
23    ///
24    /// Fast to compute.  Spatially close features end up adjacent in the
25    /// stream, improving RLE run lengths for location-correlated properties
26    /// and CPU cache locality during client-side decoding.
27    ///
28    SpatialMorton,
29
30    /// Sort features by the Hilbert curve index of their first vertex.
31    ///
32    /// Slower to compute than Morton but achieves superior spatial locality.
33    SpatialHilbert,
34
35    /// Sort features by their feature ID in ascending order.
36    Id,
37}
38
39impl TileLayer {
40    /// Reorder features by `strategy`, using `params` as the curve normalization
41    /// for [`SortStrategy::SpatialMorton`] / [`SortStrategy::SpatialHilbert`].
42    ///
43    /// `params` is taken as a parameter (rather than recomputed here) so the
44    /// same scan feeds the encoder's dictionary builders, see
45    /// [`TileLayer::curve_params`].
46    ///
47    /// [`SortStrategy::Unsorted`] is a no-op; layers with ≤1 feature are
48    /// trivially unchanged.
49    #[hotpath::measure]
50    pub fn sort(&mut self, strategy: SortStrategy, params: CurveParams) {
51        if strategy == SortStrategy::Unsorted {
52            return;
53        }
54        let order = sort_order(self, strategy, params);
55        let mut features: Vec<_> = std::mem::take(&mut self.features)
56            .into_iter()
57            .map(Some)
58            .collect();
59        self.features = order
60            .iter()
61            .map(|f| features[f].take().expect("each feature appears once"))
62            .collect();
63    }
64
65    /// Hilbert/Morton `CurveParams` for this layer.
66    /// Bounds are order-invariant, so the optimizer computes this once per layer
67    /// and reuses it across every sort trial and the encoder's dictionary builders.
68    #[must_use]
69    pub fn curve_params(&self) -> CurveParams {
70        curve_params(self)
71    }
72}
73
74/// The order `strategy` puts the features of `source` in.
75#[hotpath::measure]
76pub(crate) fn sort_order(
77    source: &impl LayerSource,
78    strategy: SortStrategy,
79    params: CurveParams,
80) -> Order {
81    let len = source.feature_count();
82    match strategy {
83        SortStrategy::Unsorted => Order::Stored(len),
84        SortStrategy::SpatialMorton | SortStrategy::SpatialHilbert => {
85            let curve_key = if strategy == SortStrategy::SpatialMorton {
86                morton_sort_key
87            } else {
88                hilbert_sort_key
89            };
90            Order::Permuted(permutation(len, |f| {
91                source
92                    .first_coord(f)
93                    .map_or(u64::MAX, |c| u64::from(curve_key(c, params)))
94            }))
95        }
96        SortStrategy::Id => Order::Permuted(permutation(len, |f| source.id(f))),
97    }
98}
99
100/// Feature indexes ordered by `key`, with ties in stored order, as a stable sort leaves them.
101fn permutation<K: Ord>(len: usize, key: impl Fn(usize) -> K) -> Vec<u32> {
102    let mut keyed: Vec<(K, u32)> = (0..len)
103        .map(|f| (key(f), u32::try_from(f).expect("feature count fits in u32")))
104        .collect();
105    keyed.sort_unstable();
106    keyed.into_iter().map(|(_, f)| f).collect()
107}
108
109/// Hilbert/Morton `CurveParams` covering every coordinate of `source`.
110#[hotpath::measure]
111pub(crate) fn curve_params(source: &impl LayerSource) -> CurveParams {
112    let (min_val, max_val) = source.coords().fold((i32::MAX, i32::MIN), |(min, max), c| {
113        (min.min(c.x).min(c.y), max.max(c.x).max(c.y))
114    });
115    hilbert_curve_params_from_bounds(min_val, max_val)
116}
117
118#[cfg(test)]
119mod tests {
120    use geo_types::{
121        Coord, Geometry as GeoGeom, Geometry, GeometryCollection, Line, LineString,
122        MultiLineString, MultiPoint, MultiPolygon, Point, Polygon, Rect, Triangle,
123    };
124    use rstest::rstest;
125
126    use crate::decoder::{GeometryType, GeometryValues, RawGeometry};
127    use crate::encoder::model::CurveParams;
128    use crate::encoder::{
129        Codecs, Encoder, EncoderConfig, ExplicitEncoder, IntEncoder, SortStrategy, stage_tile,
130    };
131    use crate::test_helpers::{assert_empty, dec, into_layer01, parser};
132    use crate::tile::{TileFeature, TileLayer};
133    use crate::{Layer, LazyParsed};
134
135    fn pt(x: i32, y: i32) -> Geometry<i32> {
136        GeoGeom::Point(Point::new(x, y))
137    }
138
139    fn ls(coords: &[(i32, i32)]) -> Geometry<i32> {
140        GeoGeom::LineString(LineString::new(
141            coords.iter().map(|&(x, y)| Coord { x, y }).collect(),
142        ))
143    }
144
145    fn poly_square(x0: i32, y0: i32, side: i32) -> Geometry<i32> {
146        let ring = LineString::new(vec![
147            Coord { x: x0, y: y0 },
148            Coord {
149                x: x0 + side,
150                y: y0,
151            },
152            Coord {
153                x: x0 + side,
154                y: y0 + side,
155            },
156            Coord {
157                x: x0,
158                y: y0 + side,
159            },
160            Coord { x: x0, y: y0 },
161        ]);
162        GeoGeom::Polygon(Polygon::new(ring, vec![]))
163    }
164
165    /// Encode + serialize + parse + decode a `GeometryValues` (round-trip).
166    fn roundtrip_geom(decoded: &GeometryValues) -> GeometryValues {
167        let mut enc = Encoder::default();
168        let mut codecs = Codecs::default();
169        decoded
170            .clone()
171            .write_to(&mut enc, &mut codecs)
172            .expect("encode failed");
173        let buf = enc.data().to_vec();
174
175        let parsed = assert_empty(RawGeometry::from_bytes(&buf, &mut parser()));
176        let mut d = dec();
177        let result = LazyParsed::Raw(parsed)
178            .into_parsed(&mut d)
179            .expect("decode failed");
180        assert!(
181            d.consumed() > 0,
182            "decoder should consume bytes after decode"
183        );
184        result
185    }
186
187    /// Build the canonical (dense, wire-decoded) form of an ordered geometry sequence.
188    fn canonical(geoms: &[Geometry<i32>]) -> GeometryValues {
189        let mut decoded = GeometryValues::default();
190        for g in geoms {
191            decoded.push_geom(g);
192        }
193        roundtrip_geom(&decoded)
194    }
195
196    /// Build a `TileLayer` from `geoms` and `ids`, apply `reorder_features`,
197    /// and return it.
198    fn layer_after_sort(geoms: &[Geometry<i32>], ids: &[u64], strategy: SortStrategy) -> TileLayer {
199        let features: Vec<TileFeature> = geoms
200            .iter()
201            .zip(ids.iter())
202            .map(|(g, &id)| TileFeature {
203                id: Some(id),
204                geometry: g.clone(),
205                properties: vec![],
206                #[cfg(feature = "unstable-v2")]
207                m_values: vec![],
208                #[cfg(feature = "unstable-v2")]
209                nested: vec![],
210                #[cfg(feature = "unstable-v2")]
211                z: Vec::new(),
212            })
213            .collect();
214
215        let mut layer = TileLayer::from_parts("test", 4096, vec![], features).unwrap();
216
217        let params = layer.curve_params();
218        layer.sort(strategy, params);
219        layer
220    }
221
222    /// Sort, then encode+decode the result and compare to `canonical(expected)`.
223    fn assert_sort_roundtrip(
224        geoms: &[Geometry<i32>],
225        ids: &[u64],
226        strategy: SortStrategy,
227        expected: &[Geometry<i32>],
228    ) {
229        let layer = layer_after_sort(geoms, ids, strategy);
230
231        let mut sorted_decoded = GeometryValues::default();
232        for f in layer.features() {
233            sorted_decoded.push_geom(f.geometry());
234        }
235
236        let after_roundtrip = roundtrip_geom(&sorted_decoded);
237        let expected_canonical = canonical(expected);
238
239        assert_eq!(
240            after_roundtrip, expected_canonical,
241            "\nsorted geometry did not match expected after encode->decode round-trip\
242             \nvector_types after sort: {:?}\
243             \nvector_types expected:   {:?}",
244            sorted_decoded.vector_types, expected_canonical.vector_types,
245        );
246    }
247
248    // ── pure Points ──────────────────────────────────────────────────────────
249
250    #[test]
251    fn pure_points_id_sort_roundtrip() {
252        assert_sort_roundtrip(
253            &[pt(0, 0), pt(1, 1), pt(2, 2)],
254            &[3, 2, 1],
255            SortStrategy::Id,
256            &[pt(2, 2), pt(1, 1), pt(0, 0)],
257        );
258    }
259
260    // ── pure LineStrings ─────────────────────────────────────────────────────
261
262    #[test]
263    fn pure_linestrings_id_sort_roundtrip() {
264        assert_sort_roundtrip(
265            &[ls(&[(0, 0), (0, 10)]), ls(&[(5, 5), (10, 10)])],
266            &[2, 1],
267            SortStrategy::Id,
268            &[ls(&[(5, 5), (10, 10)]), ls(&[(0, 0), (0, 10)])],
269        );
270    }
271
272    // ── [Point, LineString, Point] ────────────────────────────────────────────
273
274    #[test]
275    fn point_line_point_id_sort_to_line_point_point_roundtrip() {
276        assert_sort_roundtrip(
277            &[pt(0, 0), ls(&[(1, 0), (1, 5)]), pt(5, 5)],
278            &[3, 1, 2],
279            SortStrategy::Id,
280            &[ls(&[(1, 0), (1, 5)]), pt(5, 5), pt(0, 0)],
281        );
282    }
283
284    #[test]
285    fn point_line_point_id_sort_to_point_point_line_roundtrip() {
286        assert_sort_roundtrip(
287            &[pt(0, 0), ls(&[(1, 0), (1, 5)]), pt(5, 5)],
288            &[1, 3, 2],
289            SortStrategy::Id,
290            &[pt(0, 0), pt(5, 5), ls(&[(1, 0), (1, 5)])],
291        );
292    }
293
294    // ── [Point, Polygon, Point] ───────────────────────────────────────────────
295
296    #[test]
297    fn point_polygon_point_id_sort_roundtrip() {
298        assert_sort_roundtrip(
299            &[pt(0, 0), poly_square(10, 10, 5), pt(5, 5)],
300            &[2, 1, 3],
301            SortStrategy::Id,
302            &[poly_square(10, 10, 5), pt(0, 0), pt(5, 5)],
303        );
304    }
305
306    // ── spatial Morton sort ───────────────────────────────────────────────────
307
308    #[test]
309    fn point_line_point_morton_sort_roundtrip() {
310        assert_sort_roundtrip(
311            &[pt(2, 0), ls(&[(0, 0), (0, 5)]), pt(1, 0)],
312            &[1, 2, 3],
313            SortStrategy::SpatialMorton,
314            &[ls(&[(0, 0), (0, 5)]), pt(1, 0), pt(2, 0)],
315        );
316    }
317
318    // ── already-sorted is identity ────────────────────────────────────────────
319
320    #[test]
321    fn id_sort_already_sorted_is_identity_roundtrip() {
322        let geoms = &[pt(0, 0), ls(&[(1, 0), (1, 5)]), pt(5, 5)];
323        assert_sort_roundtrip(geoms, &[1, 2, 3], SortStrategy::Id, geoms);
324    }
325
326    // ── ID column co-permuted with geometry ───────────────────────────────────
327
328    #[test]
329    fn id_column_co_permuted_with_geometry() {
330        let layer = layer_after_sort(
331            &[pt(0, 0), ls(&[(1, 0), (1, 5)]), pt(5, 5)],
332            &[3, 1, 2],
333            SortStrategy::Id,
334        );
335
336        let ids: Vec<Option<u64>> = layer.features().iter().map(TileFeature::id).collect();
337        assert_eq!(ids, vec![Some(1u64), Some(2), Some(3)]);
338
339        // Verify geometry types match expected order
340        let geom_types: Vec<&str> = layer
341            .features()
342            .iter()
343            .map(|f| GeometryType::try_from(f.geometry()).unwrap().into())
344            .collect();
345        assert_eq!(geom_types, vec!["LineString", "Point", "Point"]);
346    }
347
348    /// Build row-oriented tile layer from geometries and IDs (one feature per geometry).
349    fn build_tile_layer(geoms: &[Geometry<i32>], ids: &[Option<u64>]) -> TileLayer {
350        assert_eq!(geoms.len(), ids.len());
351        TileLayer::from_parts(
352            "test",
353            4096,
354            vec![],
355            geoms
356                .iter()
357                .zip(ids.iter())
358                .map(|(g, &id)| TileFeature {
359                    id,
360                    geometry: g.clone(),
361                    properties: vec![],
362                    #[cfg(feature = "unstable-v2")]
363                    m_values: vec![],
364                    #[cfg(feature = "unstable-v2")]
365                    nested: vec![],
366                    #[cfg(feature = "unstable-v2")]
367                    z: Vec::new(),
368                })
369                .collect(),
370        )
371        .unwrap()
372    }
373
374    /// Encode the layer with a given sort strategy, decode it back, and return the `TileLayer`.
375    /// This tests the full encode->decode roundtrip, verifying that sorting was applied.
376    fn sort_encode_decode(tile: &TileLayer, sort: SortStrategy) -> TileLayer {
377        let enc_cfg = EncoderConfig::default();
378        let enc = Encoder::with_explicit(enc_cfg, ExplicitEncoder::for_id(IntEncoder::varint()));
379        let mut codecs = Codecs::default();
380        let enc = stage_tile(tile, sort, false, enc_cfg.tessellate())
381            .encode_into(enc, &mut codecs)
382            .expect("encode failed");
383
384        // Serialize to bytes and reparse to get a `Layer01`.
385        let buf = enc.into_layer_bytes().expect("into_layer_bytes failed");
386
387        let mut p = parser();
388        let layer_back = assert_empty(Layer::from_bytes(&buf, &mut p));
389        assert!(p.reserved() > 0, "parser should reserve bytes after parse");
390
391        let layer01 = into_layer01(layer_back);
392
393        let mut d = dec();
394        let tile = layer01.into_tile(&mut d).expect("decode after sort failed");
395        assert!(
396            d.consumed() > 0,
397            "decoder should consume bytes after decode"
398        );
399        tile
400    }
401
402    fn encode_decode(tile: &TileLayer, cfg: EncoderConfig) -> TileLayer {
403        let buf = tile.encode(cfg).expect("encode failed");
404        let layer = assert_empty(Layer::from_bytes(&buf, &mut parser()));
405        into_layer01(layer)
406            .into_tile(&mut dec())
407            .expect("decode failed")
408    }
409
410    fn scattered_points(count: u32) -> Vec<Geometry<i32>> {
411        (0..count)
412            .map(|i| {
413                let x = i32::try_from(i.wrapping_mul(2_654_435_761) % 4096).unwrap();
414                let y = i32::try_from(i.wrapping_mul(40_503) % 4096).unwrap();
415                pt(x, y)
416            })
417            .collect()
418    }
419
420    #[test]
421    fn default_config_keeps_the_source_order() {
422        let geoms = scattered_points(600);
423        let ids: Vec<Option<u64>> = (0..600).rev().map(Some).collect();
424        let decoded = encode_decode(&build_tile_layer(&geoms, &ids), EncoderConfig::default());
425
426        let decoded_ids: Vec<Option<u64>> =
427            decoded.features().iter().map(TileFeature::id).collect();
428        assert_eq!(decoded_ids, ids);
429    }
430
431    #[test]
432    fn hilbert_sort_reaches_a_layer_spread_over_the_whole_extent() {
433        let geoms = scattered_points(600);
434        let ids = vec![None; geoms.len()];
435        let mut expected = build_tile_layer(&geoms, &ids);
436        let params = expected.curve_params();
437        expected.sort(SortStrategy::SpatialHilbert, params);
438
439        let decoded = encode_decode(
440            &build_tile_layer(&geoms, &ids),
441            EncoderConfig::default().with_spatial_hilbert_sort(true),
442        );
443
444        assert_eq!(
445            vertices_from_source(&decoded),
446            vertices_from_source(&expected)
447        );
448    }
449
450    /// Rebuild a flat vertex buffer from the feature geometries in source order.
451    fn vertices_from_source(source: &TileLayer) -> Vec<i32> {
452        let mut geom = GeometryValues::default();
453        for f in source.features() {
454            geom.push_geom(f.geometry());
455        }
456        geom.vertices().unwrap_or_default().to_vec()
457    }
458
459    #[test]
460    fn test_shared_morton_shift() {
461        // P1 at (0, -10), P2 at (-10, 0).
462        // With shared shift = 10:
463        // P1 shifted: (10, 0) -> interleave(10, 0) = 68
464        // P2 shifted: (0, 10) -> interleave(0, 10) = 136
465        // P1 (key 68) < P2 (key 136), so expected order: [P1(0,-10), P2(-10,0)].
466
467        let tile = build_tile_layer(&[pt(0, -10), pt(-10, 0)], &[Some(1), Some(2)]);
468        let source = sort_encode_decode(&tile, SortStrategy::SpatialMorton);
469
470        let verts = vertices_from_source(&source);
471        assert_eq!(verts, vec![0, -10, -10, 0]);
472    }
473
474    #[test]
475    fn test_id_sort_nulls_first() {
476        let tile = build_tile_layer(&[pt(2, 2), pt(1, 1), pt(0, 0)], &[Some(10), None, Some(5)]);
477        let source = sort_encode_decode(&tile, SortStrategy::Id);
478
479        let ids: Vec<Option<u64>> = source.features().iter().map(TileFeature::id).collect();
480        // Expected order: [None, Some(5), Some(10)]
481        assert_eq!(ids, vec![None, Some(5), Some(10)]);
482
483        let verts = vertices_from_source(&source);
484        // Corresponding verts: [pt(1,1), pt(0,0), pt(2,2)] -> [1,1, 0,0, 2,2]
485        assert_eq!(verts, vec![1, 1, 0, 0, 2, 2]);
486    }
487
488    #[test]
489    fn test_mixed_geometry_morton_sort() {
490        // [Point(2,0), LineString(0,0 -> 0,5), Point(1,0)]
491        // Morton keys (assuming shift 0):
492        // P1(2,0) -> 4
493        // LS(0,0) -> 0
494        // P2(1,0) -> 1
495        // Expected order: [LS, P2, P1]
496
497        let tile = build_tile_layer(
498            &[pt(2, 0), ls(&[(0, 0), (0, 5)]), pt(1, 0)],
499            &[Some(1), Some(2), Some(3)],
500        );
501        let source = sort_encode_decode(&tile, SortStrategy::SpatialMorton);
502
503        let types: Vec<_> = source
504            .features()
505            .iter()
506            .map(|f| GeometryType::try_from(f.geometry()).unwrap())
507            .collect();
508
509        assert_eq!(
510            types,
511            vec![
512                GeometryType::LineString,
513                GeometryType::Point,
514                GeometryType::Point
515            ]
516        );
517
518        let verts = vertices_from_source(&source);
519        // Expected vertices: LS(0,0,0,5), P2(1,0), P1(2,0)
520        assert_eq!(verts, vec![0, 0, 0, 5, 1, 0, 2, 0]);
521    }
522
523    #[test]
524    fn id_sort_separates_the_two_largest_ids() {
525        let layer = layer_after_sort(
526            &[pt(0, 0), pt(1, 1)],
527            &[u64::MAX, u64::MAX - 1],
528            SortStrategy::Id,
529        );
530
531        let ids: Vec<Option<u64>> = layer.features().iter().map(TileFeature::id).collect();
532        assert_eq!(ids, vec![Some(u64::MAX - 1), Some(u64::MAX)]);
533    }
534
535    #[test]
536    fn id_sort_puts_the_missing_id_before_zero() {
537        let layer = build_tile_layer(&[pt(0, 0), pt(1, 1)], &[Some(0), None]);
538        let mut layer = layer;
539        layer.sort(SortStrategy::Id, CurveParams { shift: 0, bits: 1 });
540
541        let ids: Vec<Option<u64>> = layer.features().iter().map(TileFeature::id).collect();
542        assert_eq!(ids, vec![None, Some(0)]);
543    }
544
545    #[test]
546    fn spatial_sort_reads_the_first_vertex_of_every_geometry_kind() {
547        let at = |k: i32| Coord { x: k, y: k };
548        let geoms = [
549            GeoGeom::Rect(Rect::new(at(0), at(20))),
550            GeoGeom::Triangle(Triangle::new(at(1), at(11), at(21))),
551            GeoGeom::MultiPolygon(MultiPolygon(vec![Polygon::new(
552                LineString(vec![at(2), at(12), at(22), at(2)]),
553                vec![],
554            )])),
555            GeoGeom::MultiLineString(MultiLineString(vec![LineString(vec![at(3), at(13)])])),
556            GeoGeom::MultiPoint(MultiPoint(vec![Point::from(at(4)), Point::from(at(14))])),
557            GeoGeom::Polygon(Polygon::new(
558                LineString(vec![at(5), at(15), at(25), at(5)]),
559                vec![],
560            )),
561            GeoGeom::LineString(LineString(vec![at(6), at(16)])),
562            GeoGeom::Line(Line::new(at(7), at(17))),
563            GeoGeom::Point(Point::from(at(8))),
564            GeoGeom::GeometryCollection(GeometryCollection(vec![GeoGeom::Point(Point::from(at(
565                9,
566            )))])),
567            GeoGeom::LineString(LineString(vec![])),
568        ];
569        let ids: Vec<Option<u64>> = (0..u64::try_from(geoms.len()).unwrap()).map(Some).collect();
570
571        let mut layer = build_tile_layer(&geoms, &ids);
572        let params = layer.curve_params();
573        // Morton keys interleave the bits of x and y
574        // This means the diagonal they rise with the coordinate and the sorted order is the vertex order.
575        layer.sort(SortStrategy::SpatialMorton, params);
576
577        let sorted: Vec<Option<u64>> = layer.features().iter().map(TileFeature::id).collect();
578        assert_eq!(sorted, ids);
579    }
580
581    #[test]
582    fn curve_params_of_a_vertexless_layer_is_the_degenerate_grid() {
583        let layer = build_tile_layer(&[GeoGeom::LineString(LineString(vec![]))], &[None]);
584        assert_eq!(layer.curve_params(), CurveParams { shift: 0, bits: 1 });
585    }
586
587    #[rstest]
588    #[case::morton(SortStrategy::SpatialMorton, &[0, 1, 2, 3])]
589    #[case::hilbert(SortStrategy::SpatialHilbert, &[0, 2, 3, 1])]
590    fn spatial_sort_of_the_unit_square_follows_the_chosen_curve(
591        #[case] strategy: SortStrategy,
592        #[case] expected: &[u64],
593    ) {
594        let geoms = [pt(0, 0), pt(1, 0), pt(0, 1), pt(1, 1)];
595        let ids: Vec<Option<u64>> = (0..4).map(Some).collect();
596
597        let mut layer = build_tile_layer(&geoms, &ids);
598        let params = layer.curve_params();
599        assert_eq!(params, CurveParams { shift: 0, bits: 1 });
600        layer.sort(strategy, params);
601
602        let sorted: Vec<Option<u64>> = layer.features().iter().map(TileFeature::id).collect();
603        assert_eq!(
604            sorted,
605            expected.iter().copied().map(Some).collect::<Vec<_>>()
606        );
607    }
608
609    #[test]
610    fn unsorted_leaves_the_feature_order_alone() {
611        let geoms = [pt(9, 9), pt(0, 0), pt(5, 5)];
612        let ids = [Some(3u64), Some(1), Some(2)];
613
614        let mut layer = build_tile_layer(&geoms, &ids);
615        let params = layer.curve_params();
616        layer.sort(SortStrategy::Unsorted, params);
617
618        let sorted: Vec<Option<u64>> = layer.features().iter().map(TileFeature::id).collect();
619        assert_eq!(sorted, ids.to_vec());
620    }
621
622    #[test]
623    fn every_vertexless_geometry_kind_sorts_after_the_one_feature_with_a_vertex() {
624        let empty_ring = || Polygon::new(LineString(vec![]), vec![]);
625        let geoms = [
626            GeoGeom::LineString(LineString(vec![])),
627            GeoGeom::Polygon(empty_ring()),
628            GeoGeom::MultiPoint(MultiPoint(vec![])),
629            GeoGeom::MultiLineString(MultiLineString(vec![])),
630            GeoGeom::MultiLineString(MultiLineString(vec![LineString(vec![])])),
631            GeoGeom::MultiPolygon(MultiPolygon(vec![])),
632            GeoGeom::MultiPolygon(MultiPolygon(vec![empty_ring()])),
633            GeoGeom::GeometryCollection(GeometryCollection(vec![])),
634            GeoGeom::GeometryCollection(GeometryCollection(vec![GeoGeom::LineString(LineString(
635                vec![],
636            ))])),
637            pt(1, 1),
638        ];
639        let ids: Vec<Option<u64>> = (0..u64::try_from(geoms.len()).unwrap()).map(Some).collect();
640
641        let mut layer = build_tile_layer(&geoms, &ids);
642        let params = layer.curve_params();
643        layer.sort(SortStrategy::SpatialMorton, params);
644
645        let sorted: Vec<Option<u64>> = layer.features().iter().map(TileFeature::id).collect();
646        assert_eq!(
647            sorted,
648            vec![
649                Some(9),
650                Some(0),
651                Some(1),
652                Some(2),
653                Some(3),
654                Some(4),
655                Some(5),
656                Some(6),
657                Some(7),
658                Some(8)
659            ]
660        );
661    }
662}