1use crate::geo::{Coord, LineString};
2use crate::hiking::HikingModel;
3use crate::model::{
4 Access, CrossingEvidence, CrossingKind, Edge, EdgeTravel, Provenance, Terrain, TerrainEvidence,
5 WalkGraph,
6};
7use crate::{Result, TrailgenError};
8use rstar::{AABB, RTree, RTreeObject};
9use serde::de::{SeqAccess, Visitor};
10use serde::{Deserialize, Serialize};
11use std::fmt::{Display, Formatter};
12use std::str::FromStr;
13
14#[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)]
15pub struct PlanningDate {
16 pub year: u16,
17 pub month: u8,
18 pub day: u8,
19}
20
21impl PlanningDate {
22 #[must_use]
23 pub const fn new(year: u16, month: u8, day: u8) -> Option<Self> {
24 if year >= 1 && month >= 1 && month <= 12 && day >= 1 && day <= days_in_month(year, month) {
25 Some(Self { year, month, day })
26 } else {
27 None
28 }
29 }
30
31 #[must_use]
32 pub const fn weekday(self) -> Weekday {
33 const OFFSETS: [i32; 12] = [0, 3, 2, 5, 0, 3, 5, 1, 4, 6, 2, 4];
34 let month = self.month as usize;
35 let year = if month < 3 {
36 self.year as i32 - 1
37 } else {
38 self.year as i32
39 };
40 match (year + year / 4 - year / 100 + year / 400 + OFFSETS[month - 1] + self.day as i32)
41 .rem_euclid(7)
42 {
43 0 => Weekday::Sunday,
44 1 => Weekday::Monday,
45 2 => Weekday::Tuesday,
46 3 => Weekday::Wednesday,
47 4 => Weekday::Thursday,
48 5 => Weekday::Friday,
49 6 => Weekday::Saturday,
50 _ => unreachable!(),
51 }
52 }
53}
54
55impl Display for PlanningDate {
56 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
57 write!(f, "{:04}-{:02}-{:02}", self.year, self.month, self.day)
58 }
59}
60
61impl FromStr for PlanningDate {
62 type Err = String;
63
64 fn from_str(raw: &str) -> std::result::Result<Self, Self::Err> {
65 let mut parts = raw.trim().split('-');
66 let year = parts
67 .next()
68 .ok_or_else(|| "date must be YYYY-MM-DD".to_owned())?
69 .parse::<u16>()
70 .map_err(|error| error.to_string())?;
71 let month = parts
72 .next()
73 .ok_or_else(|| "date must be YYYY-MM-DD".to_owned())?
74 .parse::<u8>()
75 .map_err(|error| error.to_string())?;
76 let day = parts
77 .next()
78 .ok_or_else(|| "date must be YYYY-MM-DD".to_owned())?
79 .parse::<u8>()
80 .map_err(|error| error.to_string())?;
81 if parts.next().is_some() {
82 return Err("date must be YYYY-MM-DD".to_owned());
83 }
84 Self::new(year, month, day).ok_or_else(|| "invalid civil date".to_owned())
85 }
86}
87
88impl Serialize for PlanningDate {
89 fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
90 where
91 S: serde::Serializer,
92 {
93 serializer.serialize_str(&self.to_string())
94 }
95}
96
97impl<'de> Deserialize<'de> for PlanningDate {
98 fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
99 where
100 D: serde::Deserializer<'de>,
101 {
102 struct DateVisitor;
103
104 impl Visitor<'_> for DateVisitor {
105 type Value = PlanningDate;
106
107 fn expecting(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
108 f.write_str("a YYYY-MM-DD civil date")
109 }
110
111 fn visit_str<E>(self, v: &str) -> std::result::Result<Self::Value, E>
112 where
113 E: serde::de::Error,
114 {
115 v.parse::<PlanningDate>().map_err(E::custom)
116 }
117 }
118
119 deserializer.deserialize_str(DateVisitor)
120 }
121}
122
123#[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)]
124pub struct PlanningTime {
125 pub hour: u8,
126 pub minute: u8,
127}
128
129impl PlanningTime {
130 #[must_use]
131 pub const fn new(hour: u8, minute: u8) -> Option<Self> {
132 if hour < 24 && minute < 60 {
133 Some(Self { hour, minute })
134 } else {
135 None
136 }
137 }
138
139 const fn minute_of_day(self) -> u16 {
140 self.hour as u16 * 60 + self.minute as u16
141 }
142}
143
144impl Display for PlanningTime {
145 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
146 write!(f, "{:02}:{:02}", self.hour, self.minute)
147 }
148}
149
150impl FromStr for PlanningTime {
151 type Err = String;
152
153 fn from_str(raw: &str) -> std::result::Result<Self, Self::Err> {
154 let mut parts = raw.trim().split(':');
155 let hour = parts
156 .next()
157 .ok_or_else(|| "time must be HH:MM".to_owned())?
158 .parse::<u8>()
159 .map_err(|error| error.to_string())?;
160 let minute = parts
161 .next()
162 .ok_or_else(|| "time must be HH:MM".to_owned())?
163 .parse::<u8>()
164 .map_err(|error| error.to_string())?;
165 match parts.next() {
166 None => {}
167 Some("00") if parts.next().is_none() => {}
168 _ => return Err("time must be HH:MM or HH:MM:00".to_owned()),
169 }
170 Self::new(hour, minute).ok_or_else(|| "invalid civil time".to_owned())
171 }
172}
173
174impl Serialize for PlanningTime {
175 fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
176 where
177 S: serde::Serializer,
178 {
179 serializer.serialize_str(&self.to_string())
180 }
181}
182
183impl<'de> Deserialize<'de> for PlanningTime {
184 fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
185 where
186 D: serde::Deserializer<'de>,
187 {
188 struct TimeVisitor;
189
190 impl Visitor<'_> for TimeVisitor {
191 type Value = PlanningTime;
192
193 fn expecting(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
194 f.write_str("an HH:MM civil time")
195 }
196
197 fn visit_str<E>(self, v: &str) -> std::result::Result<Self::Value, E>
198 where
199 E: serde::de::Error,
200 {
201 v.parse::<PlanningTime>().map_err(E::custom)
202 }
203 }
204
205 deserializer.deserialize_str(TimeVisitor)
206 }
207}
208
209#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
210#[serde(rename_all = "kebab-case")]
211pub struct DailyTimeWindow {
212 pub from: PlanningTime,
213 pub to: PlanningTime,
214}
215
216impl DailyTimeWindow {
217 #[must_use]
218 pub const fn new(from: PlanningTime, to: PlanningTime) -> Self {
219 Self { from, to }
220 }
221
222 #[must_use]
223 pub const fn contains(self, time: PlanningTime) -> bool {
224 let from = self.from.minute_of_day();
225 let to = self.to.minute_of_day();
226 let time = time.minute_of_day();
227 if from <= to {
228 from <= time && time <= to
229 } else {
230 from <= time || time <= to
231 }
232 }
233}
234
235#[derive(Clone, Copy, Debug, Eq, PartialEq)]
236pub struct PlanningMoment {
237 pub date: Option<PlanningDate>,
238 pub time: Option<PlanningTime>,
239}
240
241impl PlanningMoment {
242 #[must_use]
243 pub const fn new(date: Option<PlanningDate>, time: Option<PlanningTime>) -> Self {
244 Self { date, time }
245 }
246
247 #[must_use]
248 pub const fn on(date: PlanningDate) -> Self {
249 Self::new(Some(date), None)
250 }
251}
252
253#[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)]
254pub struct MonthDay {
255 pub month: u8,
256 pub day: u8,
257}
258
259impl MonthDay {
260 #[must_use]
261 pub const fn new(month: u8, day: u8) -> Option<Self> {
262 if month >= 1 && month <= 12 && day >= 1 && day <= days_in_month(2024, month) {
263 Some(Self { month, day })
264 } else {
265 None
266 }
267 }
268}
269
270impl From<PlanningDate> for MonthDay {
271 fn from(value: PlanningDate) -> Self {
272 Self {
273 month: value.month,
274 day: value.day,
275 }
276 }
277}
278
279impl Display for MonthDay {
280 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
281 write!(f, "{:02}-{:02}", self.month, self.day)
282 }
283}
284
285impl FromStr for MonthDay {
286 type Err = String;
287
288 fn from_str(raw: &str) -> std::result::Result<Self, Self::Err> {
289 let mut parts = raw.trim().split('-');
290 let month = parts
291 .next()
292 .ok_or_else(|| "month-day must be MM-DD".to_owned())?
293 .parse::<u8>()
294 .map_err(|error| error.to_string())?;
295 let day = parts
296 .next()
297 .ok_or_else(|| "month-day must be MM-DD".to_owned())?
298 .parse::<u8>()
299 .map_err(|error| error.to_string())?;
300 if parts.next().is_some() {
301 return Err("month-day must be MM-DD".to_owned());
302 }
303 Self::new(month, day).ok_or_else(|| "invalid recurring month-day".to_owned())
304 }
305}
306
307impl Serialize for MonthDay {
308 fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
309 where
310 S: serde::Serializer,
311 {
312 serializer.serialize_str(&self.to_string())
313 }
314}
315
316impl<'de> Deserialize<'de> for MonthDay {
317 fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
318 where
319 D: serde::Deserializer<'de>,
320 {
321 struct MonthDayVisitor;
322
323 impl Visitor<'_> for MonthDayVisitor {
324 type Value = MonthDay;
325
326 fn expecting(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
327 f.write_str("an MM-DD recurring month-day")
328 }
329
330 fn visit_str<E>(self, v: &str) -> std::result::Result<Self::Value, E>
331 where
332 E: serde::de::Error,
333 {
334 v.parse::<MonthDay>().map_err(E::custom)
335 }
336 }
337
338 deserializer.deserialize_str(MonthDayVisitor)
339 }
340}
341
342#[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)]
343pub enum Weekday {
344 Monday,
345 Tuesday,
346 Wednesday,
347 Thursday,
348 Friday,
349 Saturday,
350 Sunday,
351}
352
353impl Weekday {
354 const ALL: [Self; 7] = [
355 Self::Monday,
356 Self::Tuesday,
357 Self::Wednesday,
358 Self::Thursday,
359 Self::Friday,
360 Self::Saturday,
361 Self::Sunday,
362 ];
363
364 const fn bit(self) -> u8 {
365 1 << self.index()
366 }
367
368 const fn index(self) -> u8 {
369 match self {
370 Self::Monday => 0,
371 Self::Tuesday => 1,
372 Self::Wednesday => 2,
373 Self::Thursday => 3,
374 Self::Friday => 4,
375 Self::Saturday => 5,
376 Self::Sunday => 6,
377 }
378 }
379
380 const fn from_index(index: u8) -> Self {
381 match index % 7 {
382 0 => Self::Monday,
383 1 => Self::Tuesday,
384 2 => Self::Wednesday,
385 3 => Self::Thursday,
386 4 => Self::Friday,
387 5 => Self::Saturday,
388 6 => Self::Sunday,
389 _ => unreachable!(),
390 }
391 }
392
393 fn parse_token(raw: &str) -> std::result::Result<Self, String> {
394 match weekday_atom(raw).as_str() {
395 "mon" | "monday" => Ok(Self::Monday),
396 "tue" | "tues" | "tuesday" => Ok(Self::Tuesday),
397 "wed" | "weds" | "wednesday" => Ok(Self::Wednesday),
398 "thu" | "thur" | "thurs" | "thursday" => Ok(Self::Thursday),
399 "fri" | "friday" => Ok(Self::Friday),
400 "sat" | "saturday" => Ok(Self::Saturday),
401 "sun" | "sunday" => Ok(Self::Sunday),
402 _ => Err(format!("invalid weekday {raw:?}")),
403 }
404 }
405}
406
407impl Display for Weekday {
408 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
409 f.write_str(match self {
410 Self::Monday => "monday",
411 Self::Tuesday => "tuesday",
412 Self::Wednesday => "wednesday",
413 Self::Thursday => "thursday",
414 Self::Friday => "friday",
415 Self::Saturday => "saturday",
416 Self::Sunday => "sunday",
417 })
418 }
419}
420
421impl FromStr for Weekday {
422 type Err = String;
423
424 fn from_str(raw: &str) -> std::result::Result<Self, Self::Err> {
425 Self::parse_token(raw)
426 }
427}
428
429impl Serialize for Weekday {
430 fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
431 where
432 S: serde::Serializer,
433 {
434 serializer.serialize_str(&self.to_string())
435 }
436}
437
438impl<'de> Deserialize<'de> for Weekday {
439 fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
440 where
441 D: serde::Deserializer<'de>,
442 {
443 struct WeekdayVisitor;
444
445 impl Visitor<'_> for WeekdayVisitor {
446 type Value = Weekday;
447
448 fn expecting(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
449 f.write_str("a weekday name or abbreviation")
450 }
451
452 fn visit_str<E>(self, v: &str) -> std::result::Result<Self::Value, E>
453 where
454 E: serde::de::Error,
455 {
456 v.parse::<Weekday>().map_err(E::custom)
457 }
458 }
459
460 deserializer.deserialize_str(WeekdayVisitor)
461 }
462}
463
464#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
465pub struct WeekdaySet(u8);
466
467impl WeekdaySet {
468 const ALL_BITS: u8 = 0b0111_1111;
469 const WEEKDAYS: [Weekday; 5] = [
470 Weekday::Monday,
471 Weekday::Tuesday,
472 Weekday::Wednesday,
473 Weekday::Thursday,
474 Weekday::Friday,
475 ];
476 const WEEKENDS: [Weekday; 2] = [Weekday::Saturday, Weekday::Sunday];
477
478 #[must_use]
479 pub const fn empty() -> Self {
480 Self(0)
481 }
482
483 #[must_use]
484 pub const fn is_empty(&self) -> bool {
485 self.0 == 0
486 }
487
488 #[must_use]
489 pub const fn contains(self, weekday: Weekday) -> bool {
490 self.0 & weekday.bit() != 0
491 }
492
493 #[must_use]
494 pub const fn union(self, rhs: Self) -> Self {
495 Self(self.0 | rhs.0)
496 }
497
498 const fn insert(&mut self, weekday: Weekday) {
499 self.0 |= weekday.bit();
500 }
501
502 fn insert_many(&mut self, weekdays: impl IntoIterator<Item = Weekday>) {
503 weekdays
504 .into_iter()
505 .for_each(|weekday| self.insert(weekday));
506 }
507
508 fn insert_range(&mut self, from: Weekday, to: Weekday) {
509 let mut index = from.index();
510 loop {
511 let weekday = Weekday::from_index(index);
512 self.insert(weekday);
513 if weekday == to {
514 break;
515 }
516 index = (index + 1) % 7;
517 }
518 }
519
520 fn ingest_token(&mut self, raw: &str) -> std::result::Result<(), String> {
521 let token = weekday_atom(raw);
522 match token.as_str() {
523 "" | "none" => Ok(()),
524 "all" | "daily" | "everyday" => {
525 self.0 = Self::ALL_BITS;
526 Ok(())
527 }
528 "weekday" | "weekdays" => {
529 self.insert_many(Self::WEEKDAYS);
530 Ok(())
531 }
532 "weekend" | "weekends" => {
533 self.insert_many(Self::WEEKENDS);
534 Ok(())
535 }
536 _ => {
537 if let Some((from, to)) = token.split_once('-') {
538 self.insert_range(Weekday::parse_token(from)?, Weekday::parse_token(to)?);
539 } else {
540 self.insert(Weekday::parse_token(&token)?);
541 }
542 Ok(())
543 }
544 }
545 }
546
547 fn iter(self) -> impl Iterator<Item = Weekday> {
548 Weekday::ALL
549 .into_iter()
550 .filter(move |weekday| self.contains(*weekday))
551 }
552}
553
554impl FromStr for WeekdaySet {
555 type Err = String;
556
557 fn from_str(raw: &str) -> std::result::Result<Self, Self::Err> {
558 let mut set = Self::empty();
559 for token in
560 raw.split(|c: char| c == ',' || c == ';' || c == '|' || c == '/' || c.is_whitespace())
561 {
562 set.ingest_token(token)?;
563 }
564 Ok(set)
565 }
566}
567
568impl Serialize for WeekdaySet {
569 fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
570 where
571 S: serde::Serializer,
572 {
573 let weekdays = self.iter().collect::<Vec<_>>();
574 weekdays.serialize(serializer)
575 }
576}
577
578impl<'de> Deserialize<'de> for WeekdaySet {
579 fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
580 where
581 D: serde::Deserializer<'de>,
582 {
583 struct WeekdaySetVisitor;
584
585 impl<'de> Visitor<'de> for WeekdaySetVisitor {
586 type Value = WeekdaySet;
587
588 fn expecting(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
589 f.write_str("a weekday string or sequence")
590 }
591
592 fn visit_str<E>(self, v: &str) -> std::result::Result<Self::Value, E>
593 where
594 E: serde::de::Error,
595 {
596 v.parse::<WeekdaySet>().map_err(E::custom)
597 }
598
599 fn visit_seq<A>(self, mut seq: A) -> std::result::Result<Self::Value, A::Error>
600 where
601 A: SeqAccess<'de>,
602 {
603 let mut set = WeekdaySet::empty();
604 while let Some(raw) = seq.next_element::<String>()? {
605 set = set.union(
606 raw.parse::<WeekdaySet>()
607 .map_err(serde::de::Error::custom)?,
608 );
609 }
610 Ok(set)
611 }
612 }
613
614 deserializer.deserialize_any(WeekdaySetVisitor)
615 }
616}
617
618fn weekday_atom(raw: &str) -> String {
619 raw.trim()
620 .trim_matches('.')
621 .to_ascii_lowercase()
622 .replace('_', "-")
623}
624
625#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
626#[serde(rename_all = "kebab-case")]
627pub struct SeasonalWindow {
628 pub from: MonthDay,
629 pub to: MonthDay,
630}
631
632impl SeasonalWindow {
633 #[must_use]
634 pub const fn new(from: MonthDay, to: MonthDay) -> Self {
635 Self { from, to }
636 }
637
638 #[must_use]
639 pub fn contains(self, date: PlanningDate) -> bool {
640 let day = MonthDay::from(date);
641 if self.from <= self.to {
642 self.from <= day && day <= self.to
643 } else {
644 self.from <= day || day <= self.to
645 }
646 }
647}
648
649#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
650#[serde(rename_all = "kebab-case")]
651pub struct AccessWindow {
652 #[serde(default, skip_serializing_if = "Option::is_none")]
653 pub from: Option<PlanningDate>,
654 #[serde(default, skip_serializing_if = "Option::is_none")]
655 pub to: Option<PlanningDate>,
656 #[serde(default, skip_serializing_if = "Option::is_none")]
657 pub seasonal: Option<SeasonalWindow>,
658 #[serde(default, skip_serializing_if = "WeekdaySet::is_empty")]
659 pub weekdays: WeekdaySet,
660 #[serde(default, skip_serializing_if = "Option::is_none")]
661 pub time: Option<DailyTimeWindow>,
662}
663
664impl AccessWindow {
665 #[must_use]
666 pub const fn is_always(&self) -> bool {
667 self.from.is_none()
668 && self.to.is_none()
669 && self.seasonal.is_none()
670 && self.weekdays.is_empty()
671 && self.time.is_none()
672 }
673
674 #[must_use]
675 pub fn contains(self, date: Option<PlanningDate>) -> bool {
676 self.contains_at(Some(PlanningMoment::new(date, None)))
677 }
678
679 #[must_use]
680 pub fn contains_at(self, moment: Option<PlanningMoment>) -> bool {
681 let Some(moment) = moment else {
682 return true;
683 };
684 moment.date.is_none_or(|date| {
685 self.from.is_none_or(|from| from <= date)
686 && self.to.is_none_or(|to| date <= to)
687 && self.seasonal.is_none_or(|season| season.contains(date))
688 && (self.weekdays.is_empty() || self.weekdays.contains(date.weekday()))
689 }) && self
690 .time
691 .is_none_or(|time_window| moment.time.is_none_or(|time| time_window.contains(time)))
692 }
693}
694
695#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
696pub struct AccessOverlay {
697 pub name: String,
698 pub access: Access,
699 #[serde(default, skip_serializing_if = "Option::is_none")]
700 pub travel: Option<EdgeTravel>,
701 #[serde(default, skip_serializing_if = "AccessWindow::is_always")]
702 pub active: AccessWindow,
703 pub confidence: f64,
704 pub tolerance_m: f64,
705 pub provenance: Provenance,
706 pub geometry: OverlayGeometry,
707}
708
709#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
710pub struct TerrainOverlay {
711 pub name: String,
712 pub terrain: Terrain,
713 #[serde(default, skip_serializing_if = "Option::is_none")]
714 pub surface: Option<String>,
715 pub confidence: f64,
716 pub tolerance_m: f64,
717 pub provenance: Provenance,
718 pub geometry: OverlayGeometry,
719}
720
721#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
722pub struct ContextOverlay {
723 pub name: String,
724 pub kind: CrossingKind,
725 pub confidence: f64,
726 pub provenance: Provenance,
727 pub geometry: LineString,
728}
729
730#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
731#[serde(rename_all = "kebab-case", tag = "kind", content = "value")]
732pub enum OverlayGeometry {
733 Polygon(Vec<Coord>),
734 MultiPolygon(Vec<Vec<Coord>>),
735 Line(LineString),
736 MultiLine(Vec<LineString>),
737}
738
739impl OverlayGeometry {
740 #[must_use]
741 pub fn affects(&self, edge: &Edge, tolerance_m: f64) -> bool {
742 match self {
743 Self::Polygon(ring) => line_intersects_ring(&edge.geometry, ring),
744 Self::MultiPolygon(rings) => rings
745 .iter()
746 .any(|ring| line_intersects_ring(&edge.geometry, ring)),
747 Self::Line(line) => line_distance_m(&edge.geometry, line) <= tolerance_m,
748 Self::MultiLine(lines) => lines
749 .iter()
750 .any(|line| line_distance_m(&edge.geometry, line) <= tolerance_m),
751 }
752 }
753}
754
755impl AccessOverlay {
756 #[must_use]
757 pub fn affects(&self, edge: &Edge) -> bool {
758 self.geometry.affects(edge, self.tolerance_m)
759 }
760
761 #[must_use]
762 pub fn active_on(&self, date: Option<PlanningDate>) -> bool {
763 self.active.contains(date)
764 }
765
766 #[must_use]
767 pub fn active_at(&self, moment: Option<PlanningMoment>) -> bool {
768 self.active.contains_at(moment)
769 }
770}
771
772impl TerrainOverlay {
773 #[must_use]
774 pub fn affects(&self, edge: &Edge) -> bool {
775 self.geometry.affects(edge, self.tolerance_m)
776 }
777}
778
779pub fn apply_access_overlays(
780 graph: &mut WalkGraph,
781 overlays: &[AccessOverlay],
782 planning_date: Option<PlanningDate>,
783) -> usize {
784 apply_access_overlays_at(
785 graph,
786 overlays,
787 Some(PlanningMoment::new(planning_date, None)),
788 )
789}
790
791pub fn apply_access_overlays_at(
792 graph: &mut WalkGraph,
793 overlays: &[AccessOverlay],
794 planning_moment: Option<PlanningMoment>,
795) -> usize {
796 let mut touched = 0usize;
797 let mut travel_changed = false;
798 for edge in &mut graph.edges {
799 for overlay in overlays {
800 if !overlay.active_at(planning_moment) || !overlay.affects(edge) {
801 continue;
802 }
803 touched += 1;
804 edge.attr.access = overlay.access;
805 if let Some(travel) = overlay.travel {
806 edge.attr.travel = travel;
807 travel_changed = true;
808 }
809 edge.attr.access_confidence = edge.attr.access_confidence.max(overlay.confidence);
810 edge.attr.confidence = edge.attr.confidence.min(overlay.confidence);
811 if !edge.attr.access_provenance.contains(&overlay.provenance) {
812 edge.attr.access_provenance.push(overlay.provenance.clone());
813 }
814 }
815 }
816 if travel_changed {
817 graph.rebuild_adjacency();
818 }
819 touched
820}
821
822pub fn apply_terrain_overlays(graph: &mut WalkGraph, overlays: &[TerrainOverlay]) -> usize {
823 let mut touched = 0usize;
824 for edge in &mut graph.edges {
825 let mut changed = false;
826 for overlay in overlays {
827 if !overlay.affects(edge) {
828 continue;
829 }
830 touched += 1;
831 changed = true;
832 edge.attr.terrain = overlay.terrain;
833 if let Some(surface) = &overlay.surface {
834 edge.attr.surface = Some(surface.clone());
835 }
836 edge.attr.terrain_confidence = edge.attr.terrain_confidence.max(overlay.confidence);
837 edge.attr.confidence = edge.attr.confidence.min(overlay.confidence);
838 push_terrain_evidence(edge, overlay);
839 }
840 if changed {
841 HikingModel.apply(edge);
842 }
843 }
844 touched
845}
846
847const fn days_in_month(year: u16, month: u8) -> u8 {
848 match month {
849 1 | 3 | 5 | 7 | 8 | 10 | 12 => 31,
850 4 | 6 | 9 | 11 => 30,
851 2 if is_leap_year(year) => 29,
852 2 => 28,
853 _ => 0,
854 }
855}
856
857const fn is_leap_year(year: u16) -> bool {
858 year.is_multiple_of(4) && !year.is_multiple_of(100) || year.is_multiple_of(400)
859}
860
861pub fn apply_context_overlays(graph: &mut WalkGraph, overlays: &[ContextOverlay]) -> usize {
862 let mut crossings = 0usize;
863 let index = RTree::bulk_load(
864 overlays
865 .iter()
866 .enumerate()
867 .map(|(index, overlay)| ContextEnvelope {
868 index,
869 envelope: line_envelope(&overlay.geometry),
870 })
871 .collect(),
872 );
873 for edge in &mut graph.edges {
874 let mut touched = false;
875 for candidate in index.locate_in_envelope_intersecting(&line_envelope(&edge.geometry)) {
876 let overlay = &overlays[candidate.index];
877 if same_osm_way(edge, overlay) {
878 continue;
879 }
880 let count = crossing_count(&edge.geometry, &overlay.geometry);
881 if count == 0 {
882 continue;
883 }
884 touched = true;
885 crossings += usize::try_from(count).unwrap_or(usize::MAX);
886 push_crossing(edge, overlay, count);
887 if overlay.kind == CrossingKind::Road {
888 edge.attr.road_exposure =
889 edge.attr.road_exposure.max(road_crossing_exposure(count));
890 }
891 edge.attr.confidence = edge.attr.confidence.min(overlay.confidence);
892 }
893 let _ = touched;
894 }
895 crossings
896}
897
898#[derive(Clone, Copy)]
899struct ContextEnvelope {
900 index: usize,
901 envelope: AABB<[f64; 2]>,
902}
903
904impl RTreeObject for ContextEnvelope {
905 type Envelope = AABB<[f64; 2]>;
906
907 fn envelope(&self) -> Self::Envelope {
908 self.envelope
909 }
910}
911
912fn line_envelope(line: &LineString) -> AABB<[f64; 2]> {
913 let first = line.points[0];
914 let (west, south, east, north) = line.points[1..].iter().fold(
915 (first.lon, first.lat, first.lon, first.lat),
916 |(west, south, east, north), point| {
917 (
918 west.min(point.lon),
919 south.min(point.lat),
920 east.max(point.lon),
921 north.max(point.lat),
922 )
923 },
924 );
925 AABB::from_corners([west, south], [east, north])
926}
927
928fn same_osm_way(edge: &Edge, overlay: &ContextOverlay) -> bool {
929 let Some(overlay_id) = overlay.provenance.source_id.as_deref() else {
930 return false;
931 };
932 if !overlay.provenance.source.starts_with("osm-") {
933 return false;
934 }
935 edge.attr.provenance.iter().any(|provenance| {
936 if !provenance.source.starts_with("osm-") {
937 return false;
938 }
939 provenance.source_id.as_deref().is_some_and(|source_id| {
940 source_id == overlay_id
941 || source_id
942 .strip_prefix("way ")
943 .and_then(|rest| rest.strip_prefix(overlay_id))
944 .is_some_and(|tail| tail.is_empty() || tail.starts_with(';'))
945 })
946 })
947}
948
949fn push_terrain_evidence(edge: &mut Edge, overlay: &TerrainOverlay) {
950 let rationale = "terrain overlay";
951 if let Some(existing) = edge.attr.terrain_evidence.iter_mut().find(|x| {
952 x.terrain == overlay.terrain
953 && x.provenance.as_ref() == Some(&overlay.provenance)
954 && x.rationale == rationale
955 }) {
956 existing.confidence = existing.confidence.max(overlay.confidence);
957 return;
958 }
959 edge.attr.terrain_evidence.push(TerrainEvidence {
960 terrain: overlay.terrain,
961 confidence: overlay.confidence,
962 rationale: rationale.to_owned(),
963 provenance: Some(overlay.provenance.clone()),
964 });
965}
966
967#[must_use]
968pub fn edge_midpoint(edge: &Edge) -> Coord {
969 let points = &edge.geometry.points;
970 let mid = points.len() / 2;
971 if points.len().is_multiple_of(2) {
972 points[mid - 1].lerp(points[mid], 0.5)
973 } else {
974 points[mid]
975 }
976}
977
978fn point_in_ring(point: Coord, ring: &[Coord]) -> bool {
979 if ring.len() < 3 {
980 return false;
981 }
982 let mut inside = false;
983 let mut j = ring.len() - 1;
984 for i in 0..ring.len() {
985 let pi = ring[i];
986 let pj = ring[j];
987 let crosses = (pi.lat > point.lat) != (pj.lat > point.lat);
988 if crosses {
989 let lon = (pj.lon - pi.lon).mul_add((point.lat - pi.lat) / (pj.lat - pi.lat), pi.lon);
990 if point.lon < lon {
991 inside = !inside;
992 }
993 }
994 j = i;
995 }
996 inside
997}
998
999fn line_intersects_ring(line: &LineString, ring: &[Coord]) -> bool {
1000 line.points.iter().any(|point| point_in_ring(*point, ring))
1001 || line.points.windows(2).any(|edge| {
1002 ring_segments(ring)
1003 .any(|boundary| segments_cross(edge[0], edge[1], boundary.0, boundary.1))
1004 })
1005}
1006
1007fn ring_segments(ring: &[Coord]) -> impl Iterator<Item = (Coord, Coord)> + '_ {
1008 ring.windows(2)
1009 .map(|segment| (segment[0], segment[1]))
1010 .chain((ring.len() >= 2).then(|| (ring[ring.len() - 1], ring[0])))
1011}
1012
1013fn line_distance_m(a: &LineString, b: &LineString) -> f64 {
1014 a.points
1015 .windows(2)
1016 .flat_map(|lhs| {
1017 b.points.windows(2).map(move |rhs| {
1018 if segments_cross(lhs[0], lhs[1], rhs[0], rhs[1]) {
1019 0.0
1020 } else {
1021 point_segment_distance_m(lhs[0], rhs[0], rhs[1])
1022 .min(point_segment_distance_m(lhs[1], rhs[0], rhs[1]))
1023 .min(point_segment_distance_m(rhs[0], lhs[0], lhs[1]))
1024 .min(point_segment_distance_m(rhs[1], lhs[0], lhs[1]))
1025 }
1026 })
1027 })
1028 .min_by(f64::total_cmp)
1029 .unwrap_or(f64::INFINITY)
1030}
1031
1032fn crossing_count(a: &LineString, b: &LineString) -> u32 {
1033 a.points
1034 .windows(2)
1035 .map(|lhs| {
1036 b.points
1037 .windows(2)
1038 .filter(|rhs| segments_cross(lhs[0], lhs[1], rhs[0], rhs[1]))
1039 .count()
1040 })
1041 .sum::<usize>()
1042 .try_into()
1043 .unwrap_or(u32::MAX)
1044}
1045
1046fn segments_cross(a0: Coord, a1: Coord, b0: Coord, b1: Coord) -> bool {
1047 let d1 = orient(a0, a1, b0);
1048 let d2 = orient(a0, a1, b1);
1049 let d3 = orient(b0, b1, a0);
1050 let d4 = orient(b0, b1, a1);
1051 if d1.abs() <= 1.0e-12 && on_segment(a0, a1, b0)
1052 || d2.abs() <= 1.0e-12 && on_segment(a0, a1, b1)
1053 || d3.abs() <= 1.0e-12 && on_segment(b0, b1, a0)
1054 || d4.abs() <= 1.0e-12 && on_segment(b0, b1, a1)
1055 {
1056 return true;
1057 }
1058 (d1 > 0.0) != (d2 > 0.0) && (d3 > 0.0) != (d4 > 0.0)
1059}
1060
1061fn orient(a: Coord, b: Coord, c: Coord) -> f64 {
1062 (b.lon - a.lon).mul_add(c.lat - a.lat, -(b.lat - a.lat) * (c.lon - a.lon))
1063}
1064
1065fn on_segment(a: Coord, b: Coord, p: Coord) -> bool {
1066 (a.lon.min(b.lon) - 1.0e-12..=a.lon.max(b.lon) + 1.0e-12).contains(&p.lon)
1067 && (a.lat.min(b.lat) - 1.0e-12..=a.lat.max(b.lat) + 1.0e-12).contains(&p.lat)
1068}
1069
1070fn push_crossing(edge: &mut Edge, overlay: &ContextOverlay, count: u32) {
1071 if let Some(existing) = edge
1072 .attr
1073 .crossings
1074 .iter_mut()
1075 .find(|x| x.kind == overlay.kind && x.provenance == overlay.provenance)
1076 {
1077 existing.count = existing.count.max(count);
1078 return;
1079 }
1080 edge.attr.crossings.push(CrossingEvidence {
1081 kind: overlay.kind,
1082 count,
1083 provenance: overlay.provenance.clone(),
1084 });
1085}
1086
1087fn road_crossing_exposure(count: u32) -> f64 {
1088 (f64::from(count) * 0.03).clamp(0.0, 0.20)
1089}
1090
1091fn point_segment_distance_m(point: Coord, start: Coord, end: Coord) -> f64 {
1092 let lat_scale = 111_320.0;
1093 let lon_scale = lat_scale * point.lat.to_radians().cos().abs().max(0.01);
1094 let point_x = point.lon * lon_scale;
1095 let point_y = point.lat * lat_scale;
1096 let start_x = start.lon * lon_scale;
1097 let start_y = start.lat * lat_scale;
1098 let end_x = end.lon * lon_scale;
1099 let end_y = end.lat * lat_scale;
1100 let delta_x = end_x - start_x;
1101 let delta_y = end_y - start_y;
1102 let denom = delta_x.mul_add(delta_x, delta_y * delta_y);
1103 if denom <= f64::EPSILON {
1104 return (point_x - start_x).hypot(point_y - start_y);
1105 }
1106 let projection = ((point_y - start_y).mul_add(delta_y, (point_x - start_x) * delta_x) / denom)
1107 .clamp(0.0, 1.0);
1108 let closest_x = delta_x.mul_add(projection, start_x);
1109 let closest_y = delta_y.mul_add(projection, start_y);
1110 (point_x - closest_x).hypot(point_y - closest_y)
1111}
1112
1113pub fn polygon(ring: Vec<Coord>) -> Result<OverlayGeometry> {
1114 if ring.len() < 4 {
1115 return Err(TrailgenError::InvalidGeometry(
1116 "overlay polygon ring needs at least four coordinates".to_owned(),
1117 ));
1118 }
1119 Ok(OverlayGeometry::Polygon(ring))
1120}