1use geo::CoordsIter as _;
4use geo_types::{Coord, Geometry};
5
6use crate::codecs::hilbert::{hilbert_curve_params_from_bounds, hilbert_sort_key};
7use crate::codecs::morton::morton_sort_key;
8use crate::encoder::model::CurveParams;
9use crate::tile::{TileFeature, TileLayer};
10
11#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, strum::EnumIter, strum::EnumCount)]
17pub enum SortStrategy {
18 #[default]
22 Unsorted,
23
24 SpatialMorton,
31
32 SpatialHilbert,
36
37 Id,
39}
40
41impl TileLayer {
42 #[hotpath::measure]
52 pub fn sort(&mut self, strategy: SortStrategy, params: CurveParams) {
53 match strategy {
54 SortStrategy::SpatialMorton | SortStrategy::SpatialHilbert => {
55 let curve_key = if let SortStrategy::SpatialMorton = strategy {
56 morton_sort_key
57 } else {
58 hilbert_sort_key
59 };
60 self.features_mut().sort_by_cached_key(|f| {
61 first_vertex(f.geometry()).map_or(u64::MAX, |c| u64::from(curve_key(c, params)))
62 });
63 }
64 SortStrategy::Id => {
65 self.features_mut().sort_by_cached_key(TileFeature::id);
66 }
67 SortStrategy::Unsorted => {
68 }
70 }
71 }
72
73 #[hotpath::measure]
77 #[must_use]
78 pub fn curve_params(&self) -> CurveParams {
79 let (min_val, max_val) = self
80 .features()
81 .iter()
82 .flat_map(|f| f.geometry().coords_iter())
83 .fold((i32::MAX, i32::MIN), |(min, max), c| {
84 (min.min(c.x).min(c.y), max.max(c.x).max(c.y))
85 });
86 hilbert_curve_params_from_bounds(min_val, max_val)
87 }
88}
89
90fn first_vertex(geom: &Geometry<i32>) -> Option<Coord<i32>> {
92 match geom {
93 Geometry::<i32>::Point(p) => Some(p.0),
94 Geometry::<i32>::Line(l) => Some(l.start),
95 Geometry::<i32>::LineString(ls) => ls.0.first().copied(),
96 Geometry::<i32>::Polygon(p) => p.exterior().0.first().copied(),
97 Geometry::<i32>::MultiPoint(mp) => mp.0.first().map(|p| p.0),
98 Geometry::<i32>::MultiLineString(mls) => mls.0.first().and_then(|ls| ls.0.first().copied()),
99 Geometry::<i32>::MultiPolygon(mp) => {
100 mp.0.first().and_then(|p| p.exterior().0.first().copied())
101 }
102 Geometry::<i32>::Triangle(t) => Some(t.v1()),
103 Geometry::<i32>::Rect(r) => Some(r.min()),
104 Geometry::<i32>::GeometryCollection(gc) => gc.0.first().and_then(first_vertex),
105 }
106}
107
108pub(crate) fn spatial_sort_likely_to_help(layer: &TileLayer) -> bool {
115 const SPATIAL_HELP_COVERAGE: f64 = 0.8;
116
117 let extent = f64::from(layer.extent().get());
118 if extent <= 0.0 || layer.features().is_empty() {
119 return true;
120 }
121
122 let (min_x, max_x, min_y, max_y) = layer
123 .features()
124 .iter()
125 .filter_map(|f| first_vertex(f.geometry()))
126 .fold(
127 (i32::MAX, i32::MIN, i32::MAX, i32::MIN),
128 |(min_x, max_x, min_y, max_y), Coord::<i32> { x, y }| {
129 (min_x.min(x), max_x.max(x), min_y.min(y), max_y.max(y))
130 },
131 );
132
133 if min_x > max_x || min_y > max_y {
134 return true;
135 }
136
137 let range_x = f64::from(max_x) - f64::from(min_x);
138 let range_y = f64::from(max_y) - f64::from(min_y);
139
140 let spread_x = range_x / extent;
141 let spread_y = range_y / extent;
142
143 !(spread_x > SPATIAL_HELP_COVERAGE && spread_y > SPATIAL_HELP_COVERAGE)
144}
145
146#[cfg(test)]
147mod tests {
148 use geo_types::{
149 Coord, Geometry as GeoGeom, Geometry, GeometryCollection, Line, LineString,
150 MultiLineString, MultiPoint, MultiPolygon, Point, Polygon, Rect, Triangle,
151 };
152 use rstest::rstest;
153
154 use super::spatial_sort_likely_to_help;
155 use crate::decoder::{GeometryType, GeometryValues, RawGeometry};
156 use crate::encoder::model::CurveParams;
157 use crate::encoder::{
158 Codecs, Encoder, EncoderConfig, ExplicitEncoder, IntEncoder, SortStrategy, stage_tile,
159 };
160 use crate::test_helpers::{assert_empty, dec, into_layer01, parser};
161 use crate::tile::{TileFeature, TileLayer};
162 use crate::{Layer, LazyParsed};
163
164 fn pt(x: i32, y: i32) -> Geometry<i32> {
165 GeoGeom::Point(Point::new(x, y))
166 }
167
168 fn ls(coords: &[(i32, i32)]) -> Geometry<i32> {
169 GeoGeom::LineString(LineString::new(
170 coords.iter().map(|&(x, y)| Coord { x, y }).collect(),
171 ))
172 }
173
174 fn poly_square(x0: i32, y0: i32, side: i32) -> Geometry<i32> {
175 let ring = LineString::new(vec![
176 Coord { x: x0, y: y0 },
177 Coord {
178 x: x0 + side,
179 y: y0,
180 },
181 Coord {
182 x: x0 + side,
183 y: y0 + side,
184 },
185 Coord {
186 x: x0,
187 y: y0 + side,
188 },
189 Coord { x: x0, y: y0 },
190 ]);
191 GeoGeom::Polygon(Polygon::new(ring, vec![]))
192 }
193
194 fn roundtrip_geom(decoded: &GeometryValues) -> GeometryValues {
196 let mut enc = Encoder::default();
197 let mut codecs = Codecs::default();
198 decoded
199 .clone()
200 .write_to(&mut enc, &mut codecs)
201 .expect("encode failed");
202 let buf = enc.data().to_vec();
203
204 let parsed = assert_empty(RawGeometry::from_bytes(&buf, &mut parser()));
205 let mut d = dec();
206 let result = LazyParsed::Raw(parsed)
207 .into_parsed(&mut d)
208 .expect("decode failed");
209 assert!(
210 d.consumed() > 0,
211 "decoder should consume bytes after decode"
212 );
213 result
214 }
215
216 fn canonical(geoms: &[Geometry<i32>]) -> GeometryValues {
218 let mut decoded = GeometryValues::default();
219 for g in geoms {
220 decoded.push_geom(g);
221 }
222 roundtrip_geom(&decoded)
223 }
224
225 fn layer_after_sort(geoms: &[Geometry<i32>], ids: &[u64], strategy: SortStrategy) -> TileLayer {
228 let features: Vec<TileFeature> = geoms
229 .iter()
230 .zip(ids.iter())
231 .map(|(g, &id)| TileFeature {
232 id: Some(id),
233 geometry: g.clone(),
234 properties: vec![],
235 #[cfg(feature = "unstable-v2")]
236 m_values: vec![],
237 #[cfg(feature = "unstable-v2")]
238 nested: vec![],
239 #[cfg(feature = "unstable-v2")]
240 z: Vec::new(),
241 })
242 .collect();
243
244 let mut layer = TileLayer::from_parts("test", 4096, vec![], features).unwrap();
245
246 let params = layer.curve_params();
247 layer.sort(strategy, params);
248 layer
249 }
250
251 fn assert_sort_roundtrip(
253 geoms: &[Geometry<i32>],
254 ids: &[u64],
255 strategy: SortStrategy,
256 expected: &[Geometry<i32>],
257 ) {
258 let layer = layer_after_sort(geoms, ids, strategy);
259
260 let mut sorted_decoded = GeometryValues::default();
261 for f in layer.features() {
262 sorted_decoded.push_geom(f.geometry());
263 }
264
265 let after_roundtrip = roundtrip_geom(&sorted_decoded);
266 let expected_canonical = canonical(expected);
267
268 assert_eq!(
269 after_roundtrip, expected_canonical,
270 "\nsorted geometry did not match expected after encode->decode round-trip\
271 \nvector_types after sort: {:?}\
272 \nvector_types expected: {:?}",
273 sorted_decoded.vector_types, expected_canonical.vector_types,
274 );
275 }
276
277 #[test]
280 fn pure_points_id_sort_roundtrip() {
281 assert_sort_roundtrip(
282 &[pt(0, 0), pt(1, 1), pt(2, 2)],
283 &[3, 2, 1],
284 SortStrategy::Id,
285 &[pt(2, 2), pt(1, 1), pt(0, 0)],
286 );
287 }
288
289 #[test]
292 fn pure_linestrings_id_sort_roundtrip() {
293 assert_sort_roundtrip(
294 &[ls(&[(0, 0), (0, 10)]), ls(&[(5, 5), (10, 10)])],
295 &[2, 1],
296 SortStrategy::Id,
297 &[ls(&[(5, 5), (10, 10)]), ls(&[(0, 0), (0, 10)])],
298 );
299 }
300
301 #[test]
304 fn point_line_point_id_sort_to_line_point_point_roundtrip() {
305 assert_sort_roundtrip(
306 &[pt(0, 0), ls(&[(1, 0), (1, 5)]), pt(5, 5)],
307 &[3, 1, 2],
308 SortStrategy::Id,
309 &[ls(&[(1, 0), (1, 5)]), pt(5, 5), pt(0, 0)],
310 );
311 }
312
313 #[test]
314 fn point_line_point_id_sort_to_point_point_line_roundtrip() {
315 assert_sort_roundtrip(
316 &[pt(0, 0), ls(&[(1, 0), (1, 5)]), pt(5, 5)],
317 &[1, 3, 2],
318 SortStrategy::Id,
319 &[pt(0, 0), pt(5, 5), ls(&[(1, 0), (1, 5)])],
320 );
321 }
322
323 #[test]
326 fn point_polygon_point_id_sort_roundtrip() {
327 assert_sort_roundtrip(
328 &[pt(0, 0), poly_square(10, 10, 5), pt(5, 5)],
329 &[2, 1, 3],
330 SortStrategy::Id,
331 &[poly_square(10, 10, 5), pt(0, 0), pt(5, 5)],
332 );
333 }
334
335 #[test]
338 fn point_line_point_morton_sort_roundtrip() {
339 assert_sort_roundtrip(
340 &[pt(2, 0), ls(&[(0, 0), (0, 5)]), pt(1, 0)],
341 &[1, 2, 3],
342 SortStrategy::SpatialMorton,
343 &[ls(&[(0, 0), (0, 5)]), pt(1, 0), pt(2, 0)],
344 );
345 }
346
347 #[test]
350 fn id_sort_already_sorted_is_identity_roundtrip() {
351 let geoms = &[pt(0, 0), ls(&[(1, 0), (1, 5)]), pt(5, 5)];
352 assert_sort_roundtrip(geoms, &[1, 2, 3], SortStrategy::Id, geoms);
353 }
354
355 #[test]
358 fn id_column_co_permuted_with_geometry() {
359 let layer = layer_after_sort(
360 &[pt(0, 0), ls(&[(1, 0), (1, 5)]), pt(5, 5)],
361 &[3, 1, 2],
362 SortStrategy::Id,
363 );
364
365 let ids: Vec<Option<u64>> = layer.features().iter().map(TileFeature::id).collect();
366 assert_eq!(ids, vec![Some(1u64), Some(2), Some(3)]);
367
368 let geom_types: Vec<&str> = layer
370 .features()
371 .iter()
372 .map(|f| GeometryType::try_from(f.geometry()).unwrap().into())
373 .collect();
374 assert_eq!(geom_types, vec!["LineString", "Point", "Point"]);
375 }
376
377 fn build_tile_layer(geoms: &[Geometry<i32>], ids: &[Option<u64>]) -> TileLayer {
379 assert_eq!(geoms.len(), ids.len());
380 TileLayer::from_parts(
381 "test",
382 4096,
383 vec![],
384 geoms
385 .iter()
386 .zip(ids.iter())
387 .map(|(g, &id)| TileFeature {
388 id,
389 geometry: g.clone(),
390 properties: vec![],
391 #[cfg(feature = "unstable-v2")]
392 m_values: vec![],
393 #[cfg(feature = "unstable-v2")]
394 nested: vec![],
395 #[cfg(feature = "unstable-v2")]
396 z: Vec::new(),
397 })
398 .collect(),
399 )
400 .unwrap()
401 }
402
403 fn sort_encode_decode(tile: TileLayer, sort: SortStrategy) -> TileLayer {
406 let enc_cfg = EncoderConfig::default();
407 let enc = Encoder::with_explicit(enc_cfg, ExplicitEncoder::for_id(IntEncoder::varint()));
408 let mut codecs = Codecs::default();
409 let enc = stage_tile(tile, sort, false, enc_cfg.tessellate())
410 .encode_into(enc, &mut codecs)
411 .expect("encode failed");
412
413 let buf = enc.into_layer_bytes().expect("into_layer_bytes failed");
415
416 let mut p = parser();
417 let layer_back = assert_empty(Layer::from_bytes(&buf, &mut p));
418 assert!(p.reserved() > 0, "parser should reserve bytes after parse");
419
420 let layer01 = into_layer01(layer_back);
421
422 let mut d = dec();
423 let tile = layer01.into_tile(&mut d).expect("decode after sort failed");
424 assert!(
425 d.consumed() > 0,
426 "decoder should consume bytes after decode"
427 );
428 tile
429 }
430
431 fn vertices_from_source(source: &TileLayer) -> Vec<i32> {
433 let mut geom = GeometryValues::default();
434 for f in source.features() {
435 geom.push_geom(f.geometry());
436 }
437 geom.vertices().unwrap_or_default().to_vec()
438 }
439
440 #[test]
441 fn test_shared_morton_shift() {
442 let tile = build_tile_layer(&[pt(0, -10), pt(-10, 0)], &[Some(1), Some(2)]);
449 let source = sort_encode_decode(tile, SortStrategy::SpatialMorton);
450
451 let verts = vertices_from_source(&source);
452 assert_eq!(verts, vec![0, -10, -10, 0]);
453 }
454
455 #[test]
456 fn test_id_sort_nulls_first() {
457 let tile = build_tile_layer(&[pt(2, 2), pt(1, 1), pt(0, 0)], &[Some(10), None, Some(5)]);
458 let source = sort_encode_decode(tile, SortStrategy::Id);
459
460 let ids: Vec<Option<u64>> = source.features().iter().map(TileFeature::id).collect();
461 assert_eq!(ids, vec![None, Some(5), Some(10)]);
463
464 let verts = vertices_from_source(&source);
465 assert_eq!(verts, vec![1, 1, 0, 0, 2, 2]);
467 }
468
469 #[test]
470 fn test_mixed_geometry_morton_sort() {
471 let tile = build_tile_layer(
479 &[pt(2, 0), ls(&[(0, 0), (0, 5)]), pt(1, 0)],
480 &[Some(1), Some(2), Some(3)],
481 );
482 let source = sort_encode_decode(tile, SortStrategy::SpatialMorton);
483
484 let types: Vec<_> = source
485 .features()
486 .iter()
487 .map(|f| GeometryType::try_from(f.geometry()).unwrap())
488 .collect();
489
490 assert_eq!(
491 types,
492 vec![
493 GeometryType::LineString,
494 GeometryType::Point,
495 GeometryType::Point
496 ]
497 );
498
499 let verts = vertices_from_source(&source);
500 assert_eq!(verts, vec![0, 0, 0, 5, 1, 0, 2, 0]);
502 }
503
504 #[test]
505 fn id_sort_separates_the_two_largest_ids() {
506 let layer = layer_after_sort(
507 &[pt(0, 0), pt(1, 1)],
508 &[u64::MAX, u64::MAX - 1],
509 SortStrategy::Id,
510 );
511
512 let ids: Vec<Option<u64>> = layer.features().iter().map(TileFeature::id).collect();
513 assert_eq!(ids, vec![Some(u64::MAX - 1), Some(u64::MAX)]);
514 }
515
516 #[test]
517 fn id_sort_puts_the_missing_id_before_zero() {
518 let layer = build_tile_layer(&[pt(0, 0), pt(1, 1)], &[Some(0), None]);
519 let mut layer = layer;
520 layer.sort(SortStrategy::Id, CurveParams { shift: 0, bits: 1 });
521
522 let ids: Vec<Option<u64>> = layer.features().iter().map(TileFeature::id).collect();
523 assert_eq!(ids, vec![None, Some(0)]);
524 }
525
526 #[test]
527 fn spatial_sort_reads_the_first_vertex_of_every_geometry_kind() {
528 let at = |k: i32| Coord { x: k, y: k };
529 let geoms = [
530 GeoGeom::Rect(Rect::new(at(0), at(20))),
531 GeoGeom::Triangle(Triangle::new(at(1), at(11), at(21))),
532 GeoGeom::MultiPolygon(MultiPolygon(vec![Polygon::new(
533 LineString(vec![at(2), at(12), at(22), at(2)]),
534 vec![],
535 )])),
536 GeoGeom::MultiLineString(MultiLineString(vec![LineString(vec![at(3), at(13)])])),
537 GeoGeom::MultiPoint(MultiPoint(vec![Point::from(at(4)), Point::from(at(14))])),
538 GeoGeom::Polygon(Polygon::new(
539 LineString(vec![at(5), at(15), at(25), at(5)]),
540 vec![],
541 )),
542 GeoGeom::LineString(LineString(vec![at(6), at(16)])),
543 GeoGeom::Line(Line::new(at(7), at(17))),
544 GeoGeom::Point(Point::from(at(8))),
545 GeoGeom::GeometryCollection(GeometryCollection(vec![GeoGeom::Point(Point::from(at(
546 9,
547 )))])),
548 GeoGeom::LineString(LineString(vec![])),
549 ];
550 let ids: Vec<Option<u64>> = (0..u64::try_from(geoms.len()).unwrap()).map(Some).collect();
551
552 let mut layer = build_tile_layer(&geoms, &ids);
553 let params = layer.curve_params();
554 layer.sort(SortStrategy::SpatialMorton, params);
557
558 let sorted: Vec<Option<u64>> = layer.features().iter().map(TileFeature::id).collect();
559 assert_eq!(sorted, ids);
560 }
561
562 fn points_help_spatial_sort(coords: &[(i32, i32)]) -> bool {
563 let geoms: Vec<Geometry<i32>> = coords.iter().map(|&(x, y)| pt(x, y)).collect();
564 let ids = vec![None; geoms.len()];
565 spatial_sort_likely_to_help(&build_tile_layer(&geoms, &ids))
566 }
567
568 #[rstest]
569 #[case::no_features(&[], true)]
570 #[case::one_feature(&[(100, 100)], true)]
571 #[case::clustered(&[(0, 0), (100, 100)], true)]
572 #[case::spread_on_x_only(&[(0, 0), (4000, 100)], true)]
573 #[case::spread_on_both_axes(&[(0, 0), (4000, 4000)], false)]
574 #[case::spread_on_y_only(&[(0, 0), (100, 4000)], true)]
575 #[case::exactly_at_the_coverage_limit(&[(0, 0), (3276, 3276)], true)]
576 #[case::just_past_the_coverage_limit(&[(0, 0), (3277, 3277)], false)]
577 #[case::wider_than_the_extent(&[(-9000, -9000), (9000, 9000)], false)]
578 #[case::wider_than_i32(&[(i32::MIN, i32::MIN), (i32::MAX, i32::MAX)], false)]
579 fn spatial_sort_help_heuristic(#[case] coords: &[(i32, i32)], #[case] expected: bool) {
580 assert_eq!(points_help_spatial_sort(coords), expected);
581 }
582
583 #[test]
584 fn vertexless_features_keep_the_spatial_sort() {
585 let geoms = [GeoGeom::LineString(LineString(vec![]))];
586 let layer = build_tile_layer(&geoms, &[None]);
587 assert!(spatial_sort_likely_to_help(&layer));
588 }
589
590 #[test]
591 fn curve_params_of_a_vertexless_layer_is_the_degenerate_grid() {
592 let layer = build_tile_layer(&[GeoGeom::LineString(LineString(vec![]))], &[None]);
593 assert_eq!(layer.curve_params(), CurveParams { shift: 0, bits: 1 });
594 }
595
596 #[rstest]
597 #[case::morton(SortStrategy::SpatialMorton, &[0, 1, 2, 3])]
598 #[case::hilbert(SortStrategy::SpatialHilbert, &[0, 2, 3, 1])]
599 fn spatial_sort_of_the_unit_square_follows_the_chosen_curve(
600 #[case] strategy: SortStrategy,
601 #[case] expected: &[u64],
602 ) {
603 let geoms = [pt(0, 0), pt(1, 0), pt(0, 1), pt(1, 1)];
604 let ids: Vec<Option<u64>> = (0..4).map(Some).collect();
605
606 let mut layer = build_tile_layer(&geoms, &ids);
607 let params = layer.curve_params();
608 assert_eq!(params, CurveParams { shift: 0, bits: 1 });
609 layer.sort(strategy, params);
610
611 let sorted: Vec<Option<u64>> = layer.features().iter().map(TileFeature::id).collect();
612 assert_eq!(
613 sorted,
614 expected.iter().copied().map(Some).collect::<Vec<_>>()
615 );
616 }
617
618 #[test]
619 fn unsorted_leaves_the_feature_order_alone() {
620 let geoms = [pt(9, 9), pt(0, 0), pt(5, 5)];
621 let ids = [Some(3u64), Some(1), Some(2)];
622
623 let mut layer = build_tile_layer(&geoms, &ids);
624 let params = layer.curve_params();
625 layer.sort(SortStrategy::Unsorted, params);
626
627 let sorted: Vec<Option<u64>> = layer.features().iter().map(TileFeature::id).collect();
628 assert_eq!(sorted, ids.to_vec());
629 }
630
631 #[test]
632 fn every_vertexless_geometry_kind_sorts_after_the_one_feature_with_a_vertex() {
633 let empty_ring = || Polygon::new(LineString(vec![]), vec![]);
634 let geoms = [
635 GeoGeom::LineString(LineString(vec![])),
636 GeoGeom::Polygon(empty_ring()),
637 GeoGeom::MultiPoint(MultiPoint(vec![])),
638 GeoGeom::MultiLineString(MultiLineString(vec![])),
639 GeoGeom::MultiLineString(MultiLineString(vec![LineString(vec![])])),
640 GeoGeom::MultiPolygon(MultiPolygon(vec![])),
641 GeoGeom::MultiPolygon(MultiPolygon(vec![empty_ring()])),
642 GeoGeom::GeometryCollection(GeometryCollection(vec![])),
643 GeoGeom::GeometryCollection(GeometryCollection(vec![GeoGeom::LineString(LineString(
644 vec![],
645 ))])),
646 pt(1, 1),
647 ];
648 let ids: Vec<Option<u64>> = (0..u64::try_from(geoms.len()).unwrap()).map(Some).collect();
649
650 let mut layer = build_tile_layer(&geoms, &ids);
651 let params = layer.curve_params();
652 layer.sort(SortStrategy::SpatialMorton, params);
653
654 let sorted: Vec<Option<u64>> = layer.features().iter().map(TileFeature::id).collect();
655 assert_eq!(
656 sorted,
657 vec![
658 Some(9),
659 Some(0),
660 Some(1),
661 Some(2),
662 Some(3),
663 Some(4),
664 Some(5),
665 Some(6),
666 Some(7),
667 Some(8)
668 ]
669 );
670 }
671
672 #[test]
673 fn the_spatial_sort_heuristic_ignores_features_without_a_vertex() {
674 let geoms = [
675 pt(0, 0),
676 GeoGeom::LineString(LineString(vec![])),
677 pt(4000, 4000),
678 ];
679 let layer = build_tile_layer(&geoms, &[None, None, None]);
680 assert!(!spatial_sort_likely_to_help(&layer));
681 }
682}