The OpenD Programming Language

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