map-tile-toolkit 0.2.0

Clip integer polylines and polygons into per-tile pieces on a simple tile grid
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
//! The public API never panics: invalid input yields a typed [`TileError`] instead.

use geo_types::Coord;
use map_tile_toolkit::{
    PolygonSlicerAll, PolygonSlicerOne, SlicerAll, SlicerOne, TileError, TileId,
};

/// A polyline as a `Vec<Coord<i32>>`.
fn line(coords: Vec<(i32, i32)>) -> Vec<Coord<i32>> {
    coords.into_iter().map(|(x, y)| Coord { x, y }).collect()
}

/// A fresh all-tiles slicer over `Coord` (the config validation is what these tests probe).
fn all(extent: u32, buffer: u16) -> Result<SlicerAll<Coord<i32>>, TileError> {
    SlicerAll::new(extent, buffer)
}

/// A fresh single-tile slicer over `Coord`, bound to `tile`.
fn one(extent: u32, buffer: u16, tile: TileId) -> SlicerOne<Coord<i32>> {
    SlicerOne::new(extent, buffer, tile).expect("valid config")
}

/// The observable state of an all-tiles slicer: every tile's runs, ordered by tile then feature.
fn snapshot(s: &SlicerAll<Coord<i32>>) -> Vec<(TileId, Vec<Vec<Coord<i32>>>)> {
    s.iter_tiles()
        .map(|t| {
            let runs = t
                .iter_features()
                .flat_map(|f| f.iter_polylines().map(<[_]>::to_vec))
                .collect();
            (t.tile_id(), runs)
        })
        .collect()
}

/// A polyline that starts cleanly in a tile near `i32::MIN` (which the slicer emits into) and then
/// runs to a vertex whose tile's buffered box underflows `i32` — so `add_feature` errors *mid-walk*,
/// after it has already written pieces. `extent = 4096`, `buffer = 8`: the low vertex is `i32::MIN+8`
/// (itself within buffer of the edge, so the up-front bounds check passes) but its tile's base minus
/// the buffer underflows.
fn errors_after_emitting() -> Vec<Coord<i32>> {
    line(vec![
        (i32::MIN + 8200, 0), // tile −524286 (base i32::MIN+8192): clean, gets emitted
        (i32::MIN + 8250, 0),
        (i32::MIN + 8, 0), // tile −524288 (base i32::MIN): base − buffer underflows → Overflow
    ])
}

#[test]
fn invalid_extent() {
    assert_eq!(all(0, 0).err(), Some(TileError::InvalidExtent));
    assert_eq!(all(u32::MAX, 0).err(), Some(TileError::InvalidExtent));
    assert!(all(1, 0).is_ok());
    assert!(all(i32::MAX as u32, u16::MAX).is_ok());
    // Both slicers validate the extent the same way.
    assert_eq!(
        SlicerOne::<Coord<i32>>::new(0, 0, TileId::new(0, 0)).err(),
        Some(TileError::InvalidExtent)
    );
}

#[test]
fn buffer_too_large() {
    // `buffer` must be strictly less than half the `extent` (i.e. `2*buffer < extent`).
    assert_eq!(all(10, 5).err(), Some(TileError::BufferTooLarge)); // 2*5 == 10, not < 10
    assert_eq!(all(10, 6).err(), Some(TileError::BufferTooLarge));
    assert!(all(10, 4).is_ok()); // 2*4 == 8 < 10
    // With extent 1 only a zero buffer is allowed.
    assert!(all(1, 0).is_ok());
    assert_eq!(all(2, 1).err(), Some(TileError::BufferTooLarge));
    // Both slicers validate the buffer the same way.
    assert_eq!(
        SlicerOne::<Coord<i32>>::new(10, 5, TileId::new(0, 0)).err(),
        Some(TileError::BufferTooLarge)
    );
}

#[test]
fn extreme_tile_errors_instead_of_panicking() {
    let l = line(vec![(0, 0), (10, 10)]);
    // A tile whose (buffered) box coordinates overflow i32 → Overflow, not a panic.
    assert_eq!(
        one(4096, 0, TileId::new(i32::MAX, i32::MAX))
            .add_feature(&l)
            .err(),
        Some(TileError::Overflow)
    );
    assert_eq!(
        one(4096, 0, TileId::new(i32::MIN, 0)).add_feature(&l).err(),
        Some(TileError::Overflow)
    );
    // A far-but-representable tile touches nothing → no features accumulated.
    let mut far = one(4096, 0, TileId::new(1000, 1000));
    far.add_feature(&l).expect("no error for a far tile");
    assert!(far.is_empty());
}

#[test]
fn spanning_too_many_tiles_errors() {
    let mut s = all(1, 0).expect("valid config"); // 1 unit per tile
    // Touches 40 000 001 tiles, past the 2^25 (33 554 432) visit budget.
    assert_eq!(
        s.add_feature(line(vec![(0, 0), (40_000_000, 0)])).err(),
        Some(TileError::TooManyTiles)
    );
    assert!(s.is_empty());
    // Only the touched tiles count, not how far the line reaches: a diagonal across 100 000 tiles
    // (a 7·10⁹-tile bounding box, once also past the old `i16` span limit) touches under 200 000.
    let mut s = all(10, 1).expect("valid config");
    s.add_feature(line(vec![(0, 0), (1_000_000, 700_010)]))
        .expect("a long diagonal is within budget");
    assert!(s.len() > 100_000 && s.len() < 200_000, "{}", s.len());
}

#[test]
fn unbuffered_gap_rows_are_charged() {
    // With `buffer == 0`, a segment from x = 0 to x = 1 (extent 1) runs between the closed columns
    // 0 and 1 in every interior row, touching only its two endpoint tiles. Each row it crosses
    // still costs one visit, so 2^26 rows exceed the budget instead of looping for free.
    let mut s = all(1, 0).expect("valid config");
    assert_eq!(
        s.add_feature(line(vec![(0, 0), (1, 1 << 26)])).err(),
        Some(TileError::TooManyTiles)
    );
    assert!(s.is_empty());
    s.add_feature(line(vec![(0, 0), (1, 1000)]))
        .expect("a short gap run is within budget");
    assert_eq!(s.len(), 2, "only the endpoint tiles are touched");
}

#[test]
fn coordinate_overflow_errors() {
    let mut s = all(4096, 8).expect("valid config");
    // A vertex within `buffer` of i32::MAX: the buffered bound overflows i32 → Overflow.
    assert_eq!(
        s.add_feature(line(vec![(i32::MAX, 0), (i32::MAX, 10)]))
            .err(),
        Some(TileError::Overflow)
    );
}

#[test]
fn long_polylines_are_accepted() {
    // A polyline is limited only by the `u32` indexing of the storage (more than `u32::MAX` vertices
    // yields `GeometryTooLarge`), so well past the old `u16` cap is fine.
    let mut s = all(1_000_000, 0).expect("valid config");
    let coords: Vec<Coord<i32>> = (0..200_000).map(|i| Coord { x: i % 8, y: 0 }).collect();
    s.add_feature(&coords).expect("200 000 vertices");
    let runs: Vec<usize> = s
        .iter_tiles()
        .flat_map(|t| {
            t.iter_features()
                .flat_map(|f| f.iter_polylines().map(<[_]>::len))
                .collect::<Vec<_>>()
        })
        .collect();
    assert_eq!(runs, [200_000]);
}

#[test]
fn add_feature_is_atomic_on_error() {
    // The crafted polyline really does fail after emitting (otherwise this proves nothing).
    let mut probe = all(4096, 8).expect("valid config");
    probe
        .add_feature(errors_after_emitting())
        .expect_err("must error mid-walk");
    assert!(
        probe.is_empty(),
        "a feature that errors before any other data must leave the slicer empty"
    );

    // A clean feature that shares a tile with the failing one, so rollback must *truncate* an existing
    // tile (not just drop freshly-created ones).
    let mut s = all(4096, 8).expect("valid config");
    s.add_feature(line(vec![(i32::MIN + 8200, 0), (i32::MIN + 8300, 0)]))
        .expect("clean feature");
    let before = snapshot(&s);

    // The failing feature must leave no trace — the accumulator is byte-for-byte what it was.
    assert_eq!(
        s.add_feature(errors_after_emitting()).err(),
        Some(TileError::Overflow)
    );
    assert_eq!(snapshot(&s), before, "errored feature must roll back fully");

    // And the slicer stays usable: skipping the bad input and adding more works, and matches a run
    // that never saw the bad input at all.
    s.add_feature(line(vec![(i32::MIN + 8210, 0), (i32::MIN + 8260, 0)]))
        .expect("still usable after a rolled-back error");
    let mut clean = all(4096, 8).expect("valid config");
    clean
        .add_feature(line(vec![(i32::MIN + 8200, 0), (i32::MIN + 8300, 0)]))
        .expect("clean feature");
    clean
        .add_feature(line(vec![(i32::MIN + 8210, 0), (i32::MIN + 8260, 0)]))
        .expect("clean feature");
    assert_eq!(
        snapshot(&s),
        snapshot(&clean),
        "skipping a failed feature matches never adding it"
    );
}

#[test]
fn rollback_leaves_untouched_tiles_alone() {
    // A clean feature near the origin, then a failing one near i32::MIN — their tiles are disjoint.
    // The failing feature never touches the clean tile, so its rollback must scan past that tile on
    // the "not touched by this feature" branch (leaving it intact) while still dropping the far tiles
    // it did create before erroring. The atomicity test above only exercises the *touched* branch.
    let mut s = all(4096, 8).expect("valid config");
    s.add_feature(line(vec![(0, 0), (100, 0)]))
        .expect("clean feature near the origin");
    let before = snapshot(&s);

    assert_eq!(
        s.add_feature(errors_after_emitting()).err(),
        Some(TileError::Overflow),
        "the far feature errors mid-walk"
    );
    assert_eq!(
        snapshot(&s),
        before,
        "rolling back a feature that touches none of the pre-existing tiles leaves them intact"
    );
}

#[test]
fn clear_resets_for_reuse() {
    let mut s = all(25, 0).expect("valid config");
    s.add_feature(line(vec![(5, 5), (60, 40)])).expect("slice");
    assert!(!s.is_empty());
    s.clear();
    assert!(s.is_empty());
    assert_eq!(s.len(), 0);
    // After clear the slicer behaves exactly like a fresh one.
    let mut fresh = all(25, 0).expect("valid config");
    s.add_feature(line(vec![(1, 1), (30, 30)])).expect("slice");
    fresh
        .add_feature(line(vec![(1, 1), (30, 30)]))
        .expect("slice");
    assert_eq!(snapshot(&s), snapshot(&fresh));

    // SlicerOne clears too.
    let mut o = one(25, 0, TileId::new(0, 0));
    o.add_feature(line(vec![(5, 5), (20, 20)])).expect("slice");
    assert!(!o.is_empty());
    o.clear();
    assert!(o.is_empty());
    assert_eq!(o.tile(), TileId::new(0, 0), "clear keeps the bound tile");
}

#[test]
fn empty_and_degenerate_inputs_are_ok() {
    let mut s = all(25, 0).expect("valid config");
    // Empty polyline → no tiles, no error.
    s.add_feature(Vec::<Coord<i32>>::new())
        .expect("empty polyline is ok");
    assert!(s.is_empty());
    // A single-point polyline touches its own tile but yields no ≥2-vertex run.
    let dot = line(vec![(5, 5)]);
    s.add_feature(&dot).expect("single-point polyline is ok");
    assert!(s.is_empty());

    let mut one = one(25, 0, TileId::new(0, 0));
    one.add_feature(&dot).expect("single-point polyline is ok");
    assert!(one.is_empty());
}

// ---- PolygonSlicerAll ----

/// One polygon (exterior only) from integer corner pairs.
fn poly(coords: Vec<(i32, i32)>) -> Vec<Vec<Coord<i32>>> {
    vec![line(coords)]
}

/// Every edge tile's pieces and every fill run, per feature — the slicer's whole observable state.
type PolyState = Vec<(
    Vec<(TileId, Vec<Vec<Vec<Coord<i32>>>>)>,
    Vec<map_tile_toolkit::FillRun>,
)>;

fn poly_state(s: &PolygonSlicerAll<Coord<i32>>) -> PolyState {
    s.iter_features()
        .map(|f| {
            let tiles = f
                .iter_tiles()
                .map(|t| {
                    let polys = t
                        .iter_polygons()
                        .map(|p| p.iter_rings().map(|r| r.vertices().to_vec()).collect())
                        .collect();
                    (t.tile_id(), polys)
                })
                .collect();
            (tiles, f.iter_fill_runs().collect())
        })
        .collect()
}

#[test]
fn polygon_all_validates_config() {
    assert_eq!(
        PolygonSlicerAll::<Coord<i32>>::new(0, 0).err(),
        Some(TileError::InvalidExtent)
    );
    assert_eq!(
        PolygonSlicerAll::<Coord<i32>>::new(10, 5).err(),
        Some(TileError::BufferTooLarge)
    );
}

#[test]
fn polygon_all_too_many_tiles() {
    let mut s = PolygonSlicerAll::<Coord<i32>>::new(1, 0).expect("valid config");
    // A ring whose first edge touches 40 000 001 tiles, past the 2^25 visit budget.
    assert_eq!(
        s.add_feature([poly(vec![(0, 0), (40_000_000, 0), (40_000_000, 5)])])
            .err(),
        Some(TileError::TooManyTiles)
    );
    assert!(s.is_empty());
    // How far a ring reaches does not matter, only the tiles it touches: 40 000 tiles wide is fine.
    s.add_feature([poly(vec![(0, 0), (40_000, 0), (40_000, 5)])])
        .expect("a wide ring is within budget");
    s.clear();

    // Routing charges only the tiles an edge actually touches, never its bounding box: the large
    // triangle's diagonal spans a 5781² = 33 419 961-tile box (with the small one's 601² box, once
    // over the 2^25 budget) but touches only ~17 000 tiles. Covered tiles are never charged either:
    // the small triangle alone fills ~180 000 tiles.
    let small = poly(vec![(0, 0), (600, 600), (0, 600)]);
    let large = poly(vec![(0, 0), (5780, 5780), (0, 5780)]);
    s.add_feature([small.clone()])
        .expect("one ring is within budget");
    assert_eq!(s.len(), 1);
    let filled: usize = s
        .iter_features()
        .flat_map(|f| f.iter_fill_runs())
        .map(|r| r.x.len())
        .sum();
    assert!(filled > 170_000, "{filled}");
    s.add_feature([small, large])
        .expect("long diagonals are charged per touched tile");
    assert_eq!(s.len(), 2);
}

#[test]
fn polygon_all_charges_unbuffered_gap_rows() {
    // A sliver whose two long edges run between the closed columns 0 and 1 (extent 1, buffer 0):
    // they touch tiles only near their ends, but span 2^24 rows each, and the crossing index holds
    // one entry per row an edge spans. Charging each gap row keeps that memory within the budget:
    // the 2^25 rows are rejected during routing, before anything is indexed.
    let mut s = PolygonSlicerAll::<Coord<i32>>::new(1, 0).expect("valid config");
    assert_eq!(
        s.add_feature([poly(vec![(0, 0), (1, 1 << 24), (0, 1)])])
            .err(),
        Some(TileError::TooManyTiles)
    );
    assert!(s.is_empty());
    s.add_feature([poly(vec![(0, 0), (1, 1000), (0, 1)])])
        .expect("a short sliver is within budget");
    assert_eq!(s.len(), 1);
}

#[test]
fn polygon_all_is_atomic_on_error() {
    // A square whose left edge runs through the column of tiles based at x = i32::MIN (extent 4096,
    // buffer 0). Routing succeeds, but closing that edge's ring in tile row 1 needs a `B⁺` corner one
    // unit left of i32::MIN → Overflow, after row 0's tiles were already written.
    let (x0, x1) = (i32::MIN + 10, i32::MIN + 6000);
    let bad = poly(vec![(x0, 0), (x1, 0), (x1, 10_000), (x0, 10_000)]);
    let mut one =
        PolygonSlicerOne::<Coord<i32>>::new(4096, 0, TileId::new(-524_288, 1)).expect("config");
    assert_eq!(
        one.add_feature(&bad[0], &[]).err(),
        Some(TileError::Overflow),
        "the single-tile clip agrees"
    );

    let good = poly(vec![(5, 5), (9000, 5), (9000, 9000), (5, 9000)]);
    let mut s = PolygonSlicerAll::<Coord<i32>>::new(4096, 0).expect("valid config");
    s.add_feature([good.clone()]).expect("clean feature");
    let before = poly_state(&s);
    assert_eq!(s.add_feature([bad]).err(), Some(TileError::Overflow));
    assert_eq!(
        poly_state(&s),
        before,
        "errored feature must roll back fully"
    );

    // Still usable, and identical to a slicer that never saw the bad input.
    s.add_feature([good.clone()]).expect("still usable");
    let mut clean = PolygonSlicerAll::<Coord<i32>>::new(4096, 0).expect("valid config");
    clean.add_feature([good.clone()]).expect("clean");
    clean.add_feature([good]).expect("clean");
    assert_eq!(poly_state(&s), poly_state(&clean));
}

#[test]
fn polygon_all_degenerate_inputs_and_clear() {
    let mut s = PolygonSlicerAll::<Coord<i32>>::new(25, 0).expect("valid config");
    // No polygons, a polygon with no rings, and a degenerate exterior (whose holes are then ignored):
    // nothing is recorded.
    s.add_feature(Vec::<Vec<Vec<Coord<i32>>>>::new())
        .expect("empty");
    s.add_feature([Vec::<Vec<Coord<i32>>>::new()])
        .expect("no rings");
    s.add_feature([vec![
        line(vec![(1, 1), (9, 9), (1, 1)]),
        line(vec![(2, 2), (8, 2), (8, 8)]),
    ]])
    .expect("degenerate exterior");
    assert!(s.is_empty());
    // A degenerate hole is ignored.
    let square = line(vec![(2, 2), (60, 2), (60, 60), (2, 60)]);
    s.add_feature([vec![square.clone(), line(vec![(30, 30), (31, 31)])]])
        .expect("degenerate hole");
    let mut plain = PolygonSlicerAll::<Coord<i32>>::new(25, 0).expect("valid config");
    plain.add_feature([vec![square.clone()]]).expect("plain");
    assert_eq!(poly_state(&s), poly_state(&plain));

    // After clear the slicer behaves exactly like a fresh one.
    s.clear();
    assert!(s.is_empty());
    assert_eq!(s.len(), 0);
    s.add_feature([vec![square]]).expect("reuse");
    assert_eq!(poly_state(&s), poly_state(&plain));
}