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
pub use ring360::*;

/// Defines an aspect result
/// All angle pairs have an aspect, but only some match the target within the specified orb (± tolerance)
#[derive(Debug, Clone, Copy)]
pub struct AspectResult(pub f64, pub f64, pub f64, pub bool, pub f64);

impl AspectResult {
  pub fn calculate(target: f64, angle: f64, orb: f64) -> Self {
    let mut distance = target.angle_360(angle);
    if target < Ring360::half_turn() && target > 0f64 {
      let inverse_target = Ring360::BASE - target;
      let distance_2 = inverse_target.angle_360(angle);
      if distance_2.abs() < distance.abs() {
        distance = distance_2;
      }
    }
    let neg_orb = 0f64 - orb;
    let distance_abs = distance.abs();
    let in_range = distance_abs >= neg_orb && distance_abs <= orb;
    AspectResult(angle, target, distance, in_range, orb)
  }

  /// aspect between two angles
  pub fn aspect(&self) -> f64 {
    self.0
  }

  /// target aspect between two angles
  pub fn target(&self) -> f64 {
    self.1
  }

  /// distance between the true aspect and the target, may be negative or positive depending on the direction
  pub fn distance(&self) -> f64 {
    self.2
  }

  /// absolute distance between the true aspect and the target. May only be positive
  pub fn divergence(&self) -> f64 {
    self.2.abs()
  }

  /// does the aspect distance from the target fall within the specified range (orb)
  pub fn matched(&self) -> bool {
    self.3
  }

  /// ± tolerance or range for a valid match
  pub fn orb(&self) -> f64 {
    self.4
  }
}

/// Defines a target aspect with its orb (± tolerance). Used with the find_aspect() method defined in Aspect360
/// All aspects are symmetrical e.g. 120º will also match 240º or -120º
#[derive(Debug, Clone, Copy)]
pub struct AspectOrb(pub f64, pub f64);

impl AspectOrb {

  /// target aspect
  pub fn target(&self) -> f64 {
      self.0
  }

  /// ± tolerance or orb for a valid match
  pub fn orb(&self) -> f64 {
      self.1
  }
}

/// Provides methods to calculate aspect matches from f64 values cast to Ring360 with a target aspect and orb (± tolerance)
pub trait Aspect360 {

  /// Calculate an aspect result with a symmetrical flag (i.e. if false may only be the ± target, 90º => ±90º)
  fn calc_aspect(&self, other: &Ring360, target: f64, orb: f64) -> AspectResult;

  /// find the first matched aspect. If no aspects fall within the specified orbs, None will be returned
  /// This method is faster than calling find_best_aspect, as it will return first matched target aspect and not evaulate any others
  /// It's preferable to find_best_aspect where 
  fn find_aspect(&self, other: &Ring360, targets: &[AspectOrb]) -> Option<AspectResult> {
    for aspect_orb in targets {
        let aspect = self.calc_aspect(other, aspect_orb.target(), aspect_orb.orb());
        if aspect.matched() {
            return Some(aspect);
        }
    }
    None
  }

  /// find all matching aspects, where they may potentially overlap
  fn find_aspects(&self, other: &Ring360, targets: &[AspectOrb]) -> Vec<AspectResult> {
    let mut matched_aspects: Vec<AspectResult> = Vec::new();
    for aspect_orb in targets {
        let aspect = self.calc_aspect(other, aspect_orb.target(), aspect_orb.orb());
        if aspect.matched() {
          matched_aspects.push(aspect);
        }
    }
    matched_aspects
  }

  /// Find the nearest matching aspect, if two aspects could potentially overlap.
  /// The method will return the nearest aspect wrapped in a Some Option.
  /// If no aspects fall within the specified orbs, None will be returned
  fn find_best_aspect(&self, other: &Ring360, targets: &[AspectOrb]) -> Option<AspectResult> {
    let mut matched_aspects = self.find_aspects(other, targets);
    if matched_aspects.is_empty() {
      None
    } else {
      matched_aspects.sort_by(|a, b| a.divergence().partial_cmp(&b.divergence()).unwrap());
      matched_aspects.first().map(|ar| *ar)
    }
  }

  /// Calculate an aspect from a normal f64 value representing a degree
  fn calc_aspect_f64(&self, other: f64, target: f64, orb: f64) -> AspectResult {
    self.calc_aspect(&other.to_360(), target, orb)
  }

  /// Calculate an aspect with symmetrical logic and return true if it's within the orb
  fn is_aspected(&self, other: &Ring360, target: f64, orb: f64) -> bool {
    self.calc_aspect(other, target, orb).matched()
  }

  /// Calculate an aspect with symmetrical logic from a normal f64 value and return true if it's within the orb
  fn is_aspected_f64(&self, other: f64, target: f64, orb: f64) -> bool {
    self.calc_aspect(&other.to_360(), target, orb).matched()
  }
}

/// Implement only the core calc_aspect() method from which all other extension methods derive
impl Aspect360 for Ring360 {

  /// Calculate an aspect result with a symmetrical flag (i.e. if false may only be the ± target, 90º => ±90º)
  fn calc_aspect(&self, other: &Ring360, target: f64, orb: f64) -> AspectResult {
    let angle = self.angle(*other);
    AspectResult::calculate(target, angle, orb)
  }

}

/// Provide method to cast simple (f64, f64) tuples to a vetcor AspectOrb tuple structs
pub trait ToAspectOrbs {
  fn to_aspect_orbs(&self) -> Vec<AspectOrb>;
}

/// Provide method to cast arrays or vectors of (f64, f64) tuples to a vector of AspectOrb objects
impl ToAspectOrbs for [(f64, f64)] {
  fn to_aspect_orbs(&self) -> Vec<AspectOrb> {
      self.into_iter().map(|(aspect, orb)| AspectOrb(*aspect, *orb)).collect()
  }
}