1 module ggplotd.bounds;
2 
3 ///
4 struct Point
5 {
6     double x;
7     double y;
8     this(double my_x, double my_y)
9     {
10         x = my_x;
11         y = my_y;
12     }
13 
14     this(string value)
15     {
16         import std.conv : to;
17         import std.range : split;
18 
19         auto coords = value.split(",");
20         assert(coords.length == 2);
21         x = to!double(coords[0]);
22         y = to!double(coords[1]);
23     }
24 
25     unittest
26     {
27         assert(Point("1.0,0.1") == Point(1.0, 0.1));
28     }
29 
30     bool opEquals(const Point point)
31     {
32         return point.x == x && point.y == y;
33     }
34 
35 }
36 
37 /// Bounds struct holding the bounds (min_x, max_x, min_y, max_y)
38 struct Bounds
39 {
40     double min_x;
41     double max_x;
42     double min_y;
43     double max_y;
44     this(double my_min_x, double my_max_x, double my_min_y, double my_max_y)
45     {
46         min_x = my_min_x;
47         max_x = my_max_x;
48         min_y = my_min_y;
49         max_y = my_max_y;
50     }
51 
52     this(string value)
53     {
54         import std.conv : to;
55         import std.range : split;
56         import std.string : strip;
57 
58         auto bnds = value.strip.split(",");
59         assert(bnds.length == 4);
60         min_x = to!double(bnds[0]);
61         max_x = to!double(bnds[1]);
62         min_y = to!double(bnds[2]);
63         max_y = to!double(bnds[3]);
64     }
65 
66     unittest
67     {
68         assert(Bounds("0.1,0.2,0.3,0.4") == Bounds(0.1, 0.2, 0.3, 0.4));
69         assert(Bounds("0.1,0.2,0.3,0.4\n") == Bounds(0.1, 0.2, 0.3, 0.4));
70     }
71 
72 }
73 
74 /// Return the height of the given bounds 
75 double height(Bounds bounds)
76 {
77     return bounds.max_y - bounds.min_y;
78 }
79 
80 unittest
81 {
82     assert(Bounds(0, 1.5, 1, 5).height == 4);
83 }
84 
85 /// Return the width of the given bounds 
86 double width(Bounds bounds)
87 {
88     return bounds.max_x - bounds.min_x;
89 }
90 
91 unittest
92 {
93     assert(Bounds(0, 1.5, 1, 5).width == 1.5);
94 }
95 
96 /// Is the point within the Bounds
97 bool withinBounds(Bounds bounds, Point point)
98 {
99     return (point.x <= bounds.max_x && point.x >= bounds.min_x
100         && point.y <= bounds.max_y && point.y >= bounds.min_y);
101 }
102 
103 unittest
104 {
105     assert(Bounds(0, 1, 0, 1).withinBounds(Point(1, 0)));
106     assert(Bounds(0, 1, 0, 1).withinBounds(Point(0, 1)));
107     assert(!Bounds(0, 1, 0, 1).withinBounds(Point(0, 1.1)));
108     assert(!Bounds(0, 1, 0, 1).withinBounds(Point(-0.1, 1)));
109     assert(!Bounds(0, 1, 0, 1).withinBounds(Point(1.1, 0.5)));
110     assert(!Bounds(0, 1, 0, 1).withinBounds(Point(0.1, -0.1)));
111 }
112 
113 /// Can we construct valid bounds given these points
114 bool validBounds(Point[] points)
115 {
116     if (points.length < 2)
117         return false;
118     bool validx = false;
119     bool validy = false;
120     double x = points[0].x;
121     double y = points[0].y;
122     foreach (point; points[1 .. $])
123     {
124         if (point.x != x)
125             validx = true;
126         if (point.y != y)
127             validy = true;
128         if (validx && validy)
129             return true;
130     }
131     return false;
132 }
133 
134 unittest
135 {
136     assert(validBounds([Point(0, 1), Point(1, 0)]));
137     assert(!validBounds([Point(0, 1)]));
138     assert(!validBounds([Point(0, 1), Point(0, 0)]));
139     assert(!validBounds([Point(0, 1), Point(1, 1)]));
140 }
141 
142 ///
143 Bounds minimalBounds(Point[] points)
144 {
145     if (points.length == 0)
146         return Bounds(-1, 1, -1, 1);
147     double min_x = points[0].x;
148     double max_x = points[0].x;
149     double min_y = points[0].y;
150     double max_y = points[0].y;
151     if (points.length > 1)
152     {
153         foreach (point; points[1 .. $])
154         {
155             if (point.x < min_x)
156                 min_x = point.x;
157             else if (point.x > max_x)
158                 max_x = point.x;
159             if (point.y < min_y)
160                 min_y = point.y;
161             else if (point.y > max_y)
162                 max_y = point.y;
163         }
164     }
165     if (min_x == max_x)
166     {
167         min_x = min_x - 0.5;
168         max_x = max_x + 0.5;
169     }
170     if (min_y == max_y)
171     {
172         min_y = min_y - 0.5;
173         max_y = max_y + 0.5;
174     }
175     return Bounds(min_x, max_x, min_y, max_y);
176 }
177 
178 unittest
179 {
180     assert(minimalBounds([]) == Bounds(-1, 1, -1, 1));
181     assert(minimalBounds([Point(0, 0)]) == Bounds(-0.5, 0.5, -0.5, 0.5));
182     assert(minimalBounds([Point(0, 0), Point(0, 0)]) == Bounds(-0.5, 0.5, -0.5, 0.5));
183     assert(minimalBounds([Point(0.1, 0), Point(0, 0.2)]) == Bounds(0, 0.1, 0, 0.2));
184 }
185 
186 /// Returns adjust bounds based on given bounds to include point
187 Bounds adjustedBounds(Bounds bounds, Point point)
188 {
189     import std.algorithm : min, max;
190 
191     if (bounds.min_x > point.x)
192     {
193         bounds.min_x = min(bounds.min_x - 0.1 * bounds.width, point.x);
194     }
195     else if (bounds.max_x < point.x)
196     {
197         bounds.max_x = max(bounds.max_x + 0.1 * bounds.width, point.x);
198     }
199     if (bounds.min_y > point.y)
200     {
201         bounds.min_y = min(bounds.min_y - 0.1 * bounds.height, point.y);
202     }
203     else if (bounds.max_y < point.y)
204     {
205         bounds.max_y = max(bounds.max_y + 0.1 * bounds.height, point.y);
206     }
207     return bounds;
208 }
209 
210 unittest
211 {
212     assert(adjustedBounds(Bounds(0, 1, 0, 1), Point(0, 1.01)) == Bounds(0, 1, 0, 1.1));
213     assert(adjustedBounds(Bounds(0, 1, 0, 1), Point(0, 1.5)) == Bounds(0, 1, 0, 1.5));
214     assert(adjustedBounds(Bounds(0, 1, 0, 1), Point(-1, 1.01)) == Bounds(-1, 1, 0,
215         1.1));
216     assert(adjustedBounds(Bounds(0, 1, 0, 1), Point(1.2, -0.01)) == Bounds(0, 1.2,
217         -0.1, 1));
218 }
219 
220 ///
221 struct AdaptiveBounds
222 {
223     /*
224 Notes: the main problem with adaptive bounds is the beginning, where we need to
225 make sure we have enough points to form valid bounds (i.e. with width and height
226 > 0). For example if all points fall on a vertical lines, we have no information
227 for the width of the plot
228 
229 Here we take care to always return a valid set of bounds
230 	 */
231 
232     Bounds bounds = Bounds(0, 1, 0, 1);
233     alias bounds this;
234     ///
235     this(string str)
236     {
237         bounds = Bounds(str);
238     }
239 
240     ///
241     this(double my_min_x, double my_max_x, double my_min_y, double my_max_y)
242     {
243         bounds = Bounds(my_min_x, my_max_x, my_min_y, my_max_y);
244     }
245 
246     ///
247     this(Bounds bnds)
248     {
249         bounds = bnds;
250     }
251 
252     ///
253     bool adapt(T : Point)(in T point)
254     {
255         bool adapted = false;
256         if (!valid)
257         {
258             adapted = true;
259             pointCache ~= point;
260             valid = validBounds(pointCache);
261             bounds = minimalBounds(pointCache);
262             if (valid)
263                 pointCache = [];
264         }
265         else
266         {
267             if (!bounds.withinBounds(point))
268             {
269                 bounds = bounds.adjustedBounds(point);
270                 adapted = true;
271             }
272         }
273         assert((valid && pointCache.length == 0) || !valid);
274         return adapted;
275     }
276 
277     ///
278     bool adapt(T : AdaptiveBounds)(in T bounds)
279     {
280         bool adapted = false;
281         if (bounds.valid)
282         {
283             bool adaptMin = adapt(Point(bounds.min_x, bounds.min_y));
284             bool adaptMax = adapt(Point(bounds.max_x, bounds.max_y));
285             adapted = (adaptMin || adaptMax);
286         }
287         else
288         {
289             adapted = adapt(bounds.pointCache);
290         }
291         return adapted;
292     }
293 
294     import std.range : isInputRange;
295 
296     ///
297     bool adapt(T)(in T points)
298     {
299         import std.range : save;
300         bool adapted = false;
301         foreach (point; points.save)
302         {
303             auto a = adapt(point);
304             if (a)
305                 adapted = true;
306         }
307         return adapted;
308     }
309 
310 private:
311 Point[] pointCache;
312     bool valid = false;
313 }
314 
315 unittest
316 {
317     assert(AdaptiveBounds("0.1,0.2,0.3,0.4") == Bounds(0.1, 0.2, 0.3, 0.4));
318     // Test adapt
319     AdaptiveBounds bounds;
320     assert(bounds.width > 0);
321     assert(bounds.height > 0);
322     auto pnt = Point(5, 2);
323     assert(bounds.adapt(pnt));
324     assert(bounds.width > 0);
325     assert(bounds.height > 0);
326     assert(bounds.withinBounds(pnt));
327     assert(!bounds.valid);
328     pnt = Point(3, 2);
329     assert(bounds.adapt(pnt));
330     assert(bounds.width >= 2);
331     assert(bounds.height > 0);
332     assert(bounds.withinBounds(pnt));
333     assert(!bounds.valid);
334     pnt = Point(3, 5);
335     assert(bounds.adapt(pnt));
336     assert(bounds.width >= 2);
337     assert(bounds.height >= 3);
338     assert(bounds.withinBounds(pnt));
339     assert(bounds.valid);
340     pnt = Point(4, 4);
341     assert(!bounds.adapt(pnt));
342 }
343 
344 unittest
345 {
346     AdaptiveBounds bounds;
347     assert(!bounds.valid);
348     AdaptiveBounds bounds2;
349     assert(!bounds.adapt(bounds2));
350 
351     bounds2.adapt(Point(1.1, 1.2));
352     bounds.adapt(bounds2);
353     assert(!bounds.valid);
354     AdaptiveBounds bounds3;
355     bounds3.adapt(Point(1.2, 1.3));
356     bounds.adapt(bounds3);
357     assert(bounds.valid);
358 
359     AdaptiveBounds bounds4;
360     assert(!bounds4.valid);
361     AdaptiveBounds bounds5;
362     bounds5.adapt(Point(1.1, 1.2));
363     bounds5.adapt(Point(1.3, 1.3));
364     assert(bounds5.valid);
365     bounds4.adapt(bounds5);
366     assert(bounds4.valid);
367 }