1use serde::{Deserialize, Serialize};
2use ts_rs::TS;
3
4use crate::error::{PolygonizeError, Result};
5use std::sync::{
6 atomic::{AtomicBool, Ordering},
7 Arc,
8};
9
10pub(crate) const CANCELLATION_CHECK_INTERVAL: usize = 256;
12pub(crate) const MAX_UNCANCELLABLE_SORT_ITEMS: usize = 1_000_000;
15
16#[derive(Clone, Debug, Default)]
18pub struct CancellationToken(Arc<AtomicBool>);
19
20impl CancellationToken {
21 pub fn new() -> Self {
22 Self::default()
23 }
24
25 pub fn cancel(&self) {
26 self.0.store(true, Ordering::Release);
27 }
28
29 pub fn reset(&self) {
30 self.0.store(false, Ordering::Release);
31 }
32
33 pub fn is_cancelled(&self) -> bool {
34 self.0.load(Ordering::Acquire)
35 }
36}
37
38#[derive(Clone, Debug, Default)]
43pub struct ExecutionPolicy {
44 pub cancellation_token: Option<CancellationToken>,
45 #[cfg(test)]
46 pub(crate) cancel_at_work_item: Option<(CancellationToken, usize)>,
47 pub max_input_line_strings: Option<usize>,
48 pub max_input_segments: Option<usize>,
49 pub max_input_coordinates: Option<usize>,
50 pub max_noded_segments: Option<usize>,
51 pub max_candidate_pairs: Option<usize>,
52 pub max_exact_intersection_calls: Option<usize>,
53 pub max_split_events: Option<usize>,
54 pub max_noding_iterations: Option<usize>,
55 pub max_graph_nodes: Option<usize>,
56 pub max_graph_edges: Option<usize>,
57 pub max_rings: Option<usize>,
58 pub max_output_polygons: Option<usize>,
59 pub max_output_coordinates: Option<usize>,
60}
61
62impl ExecutionPolicy {
63 pub(crate) fn check(&self, stage: &str, limit: Option<usize>, observed: usize) -> Result<()> {
64 if let Some(limit) = limit.filter(|limit| observed > *limit) {
65 return Err(PolygonizeError::ResourceLimitExceeded {
66 stage: stage.to_string(),
67 limit,
68 observed,
69 });
70 }
71 Ok(())
72 }
73
74 pub(crate) fn has_noding_work_budgets(&self) -> bool {
75 self.max_candidate_pairs.is_some()
76 || self.max_exact_intersection_calls.is_some()
77 || self.max_split_events.is_some()
78 || self.max_noding_iterations.is_some()
79 }
80
81 pub(crate) fn has_cancellation(&self) -> bool {
82 self.cancellation_token.is_some()
83 }
84
85 pub(crate) fn check_noding_work(
86 &self,
87 candidate_pairs: usize,
88 exact_intersection_calls: usize,
89 ) -> Result<()> {
90 self.check("candidate_pairs", self.max_candidate_pairs, candidate_pairs)?;
91 self.check(
92 "exact_intersection_calls",
93 self.max_exact_intersection_calls,
94 exact_intersection_calls,
95 )
96 }
97
98 pub(crate) fn check_split_events(&self, observed: usize) -> Result<()> {
99 self.check("split_events", self.max_split_events, observed)
100 }
101
102 pub(crate) fn check_noding_iterations(&self, observed: usize) -> Result<()> {
103 self.check("noding_iterations", self.max_noding_iterations, observed)
104 }
105
106 pub(crate) fn check_cancelled(&self, stage: &str) -> Result<()> {
107 if self
108 .cancellation_token
109 .as_ref()
110 .is_some_and(CancellationToken::is_cancelled)
111 {
112 return Err(PolygonizeError::Cancelled {
113 stage: stage.to_string(),
114 });
115 }
116 Ok(())
117 }
118
119 pub(crate) fn check_cancelled_every(&self, stage: &str, work_items: usize) -> Result<()> {
120 #[cfg(test)]
121 if let Some((token, cancel_at)) = &self.cancel_at_work_item {
122 if work_items == *cancel_at {
123 token.cancel();
124 }
125 }
126 if work_items.is_multiple_of(CANCELLATION_CHECK_INTERVAL) {
127 self.check_cancelled(stage)?;
128 }
129 Ok(())
130 }
131
132 pub(crate) fn check_uncancellable_sort(&self, stage: &str, items: usize) -> Result<()> {
133 self.check_cancelled(stage)?;
134 if self.has_cancellation() {
135 self.check(stage, Some(MAX_UNCANCELLABLE_SORT_ITEMS), items)?;
136 }
137 Ok(())
138 }
139}
140
141#[derive(Clone, Debug, Serialize, Deserialize, TS)]
142#[serde(default, deny_unknown_fields)]
143#[ts(export)]
144pub struct PolygonizerOptions {
149 pub node_input: bool,
157
158 pub precision_model: PrecisionModel,
162
163 #[serde(default)]
171 pub pre_snap_tolerance: f64,
172
173 pub extract_only_polygonal: bool,
179
180 pub snap_strategy: SnapStrategy,
187
188 pub noding: NodingOptions,
190
191 pub containment: ContainmentOptions,
194
195 pub determinism: DeterminismOptions,
197
198 pub diagnostics: DiagnosticsOptions,
200
201 pub provenance: ProvenanceOptions,
203
204 pub z: ZOptions,
206
207 pub output_filter: OutputFilterOptions,
209
210 #[ts(optional)]
212 pub input_profile_id: Option<String>,
213}
214
215impl Default for PolygonizerOptions {
216 fn default() -> Self {
217 Self {
218 node_input: false,
219 precision_model: PrecisionModel::Floating,
220 pre_snap_tolerance: 0.0,
221 extract_only_polygonal: false,
222 snap_strategy: SnapStrategy::Grid,
223 noding: NodingOptions::default(),
224 containment: ContainmentOptions::default(),
225 determinism: DeterminismOptions::default(),
226 diagnostics: DiagnosticsOptions::default(),
227 provenance: ProvenanceOptions::default(),
228 z: ZOptions::default(),
229 output_filter: OutputFilterOptions::default(),
230 input_profile_id: None,
231 }
232 }
233}
234
235impl PolygonizerOptions {
236 pub fn cfb_robust_v1() -> Self {
237 Self {
238 node_input: true,
239 precision_model: PrecisionModel::FixedGrid { grid_size: 0.1 },
240 pre_snap_tolerance: 0.5,
241 extract_only_polygonal: false,
242 snap_strategy: SnapStrategy::GeosCompat,
243 noding: NodingOptions {
244 backend: NodingBackend::Snap,
245 guarantee: NodingGuarantee::Unchecked,
246 },
247 containment: ContainmentOptions {
248 touch_policy: TouchPolicy::AllowPointTouchDisallowEdgeShare,
249 },
250 determinism: DeterminismOptions {
251 canonical_sort: true,
252 canonical_ring_rotation: true,
253 stable_tie_breaks: true,
254 },
255 diagnostics: DiagnosticsOptions {
256 enabled: true,
257 report_mode: true,
258 timings: false,
259 },
260 provenance: ProvenanceOptions {
261 enabled: true,
262 include_boundary_line_ids: true,
263 },
264 z: ZOptions::default(),
265 output_filter: OutputFilterOptions::default(),
266 input_profile_id: Some("cfb_robust_v1".to_string()),
267 }
268 }
269
270 pub fn validate(&self) -> Result<()> {
271 for (field, value) in [("pre_snap_tolerance", self.pre_snap_tolerance)] {
272 if !value.is_finite() || value < 0.0 {
273 return Err(PolygonizeError::InvalidArgumentType {
274 field: field.to_string(),
275 expected: "a finite non-negative number".to_string(),
276 actual: value.to_string(),
277 });
278 }
279 }
280
281 if let PrecisionModel::FixedGrid { grid_size } = self.precision_model {
282 if !grid_size.is_finite() || grid_size <= 0.0 {
283 return Err(PolygonizeError::InvalidArgumentType {
284 field: "precision_model.grid_size".to_string(),
285 expected: "a finite positive number".to_string(),
286 actual: grid_size.to_string(),
287 });
288 }
289 if self.node_input && matches!(self.noding.backend, NodingBackend::Advanced) {
290 return Err(PolygonizeError::UnsupportedOptionCombination {
291 reason: "the Advanced compatibility noder supports floating precision only"
292 .to_string(),
293 });
294 }
295 }
296
297 if self.pre_snap_tolerance > 0.0 && !self.node_input {
298 return Err(PolygonizeError::UnsupportedOptionCombination {
299 reason: "pre_snap_tolerance requires node_input=true".to_string(),
300 });
301 }
302
303 if matches!(
304 self.noding.guarantee,
305 NodingGuarantee::CertifiedFixedPrecision
306 ) && (!self.node_input
307 || !matches!(self.precision_model, PrecisionModel::FixedGrid { .. })
308 || !matches!(self.noding.backend, NodingBackend::Snap)
309 || !matches!(self.snap_strategy, SnapStrategy::Grid))
310 {
311 return Err(PolygonizeError::UnsupportedOptionCombination {
312 reason: "CertifiedFixedPrecision requires node_input=true, FixedGrid precision, the Snap backend, and the Grid snap strategy".to_string(),
313 });
314 }
315
316 if let Some(value) = self.output_filter.minimum_face_area {
317 if !value.is_finite() || value < 0.0 {
318 return Err(PolygonizeError::InvalidArgumentType {
319 field: "output_filter.minimum_face_area".to_string(),
320 expected: "a finite non-negative number".to_string(),
321 actual: value.to_string(),
322 });
323 }
324 }
325
326 if !self.z.conflict_tolerance.is_finite() || self.z.conflict_tolerance < 0.0 {
327 return Err(PolygonizeError::InvalidArgumentType {
328 field: "z.conflict_tolerance".to_string(),
329 expected: "a finite non-negative number".to_string(),
330 actual: self.z.conflict_tolerance.to_string(),
331 });
332 }
333
334 Ok(())
335 }
336}
337
338#[derive(Clone, Copy, Debug, Default, PartialEq, Serialize, Deserialize, TS)]
339#[serde(tag = "type", rename_all = "snake_case")]
340#[ts(export)]
341pub enum PrecisionModel {
343 #[default]
345 Floating,
346 FixedGrid { grid_size: f64 },
348}
349
350impl PrecisionModel {
351 pub fn grid_size(self) -> f64 {
352 match self {
353 Self::Floating => 0.0,
354 Self::FixedGrid { grid_size } => grid_size,
355 }
356 }
357
358 pub fn from_grid_size(grid_size: f64) -> Self {
359 if grid_size == 0.0 {
360 Self::Floating
361 } else {
362 Self::FixedGrid { grid_size }
363 }
364 }
365}
366
367#[derive(Clone, Debug, Serialize, Deserialize, TS)]
368#[ts(export)]
369pub enum SnapStrategy {
382 Grid,
383 GeosCompat,
384}
385
386#[derive(Clone, Debug, Serialize, Deserialize, TS)]
387#[ts(export)]
388pub enum TouchPolicy {
389 AllowPointTouchDisallowEdgeShare,
390 TreatAnyTouchAsDisjoint,
391 AllowEdgeShare,
392}
393
394#[derive(Clone, Debug, Serialize, Deserialize, TS)]
395#[ts(export)]
396pub enum TileOwnershipPolicy {
397 Centroid,
399 RepresentativePointInsidePolygon,
401 LexicographicMinVertex,
403 CanonicalBoundaryHash,
405}
406
407#[derive(Clone, Debug, Serialize, Deserialize, TS)]
408#[ts(export)]
409pub enum NodingBackend {
410 Snap,
412 Advanced,
417}
418
419#[derive(Clone, Copy, Debug, Default, PartialEq, Serialize, Deserialize, TS)]
420#[ts(export)]
421pub enum NodingGuarantee {
422 #[default]
424 Unchecked,
425 Validate,
427 CertifiedFixedPrecision,
429}
430
431#[derive(Clone, Debug, Serialize, Deserialize, TS)]
432#[serde(default)]
433#[ts(export)]
434pub struct NodingOptions {
435 pub backend: NodingBackend,
436 pub guarantee: NodingGuarantee,
437}
438
439impl Default for NodingOptions {
440 fn default() -> Self {
441 Self {
442 backend: NodingBackend::Snap,
443 guarantee: NodingGuarantee::Unchecked,
444 }
445 }
446}
447
448#[derive(Clone, Debug, Serialize, Deserialize, TS)]
449#[serde(default)]
450#[ts(export)]
451pub struct ContainmentOptions {
452 pub touch_policy: TouchPolicy,
453}
454
455impl Default for ContainmentOptions {
456 fn default() -> Self {
457 Self {
458 touch_policy: TouchPolicy::AllowPointTouchDisallowEdgeShare,
459 }
460 }
461}
462
463#[derive(Clone, Debug, Serialize, Deserialize, TS)]
464#[serde(default)]
465#[ts(export)]
466pub struct DeterminismOptions {
467 pub canonical_sort: bool,
468 pub canonical_ring_rotation: bool,
469 pub stable_tie_breaks: bool,
470}
471
472impl Default for DeterminismOptions {
473 fn default() -> Self {
474 Self {
475 canonical_sort: true,
476 canonical_ring_rotation: true,
477 stable_tie_breaks: true,
478 }
479 }
480}
481
482#[derive(Clone, Debug, Default, Serialize, Deserialize, TS)]
483#[serde(default)]
484#[ts(export)]
485pub struct DiagnosticsOptions {
486 pub enabled: bool,
487 pub report_mode: bool,
488 #[serde(default)]
490 pub timings: bool,
491}
492
493#[derive(Clone, Debug, Default, Serialize, Deserialize, TS)]
494#[serde(default)]
495#[ts(export)]
496pub struct ProvenanceOptions {
497 pub enabled: bool,
498 pub include_boundary_line_ids: bool,
499}
500
501#[derive(Clone, Copy, Debug, Default, PartialEq, Serialize, Deserialize, TS)]
502#[ts(export)]
503pub enum ZPolicy {
504 Ignore,
506 #[default]
508 InterpolateAlongEdge,
509 PreferNearestEndpoint,
511 ErrorOnConflict,
513}
514
515#[derive(Clone, Copy, Debug, Default, PartialEq, Serialize, Deserialize, TS)]
516#[serde(default)]
517#[ts(export)]
518pub struct ZOptions {
519 pub policy: ZPolicy,
520 pub conflict_tolerance: f64,
522}
523
524#[derive(Clone, Debug, Default, Serialize, Deserialize, TS)]
525#[serde(default)]
526#[ts(export)]
527pub struct OutputFilterOptions {
528 #[ts(type = "number | null", optional)]
530 pub minimum_face_area: Option<f64>,
531}
532
533#[derive(Clone, Debug, Default, Serialize, Deserialize, TS)]
534#[ts(export)]
535pub enum DedupPolicy {
536 #[default]
537 KeepAll,
538 CanonicalRingHash,
539}
540
541#[cfg(test)]
542mod tests {
543 use super::*;
544
545 #[test]
546 fn partial_json_uses_defaults() {
547 let options: PolygonizerOptions = serde_json::from_str(
548 r#"{"diagnostics":{"enabled":true},"output_filter":{"minimum_face_area":2.0}}"#,
549 )
550 .unwrap();
551
552 assert!(options.diagnostics.enabled);
553 assert!(!options.diagnostics.report_mode);
554 assert_eq!(options.precision_model, PrecisionModel::Floating);
555 assert_eq!(options.output_filter.minimum_face_area, Some(2.0));
556 assert_eq!(options.z, ZOptions::default());
557
558 let fixed: PolygonizerOptions =
559 serde_json::from_str(r#"{"precision_model":{"type":"fixed_grid","grid_size":0.25}}"#)
560 .unwrap();
561 assert_eq!(
562 fixed.precision_model,
563 PrecisionModel::FixedGrid { grid_size: 0.25 }
564 );
565
566 let validated: PolygonizerOptions =
567 serde_json::from_str(r#"{"noding":{"guarantee":"Validate"}}"#).unwrap();
568 assert_eq!(validated.noding.guarantee, NodingGuarantee::Validate);
569
570 let z: PolygonizerOptions =
571 serde_json::from_str(r#"{"z":{"policy":"ErrorOnConflict"}}"#).unwrap();
572 assert_eq!(z.z.policy, ZPolicy::ErrorOnConflict);
573 assert_eq!(z.z.conflict_tolerance, 0.0);
574 }
575
576 #[test]
577 fn legacy_snap_grid_size_is_rejected_instead_of_ignored() {
578 let error =
579 serde_json::from_str::<PolygonizerOptions>(r#"{"snap_grid_size":0.1}"#).unwrap_err();
580 assert!(error.to_string().contains("unknown field `snap_grid_size`"));
581 }
582
583 #[test]
584 fn validation_rejects_invalid_options() {
585 for value in [-1.0, f64::NAN, f64::INFINITY, f64::NEG_INFINITY] {
586 let options = PolygonizerOptions {
587 node_input: true,
588 pre_snap_tolerance: value,
589 ..Default::default()
590 };
591 assert!(options.validate().is_err());
592
593 let options = PolygonizerOptions {
594 output_filter: OutputFilterOptions {
595 minimum_face_area: Some(value),
596 },
597 ..Default::default()
598 };
599 assert!(options.validate().is_err());
600
601 let options = PolygonizerOptions {
602 z: ZOptions {
603 conflict_tolerance: value,
604 ..Default::default()
605 },
606 ..Default::default()
607 };
608 assert!(options.validate().is_err());
609 }
610
611 for grid_size in [-1.0, 0.0, f64::NAN, f64::INFINITY, f64::NEG_INFINITY] {
612 let options = PolygonizerOptions {
613 precision_model: PrecisionModel::FixedGrid { grid_size },
614 ..Default::default()
615 };
616 assert!(options.validate().is_err());
617 }
618
619 let options = PolygonizerOptions {
620 pre_snap_tolerance: 1.0,
621 ..Default::default()
622 };
623 assert!(matches!(
624 options.validate(),
625 Err(PolygonizeError::UnsupportedOptionCombination { .. })
626 ));
627
628 let options = PolygonizerOptions {
629 node_input: true,
630 precision_model: PrecisionModel::FixedGrid { grid_size: 1.0 },
631 noding: NodingOptions {
632 backend: NodingBackend::Advanced,
633 guarantee: NodingGuarantee::Unchecked,
634 },
635 ..Default::default()
636 };
637 assert!(matches!(
638 options.validate(),
639 Err(PolygonizeError::UnsupportedOptionCombination { .. })
640 ));
641
642 let certified = PolygonizerOptions {
643 node_input: true,
644 precision_model: PrecisionModel::FixedGrid { grid_size: 1.0 },
645 noding: NodingOptions {
646 guarantee: NodingGuarantee::CertifiedFixedPrecision,
647 ..Default::default()
648 },
649 ..Default::default()
650 };
651 assert!(certified.validate().is_ok());
652 assert!(PolygonizerOptions {
653 node_input: false,
654 ..certified.clone()
655 }
656 .validate()
657 .is_err());
658 assert!(PolygonizerOptions {
659 precision_model: PrecisionModel::Floating,
660 ..certified
661 }
662 .validate()
663 .is_err());
664
665 assert!(PolygonizerOptions::default().validate().is_ok());
666 }
667
668 #[test]
669 fn cancellation_enabled_sorts_have_a_fixed_item_ceiling() {
670 let policy = ExecutionPolicy {
671 cancellation_token: Some(CancellationToken::new()),
672 ..Default::default()
673 };
674 assert!(policy
675 .check_uncancellable_sort("test_sort", MAX_UNCANCELLABLE_SORT_ITEMS)
676 .is_ok());
677 assert!(matches!(
678 policy.check_uncancellable_sort(
679 "test_sort",
680 MAX_UNCANCELLABLE_SORT_ITEMS + 1
681 ),
682 Err(PolygonizeError::ResourceLimitExceeded {
683 stage,
684 limit: MAX_UNCANCELLABLE_SORT_ITEMS,
685 observed,
686 }) if stage == "test_sort" && observed == MAX_UNCANCELLABLE_SORT_ITEMS + 1
687 ));
688 }
689}