Skip to main content

trailgen_core/
overlay.rs

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}