• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

geo-engine / geoengine / 3929938005

pending completion
3929938005

push

github

GitHub
Merge #713

84930 of 96741 relevant lines covered (87.79%)

79640.1 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

98.39
/operators/src/processing/circle_merging_quadtree/circle_of_points.rs
1
use std::collections::HashMap;
2
use std::num::NonZeroUsize;
3

4
use geoengine_datatypes::primitives::{Circle, Coordinate2D, TimeInterval};
5

6
use crate::error::Error;
7
use crate::util::Result;
8

9
use super::aggregates::AttributeAggregate;
10
use super::circle_radius_model::CircleRadiusModel;
11

12
/// A `Circle` type that is extended by the amount of submerged points.
13
#[derive(Clone, Debug, PartialEq)]
8✔
14
pub struct CircleOfPoints {
15
    pub circle: Circle,
16
    pub number_of_points: NonZeroUsize,
17
    pub time_aggregate: TimeInterval,
18
    pub attribute_aggregates: HashMap<String, AttributeAggregate>,
19
}
20

21
impl CircleOfPoints {
22
    /// Create a new `CircleOfPoint` by giving a `Cirlce` and a number of submerged points.
23
    pub fn new(
13✔
24
        circle: Circle,
13✔
25
        number_of_points: usize,
13✔
26
        time_aggregate: TimeInterval,
13✔
27
        attribute_aggregates: HashMap<String, AttributeAggregate>,
13✔
28
    ) -> Result<Self> {
13✔
29
        let number_of_points =
13✔
30
            NonZeroUsize::new(number_of_points).ok_or(Error::InputMustBeGreaterThanZero {
13✔
31
                scope: "VisualPointClustering",
13✔
32
                name: "number_of_points",
13✔
33
            })?;
13✔
34

35
        Ok(CircleOfPoints {
13✔
36
            circle,
13✔
37
            number_of_points,
13✔
38
            time_aggregate,
13✔
39
            attribute_aggregates,
13✔
40
        })
13✔
41
    }
13✔
42

43
    pub fn new_with_one_point(
48✔
44
        circle: Circle,
48✔
45
        time_aggregate: TimeInterval,
48✔
46
        attribute_aggregates: HashMap<String, AttributeAggregate>,
48✔
47
    ) -> Self {
48✔
48
        CircleOfPoints {
48✔
49
            circle,
48✔
50
            number_of_points: unsafe { NonZeroUsize::new_unchecked(1) },
48✔
51
            time_aggregate,
48✔
52
            attribute_aggregates,
48✔
53
        }
48✔
54
    }
48✔
55

56
    /// Merge this `CircleOfPoint` with another one.
57
    ///
58
    /// Specify the `min_radius` to influence the computation of the new radius (log growth).
59
    /// This depends on the amount of points which is the sum of the both amounts.
60
    ///
61
    /// The `Circle` center is the weighted center of the two `Circle`s.
62
    // TODO: make merge return a new circle of points?
63
    pub fn merge<C>(&mut self, other: &CircleOfPoints, circle_radius_model: &C)
31✔
64
    where
31✔
65
        C: CircleRadiusModel,
31✔
66
    {
31✔
67
        self.merge_circles_and_number_of_points(other, circle_radius_model);
31✔
68
        self.merge_time_intervals(&other.time_aggregate);
31✔
69
        self.merge_attributes(&other.attribute_aggregates);
31✔
70
    }
31✔
71

72
    #[inline]
73
    fn merge_circles_and_number_of_points<C>(
31✔
74
        &mut self,
31✔
75
        other: &CircleOfPoints,
31✔
76
        circle_radius_model: &C,
31✔
77
    ) where
31✔
78
        C: CircleRadiusModel,
31✔
79
    {
31✔
80
        let total_number_of_points = unsafe {
31✔
81
            NonZeroUsize::new_unchecked(self.number_of_points.get() + other.number_of_points.get())
31✔
82
        };
31✔
83

31✔
84
        let new_center = {
31✔
85
            let total_length = total_number_of_points.get() as f64;
31✔
86
            let new_x = (self.circle.x() * self.number_of_points.get() as f64
31✔
87
                + other.circle.x() * other.number_of_points.get() as f64)
31✔
88
                / total_length;
31✔
89

31✔
90
            let new_y = (self.circle.y() * self.number_of_points.get() as f64
31✔
91
                + other.circle.y() * other.number_of_points.get() as f64)
31✔
92
                / total_length;
31✔
93

31✔
94
            Coordinate2D::new(new_x, new_y)
31✔
95
        };
31✔
96

31✔
97
        self.number_of_points = total_number_of_points;
31✔
98

31✔
99
        self.circle = Circle::from_coordinate(
31✔
100
            &new_center,
31✔
101
            circle_radius_model.calculate_radius(self.number_of_points),
31✔
102
        );
31✔
103
    }
31✔
104

105
    #[inline]
106
    fn merge_attributes(&mut self, other_aggregates: &HashMap<String, AttributeAggregate>) {
31✔
107
        for (attribute, aggregate) in &mut self.attribute_aggregates {
45✔
108
            if let Some(other_aggregate) = other_aggregates.get(attribute) {
14✔
109
                aggregate.merge(other_aggregate);
14✔
110
            } else {
14✔
111
                // use null if not found - but should not happen
×
112
                aggregate.merge(&AttributeAggregate::Null);
×
113
            }
×
114
        }
115
    }
31✔
116

117
    #[inline]
118
    fn merge_time_intervals(&mut self, other_time_interval: &TimeInterval) {
31✔
119
        self.time_aggregate = self.time_aggregate.extend(other_time_interval);
31✔
120
    }
31✔
121

122
    pub fn number_of_points(&self) -> usize {
12✔
123
        self.number_of_points.get()
12✔
124
    }
12✔
125
}
126

127
#[cfg(test)]
128
mod tests {
129
    use crate::processing::circle_merging_quadtree::{
130
        aggregates::MeanAggregator, circle_radius_model::LogScaledRadius,
131
    };
132

133
    use super::*;
134

135
    #[test]
1✔
136
    #[allow(clippy::float_cmp)]
137
    fn test_circle_merging() {
1✔
138
        let mut c1 = CircleOfPoints::new(
1✔
139
            Circle::from_coordinate(&Coordinate2D::new(1.0, 1.0), 1.0),
1✔
140
            1,
1✔
141
            TimeInterval::default(),
1✔
142
            Default::default(),
1✔
143
        )
1✔
144
        .unwrap();
1✔
145
        let c2 = CircleOfPoints::new(
1✔
146
            Circle::from_coordinate(&Coordinate2D::new(2.0, 1.0), 1.0),
1✔
147
            1,
1✔
148
            TimeInterval::default(),
1✔
149
            Default::default(),
1✔
150
        )
1✔
151
        .unwrap();
1✔
152

1✔
153
        let radius_model = LogScaledRadius::new(1.0, 0.).unwrap();
1✔
154

1✔
155
        c1.merge(&c2, &radius_model);
1✔
156

1✔
157
        assert_eq!(c1.number_of_points(), 2);
1✔
158
        assert_eq!(c1.circle.x(), 1.5);
1✔
159
        assert_eq!(c1.circle.y(), 1.0);
1✔
160
        assert_eq!(c1.circle.radius(), 1.0 + 2.0_f64.ln());
1✔
161
    }
1✔
162

163
    #[test]
1✔
164
    #[allow(clippy::float_cmp)]
165
    fn test_circle_merging_with_attribute() {
1✔
166
        let mut c1 = CircleOfPoints::new(
1✔
167
            Circle::from_coordinate(&Coordinate2D::new(1.0, 1.0), 1.0),
1✔
168
            1,
1✔
169
            TimeInterval::default(),
1✔
170
            [(
1✔
171
                "foo".to_string(),
1✔
172
                AttributeAggregate::MeanNumber(MeanAggregator::from_value(42.)),
1✔
173
            )]
1✔
174
            .iter()
1✔
175
            .cloned()
1✔
176
            .collect(),
1✔
177
        )
1✔
178
        .unwrap();
1✔
179
        let c2 = CircleOfPoints::new(
1✔
180
            Circle::from_coordinate(&Coordinate2D::new(2.0, 1.0), 1.0),
1✔
181
            1,
1✔
182
            TimeInterval::default(),
1✔
183
            [(
1✔
184
                "foo".to_string(),
1✔
185
                AttributeAggregate::MeanNumber(MeanAggregator::from_value(44.)),
1✔
186
            )]
1✔
187
            .iter()
1✔
188
            .cloned()
1✔
189
            .collect(),
1✔
190
        )
1✔
191
        .unwrap();
1✔
192

1✔
193
        let radius_model = LogScaledRadius::new(1.0, 0.).unwrap();
1✔
194

1✔
195
        c1.merge(&c2, &radius_model);
1✔
196

1✔
197
        assert_eq!(c1.number_of_points(), 2);
1✔
198
        assert_eq!(c1.circle.x(), 1.5);
1✔
199
        assert_eq!(c1.circle.y(), 1.0);
1✔
200
        assert_eq!(c1.circle.radius(), 1.0 + 2.0_f64.ln());
1✔
201
        assert_eq!(
1✔
202
            c1.attribute_aggregates
1✔
203
                .get("foo")
1✔
204
                .unwrap()
1✔
205
                .mean_number()
1✔
206
                .unwrap()
1✔
207
                .mean,
1✔
208
            43.
1✔
209
        );
1✔
210
    }
1✔
211

212
    #[test]
1✔
213
    #[allow(clippy::float_cmp)]
214
    fn test_circle_merging_with_time() {
1✔
215
        let mut c1 = CircleOfPoints::new(
1✔
216
            Circle::from_coordinate(&Coordinate2D::new(1.0, 1.0), 1.0),
1✔
217
            1,
1✔
218
            TimeInterval::new_unchecked(0, 4),
1✔
219
            Default::default(),
1✔
220
        )
1✔
221
        .unwrap();
1✔
222
        let c2 = CircleOfPoints::new(
1✔
223
            Circle::from_coordinate(&Coordinate2D::new(2.0, 1.0), 1.0),
1✔
224
            1,
1✔
225
            TimeInterval::new_unchecked(5, 10),
1✔
226
            Default::default(),
1✔
227
        )
1✔
228
        .unwrap();
1✔
229

1✔
230
        let radius_model = LogScaledRadius::new(1.0, 0.).unwrap();
1✔
231

1✔
232
        c1.merge(&c2, &radius_model);
1✔
233

1✔
234
        assert_eq!(c1.number_of_points(), 2);
1✔
235
        assert_eq!(c1.circle.x(), 1.5);
1✔
236
        assert_eq!(c1.circle.y(), 1.0);
1✔
237
        assert_eq!(c1.circle.radius(), 1.0 + 2.0_f64.ln());
1✔
238
        assert!(c1.attribute_aggregates.is_empty());
1✔
239
        assert_eq!(c1.time_aggregate, TimeInterval::new_unchecked(0, 10));
1✔
240
    }
1✔
241
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc