1#[cfg(feature = "sort-coords-iter")]
4use geo::CoordsIter as _;
5
6#[cfg(feature = "sort-coords-iter")]
7use crate::codecs::hilbert::hilbert_curve_params_from_bounds;
8#[cfg(feature = "sort-coords-iter")]
9use crate::codecs::hilbert::hilbert_sort_key;
10#[cfg(feature = "sort-coords-iter")]
11use crate::codecs::morton::morton_sort_key;
12#[cfg(feature = "sort-coords-iter")]
13use crate::decoder::TileFeature;
14use crate::decoder::TileLayer01;
15#[cfg(feature = "sort-coords-iter")]
16use crate::geojson::Geom32;
17
18#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, strum::EnumIter, strum::EnumCount)]
24pub enum SortStrategy {
25 #[default]
29 Unsorted,
30
31 #[cfg(feature = "sort-coords-iter")]
39 SpatialMorton,
40
41 #[cfg(feature = "sort-coords-iter")]
47 SpatialHilbert,
48
49 Id,
51}
52
53impl TileLayer01 {
54 #[hotpath::measure]
59 pub fn sort(&mut self, strategy: SortStrategy) {
60 match strategy {
61 #[cfg(feature = "sort-coords-iter")]
62 SortStrategy::SpatialMorton | SortStrategy::SpatialHilbert => {
63 let params = curve_params_from_features(&self.features);
64 let curve_key = if let SortStrategy::SpatialMorton = strategy {
65 morton_sort_key
66 } else {
67 hilbert_sort_key
68 };
69 self.features.sort_by_cached_key(|f| {
70 first_vertex(&f.geometry).map_or(u64::MAX, |(x, y)| {
71 u64::from(curve_key(x, y, params.shift, params.num_bits))
72 })
73 });
74 }
75 SortStrategy::Id => {
76 self.features
77 .sort_by_cached_key(|f| f.id.map_or(0, |v| v.saturating_add(1)));
78 }
79 SortStrategy::Unsorted => {
80 }
82 }
83 }
84}
85
86#[cfg(feature = "sort-coords-iter")]
89struct CurveParams {
90 shift: u32,
91 num_bits: u32,
92}
93
94#[cfg(feature = "sort-coords-iter")]
97fn curve_params_from_features(features: &[TileFeature]) -> CurveParams {
98 let (min_val, max_val) = features
99 .iter()
100 .flat_map(|f| f.geometry.coords_iter())
101 .fold((i32::MAX, i32::MIN), |(min, max), c| {
102 (min.min(c.x).min(c.y), max.max(c.x).max(c.y))
103 });
104 let (shift, num_bits) = hilbert_curve_params_from_bounds(min_val, max_val);
105 CurveParams { shift, num_bits }
106}
107
108#[cfg(feature = "sort-coords-iter")]
110fn first_vertex(geom: &Geom32) -> Option<(i32, i32)> {
111 match geom {
112 Geom32::Point(p) => Some((p.0.x, p.0.y)),
113 Geom32::Line(l) => Some((l.start.x, l.start.y)),
114 Geom32::LineString(ls) => ls.0.first().map(|c| (c.x, c.y)),
115 Geom32::Polygon(p) => p.exterior().0.first().map(|c| (c.x, c.y)),
116 Geom32::MultiPoint(mp) => mp.0.first().map(|p| (p.0.x, p.0.y)),
117 Geom32::MultiLineString(mls) => mls
118 .0
119 .first()
120 .and_then(|ls| ls.0.first().map(|c| (c.x, c.y))),
121 Geom32::MultiPolygon(mp) => {
122 mp.0.first()
123 .and_then(|p| p.exterior().0.first().map(|c| (c.x, c.y)))
124 }
125 Geom32::Triangle(t) => Some((t.0.x, t.0.y)),
126 Geom32::Rect(r) => Some((r.min().x, r.min().y)),
127 Geom32::GeometryCollection(gc) => gc.0.first().and_then(first_vertex),
128 }
129}
130
131#[cfg(feature = "sort-coords-iter")]
138pub(crate) fn spatial_sort_likely_to_help(layer: &TileLayer01) -> bool {
139 const SPATIAL_HELP_COVERAGE: f64 = 0.8;
140
141 let extent = f64::from(layer.extent);
142 if extent <= 0.0 || layer.features.is_empty() {
143 return true;
144 }
145
146 let (min_x, max_x, min_y, max_y) = layer
147 .features
148 .iter()
149 .filter_map(|f| first_vertex(&f.geometry))
150 .fold(
151 (i32::MAX, i32::MIN, i32::MAX, i32::MIN),
152 |(min_x, max_x, min_y, max_y), (x, y)| {
153 (min_x.min(x), max_x.max(x), min_y.min(y), max_y.max(y))
154 },
155 );
156
157 if min_x > max_x || min_y > max_y {
158 return true;
159 }
160
161 let range_x = f64::from(max_x - min_x);
162 let range_y = f64::from(max_y - min_y);
163
164 let spread_x = range_x / extent;
165 let spread_y = range_y / extent;
166
167 !(spread_x > SPATIAL_HELP_COVERAGE && spread_y > SPATIAL_HELP_COVERAGE)
168}
169
170#[cfg(test)]
171mod tests {
172 use geo_types::{Coord, Geometry as GeoGeom, LineString, Point, Polygon};
173
174 use crate::decoder::{GeometryType, GeometryValues, RawGeometry, TileFeature, TileLayer01};
175 use crate::encoder::{
176 Encoder, ExplicitEncoder, IdWidth, IntEncoder, SortStrategy, StagedLayer01,
177 };
178 use crate::geojson::Geom32;
179 use crate::test_helpers::{assert_empty, dec, into_layer01, parser};
180 use crate::{Layer, LazyParsed};
181
182 fn pt(x: i32, y: i32) -> Geom32 {
185 GeoGeom::Point(Point::new(x, y))
186 }
187
188 fn ls(coords: &[(i32, i32)]) -> Geom32 {
189 GeoGeom::LineString(LineString::new(
190 coords.iter().map(|&(x, y)| Coord { x, y }).collect(),
191 ))
192 }
193
194 fn poly_square(x0: i32, y0: i32, side: i32) -> Geom32 {
195 let ring = LineString::new(vec![
196 Coord { x: x0, y: y0 },
197 Coord {
198 x: x0 + side,
199 y: y0,
200 },
201 Coord {
202 x: x0 + side,
203 y: y0 + side,
204 },
205 Coord {
206 x: x0,
207 y: y0 + side,
208 },
209 Coord { x: x0, y: y0 },
210 ]);
211 GeoGeom::Polygon(Polygon::new(ring, vec![]))
212 }
213
214 fn roundtrip_geom(decoded: &GeometryValues) -> GeometryValues {
216 let mut enc = Encoder::default();
217 decoded.clone().write_to(&mut enc).expect("encode failed");
218 let buf = enc.data;
219
220 let parsed = assert_empty(RawGeometry::from_bytes(&buf, &mut parser()));
221 let mut d = dec();
222 let result = LazyParsed::Raw(parsed)
223 .into_parsed(&mut d)
224 .expect("decode failed");
225 assert!(
226 d.consumed() > 0,
227 "decoder should consume bytes after decode"
228 );
229 result
230 }
231
232 fn canonical(geoms: &[Geom32]) -> GeometryValues {
234 let mut decoded = GeometryValues::default();
235 for g in geoms {
236 decoded.push_geom(g);
237 }
238 roundtrip_geom(&decoded)
239 }
240
241 fn layer_after_sort(geoms: &[Geom32], ids: &[u64], strategy: SortStrategy) -> TileLayer01 {
244 let features: Vec<TileFeature> = geoms
245 .iter()
246 .zip(ids.iter())
247 .map(|(g, &id)| TileFeature {
248 id: Some(id),
249 geometry: g.clone(),
250 properties: vec![],
251 })
252 .collect();
253
254 let mut layer = TileLayer01 {
255 name: "test".to_string(),
256 extent: 4096,
257 property_names: vec![],
258 features,
259 };
260
261 layer.sort(strategy);
262 layer
263 }
264
265 fn assert_sort_roundtrip(
267 geoms: &[Geom32],
268 ids: &[u64],
269 strategy: SortStrategy,
270 expected: &[Geom32],
271 ) {
272 let layer = layer_after_sort(geoms, ids, strategy);
273
274 let mut sorted_decoded = GeometryValues::default();
275 for f in &layer.features {
276 sorted_decoded.push_geom(&f.geometry);
277 }
278
279 let after_roundtrip = roundtrip_geom(&sorted_decoded);
280 let expected_canonical = canonical(expected);
281
282 assert_eq!(
283 after_roundtrip, expected_canonical,
284 "\nsorted geometry did not match expected after encode→decode round-trip\
285 \nvector_types after sort: {:?}\
286 \nvector_types expected: {:?}",
287 sorted_decoded.vector_types, expected_canonical.vector_types,
288 );
289 }
290
291 #[test]
294 fn pure_points_id_sort_roundtrip() {
295 assert_sort_roundtrip(
296 &[pt(0, 0), pt(1, 1), pt(2, 2)],
297 &[3, 2, 1],
298 SortStrategy::Id,
299 &[pt(2, 2), pt(1, 1), pt(0, 0)],
300 );
301 }
302
303 #[test]
306 fn pure_linestrings_id_sort_roundtrip() {
307 assert_sort_roundtrip(
308 &[ls(&[(0, 0), (0, 10)]), ls(&[(5, 5), (10, 10)])],
309 &[2, 1],
310 SortStrategy::Id,
311 &[ls(&[(5, 5), (10, 10)]), ls(&[(0, 0), (0, 10)])],
312 );
313 }
314
315 #[test]
318 fn point_line_point_id_sort_to_line_point_point_roundtrip() {
319 assert_sort_roundtrip(
320 &[pt(0, 0), ls(&[(1, 0), (1, 5)]), pt(5, 5)],
321 &[3, 1, 2],
322 SortStrategy::Id,
323 &[ls(&[(1, 0), (1, 5)]), pt(5, 5), pt(0, 0)],
324 );
325 }
326
327 #[test]
328 fn point_line_point_id_sort_to_point_point_line_roundtrip() {
329 assert_sort_roundtrip(
330 &[pt(0, 0), ls(&[(1, 0), (1, 5)]), pt(5, 5)],
331 &[1, 3, 2],
332 SortStrategy::Id,
333 &[pt(0, 0), pt(5, 5), ls(&[(1, 0), (1, 5)])],
334 );
335 }
336
337 #[test]
340 fn point_polygon_point_id_sort_roundtrip() {
341 assert_sort_roundtrip(
342 &[pt(0, 0), poly_square(10, 10, 5), pt(5, 5)],
343 &[2, 1, 3],
344 SortStrategy::Id,
345 &[poly_square(10, 10, 5), pt(0, 0), pt(5, 5)],
346 );
347 }
348
349 #[cfg(feature = "sort-coords-iter")]
352 #[test]
353 fn point_line_point_morton_sort_roundtrip() {
354 assert_sort_roundtrip(
355 &[pt(2, 0), ls(&[(0, 0), (0, 5)]), pt(1, 0)],
356 &[1, 2, 3],
357 SortStrategy::SpatialMorton,
358 &[ls(&[(0, 0), (0, 5)]), pt(1, 0), pt(2, 0)],
359 );
360 }
361
362 #[test]
365 fn id_sort_already_sorted_is_identity_roundtrip() {
366 let geoms = &[pt(0, 0), ls(&[(1, 0), (1, 5)]), pt(5, 5)];
367 assert_sort_roundtrip(geoms, &[1, 2, 3], SortStrategy::Id, geoms);
368 }
369
370 #[test]
373 fn id_column_co_permuted_with_geometry() {
374 let layer = layer_after_sort(
375 &[pt(0, 0), ls(&[(1, 0), (1, 5)]), pt(5, 5)],
376 &[3, 1, 2],
377 SortStrategy::Id,
378 );
379
380 let ids: Vec<Option<u64>> = layer.features.iter().map(|f| f.id).collect();
381 assert_eq!(ids, vec![Some(1u64), Some(2), Some(3)]);
382
383 let geom_types: Vec<&str> = layer
385 .features
386 .iter()
387 .map(|f| GeometryType::try_from(&f.geometry).unwrap().into())
388 .collect();
389 assert_eq!(geom_types, vec!["LineString", "Point", "Point"]);
390 }
391
392 fn build_tile_layer(geoms: &[Geom32], ids: &[Option<u64>]) -> TileLayer01 {
394 assert_eq!(geoms.len(), ids.len());
395 TileLayer01 {
396 name: "test".to_string(),
397 extent: 4096,
398 property_names: vec![],
399 features: geoms
400 .iter()
401 .zip(ids.iter())
402 .map(|(g, &id)| TileFeature {
403 id,
404 geometry: g.clone(),
405 properties: vec![],
406 })
407 .collect(),
408 }
409 }
410
411 fn sort_encode_decode(tile: TileLayer01, sort: SortStrategy) -> TileLayer01 {
414 let enc = Encoder::with_explicit(
415 Encoder::default().cfg,
416 ExplicitEncoder::for_id(IntEncoder::varint(), IdWidth::Id32),
417 );
418 let enc = StagedLayer01::from_tile(tile, sort, &[])
419 .encode_into(enc)
420 .expect("encode failed");
421
422 let buf = enc.into_layer_bytes().expect("into_layer_bytes failed");
424
425 let mut p = parser();
426 let layer_back = assert_empty(Layer::from_bytes(&buf, &mut p));
427 assert!(p.reserved() > 0, "parser should reserve bytes after parse");
428
429 let layer01 = into_layer01(layer_back);
430
431 let mut d = dec();
432 let tile = layer01.into_tile(&mut d).expect("decode after sort failed");
433 assert!(
434 d.consumed() > 0,
435 "decoder should consume bytes after decode"
436 );
437 tile
438 }
439
440 fn vertices_from_source(source: &TileLayer01) -> Vec<i32> {
442 let mut geom = GeometryValues::default();
443 for f in &source.features {
444 geom.push_geom(&f.geometry);
445 }
446 geom.vertices().unwrap_or_default().to_vec()
447 }
448
449 #[cfg(feature = "sort-coords-iter")]
450 #[test]
451 fn test_shared_morton_shift() {
452 let tile = build_tile_layer(&[pt(0, -10), pt(-10, 0)], &[Some(1), Some(2)]);
459 let source = sort_encode_decode(tile, SortStrategy::SpatialMorton);
460
461 let verts = vertices_from_source(&source);
462 assert_eq!(verts, vec![0, -10, -10, 0]);
463 }
464
465 #[test]
466 fn test_id_sort_nulls_first() {
467 let tile = build_tile_layer(&[pt(2, 2), pt(1, 1), pt(0, 0)], &[Some(10), None, Some(5)]);
468 let source = sort_encode_decode(tile, SortStrategy::Id);
469
470 let ids: Vec<Option<u64>> = source.features.iter().map(|f| f.id).collect();
471 assert_eq!(ids, vec![None, Some(5), Some(10)]);
473
474 let verts = vertices_from_source(&source);
475 assert_eq!(verts, vec![1, 1, 0, 0, 2, 2]);
477 }
478
479 #[cfg(feature = "sort-coords-iter")]
480 #[test]
481 fn test_mixed_geometry_morton_sort() {
482 let tile = build_tile_layer(
490 &[pt(2, 0), ls(&[(0, 0), (0, 5)]), pt(1, 0)],
491 &[Some(1), Some(2), Some(3)],
492 );
493 let source = sort_encode_decode(tile, SortStrategy::SpatialMorton);
494
495 let types: Vec<_> = source
496 .features
497 .iter()
498 .map(|f| GeometryType::try_from(&f.geometry).unwrap())
499 .collect();
500
501 assert_eq!(
502 types,
503 vec![
504 GeometryType::LineString,
505 GeometryType::Point,
506 GeometryType::Point
507 ]
508 );
509
510 let verts = vertices_from_source(&source);
511 assert_eq!(verts, vec![0, 0, 0, 5, 1, 0, 2, 0]);
513 }
514}