1 /*
2 	 -------------------------------------------------------------------
3 
4 	 Copyright (C) 2014, Edwin van Leeuwen
5 
6 	 This file is part of plotd plotting library.
7 
8 	 Plotd is free software; you can redistribute it and/or modify
9 	 it under the terms of the GNU General Public License as published by
10 	 the Free Software Foundation; either version 3 of the License, or
11 	 (at your option) any later version.
12 
13 	 Plotd is distributed in the hope that it will be useful,
14 	 but WITHOUT ANY WARRANTY; without even the implied warranty of
15 	 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
16 	 GNU General Public License for more details.
17 
18 	 You should have received a copy of the GNU General Public License
19 	 along with Plotd. If not, see <http://www.gnu.org/licenses/>.
20 
21 	 -------------------------------------------------------------------
22 	 */
23 
24 module plotd.primitives;
25 
26 import std.algorithm : min, max;
27 import std.conv;
28 import std.math;
29 import std.stdio;
30 import std.string;
31 
32 version( unittest ) {
33 	import std.algorithm : take;
34 }
35 
36 /// Color class using rgba representation internally
37 class Color {
38     double r = 1, g = 1, b = 1, a = 1;
39     this( double red, double green, double blue, double alpha ) { 
40         r = red; g = green; b = blue; a = alpha; }
41 
42     this( string value ) {
43         auto rgba = value.split( "," );
44         assert( rgba.length == 4 );
45         r = to!double(rgba[0]); g = to!double(rgba[1]); 
46         b = to!double(rgba[2]); a = to!double(rgba[3]);
47     }
48 
49     unittest {
50         assert( new Color( "0.1,0.2,0.3,0.4" ) == new Color( 0.1, 0.2, 0.3, 0.4 ) );
51     }
52 
53     override bool opEquals( Object o ) const {
54         auto color = cast( typeof( this ) ) o;
55         return ( r == color.r &&
56                 g == color.g &&
57                 b == color.b &&
58                 a == color.a );
59     }
60 
61     static Color black() {
62         auto color = new Color( 0, 0, 0, 1 );
63         return color;
64     }
65 
66     static Color white() {
67         auto color = new Color( 1, 1, 1, 1 );
68         return color;
69     }
70 
71     unittest {
72         auto col1 = Color.black;
73         auto col2 = Color.black;
74         assert( col1.r == col2.r );
75         assert( col1.g == col2.g );
76         assert( col1.b == col2.b );
77         assert( col1.a == col2.a );
78         assert ( col1.opEquals( col2 ) );
79         assert ( col1 == col2 );
80     }
81 }
82 
83 /// Infinite range of colors
84 struct ColorRange {
85 	@property bool empty() const
86 	{
87 		return false;
88 	}
89 
90 	@property Color front() {
91 		return new Color( r, g, b, a );
92 	}
93 
94 	void popFront() {
95 		if ( r == 0 && g == 0 && b == 0 )
96 			r = 1;
97 		else if ( r == 1 && g == 0 && b == 0 )
98 		{
99 			r = 0; // Skip yellow, because difficult to see on white
100 			g = 1;
101 		}
102 		else if ( r == 0 && g == 1 && b == 0 )
103 			b = 1;
104 		else if ( r == 0 && g == 1 && b == 1 )
105 			g = 0;
106 		else if ( r == 0 && g == 0 && b == 1 ) {
107 			b = 0;
108 		}
109 	}
110 
111 	private:
112 		double r = 0;
113 		double g = 0;
114 		double b = 0;
115 		double a = 1;
116 }
117 
118 unittest {
119 	Color prevColor = Color.white;
120 	ColorRange colorRange;
121 	foreach( col ; take( colorRange, 15 ) ) {
122 		assert( prevColor != col );
123 		prevColor = col;
124 	}
125 }
126 
127 /// Bounds struct holding the bounds (min_x, max_x, min_y, max_y)
128 struct Bounds {
129     double min_x;
130     double max_x;
131     double min_y;
132     double max_y;
133 
134     this( double my_min_x, double my_max_x, double my_min_y, double my_max_y ) {
135         min_x = my_min_x;
136         max_x = my_max_x;
137         min_y = my_min_y;
138         max_y = my_max_y;
139     }
140 
141     this( string value ) {
142         auto bnds = value.split( "," );
143         assert( bnds.length == 4 );
144         min_x = to!double(bnds[0]); max_x = to!double(bnds[1]); 
145         min_y = to!double(bnds[2]); max_y = to!double(bnds[3]);
146     }
147 
148     unittest {
149         assert( Bounds( "0.1,0.2,0.3,0.4" ) == Bounds( 0.1, 0.2, 0.3, 0.4 ) );
150     }
151 }
152 
153 /// Return the height of the given bounds 
154 double height( Bounds bounds ) {
155 	return bounds.max_y-bounds.min_y;
156 }
157 
158 unittest {
159 	assert( Bounds(0,1.5,1,5).height == 4 );
160 }
161 
162 /// Return the width of the given bounds 
163 double width( Bounds bounds ) {
164 	return bounds.max_x-bounds.min_x;
165 }
166 
167 unittest {
168 	assert( Bounds(0,1.5,1,5).width == 1.5 );
169 }
170 
171 /// Is the point within the Bounds
172 bool withinBounds( Bounds bounds, Point point ) {
173 	return ( point.x <= bounds.max_x && point.x >= bounds.min_x
174 			&& point.y <= bounds.max_y && point.y >= bounds.min_y );
175 }
176 
177 unittest {
178 	assert( Bounds( 0, 1, 0, 1 ).withinBounds( Point( 1, 0 ) ) );
179 	assert( Bounds( 0, 1, 0, 1 ).withinBounds( Point( 0, 1 ) ) );
180 	assert( !Bounds( 0, 1, 0, 1 ).withinBounds( Point( 0, 1.1 ) ) );
181 	assert( !Bounds( 0, 1, 0, 1 ).withinBounds( Point( -0.1, 1 ) ) );
182 	assert( !Bounds( 0, 1, 0, 1 ).withinBounds( Point( 1.1, 0.5 ) ) );
183 	assert( !Bounds( 0, 1, 0, 1 ).withinBounds( Point( 0.1, -0.1 ) ) );
184 }
185 
186 /// Returns adjust bounds based on given bounds to include point
187 Bounds adjustedBounds( Bounds bounds, Point point ) {
188 	if ( bounds.min_x > point.x ) {
189 		bounds.min_x = min( bounds.min_x - 0.1*bounds.width, point.x );
190 	} else if ( bounds.max_x < point.x ) {
191 		bounds.max_x = max( bounds.max_x + 0.1*bounds.width, point.x );
192 	}
193 	if ( bounds.min_y > point.y ) {
194 		bounds.min_y = min( bounds.min_y - 0.1*bounds.height, point.y );
195 	} else if ( bounds.max_y < point.y ) {
196 		bounds.max_y = max( bounds.max_y + 0.1*bounds.height, point.y );
197 	}
198 	return bounds;
199 }
200 
201 unittest {
202 	assert( adjustedBounds( Bounds( 0, 1, 0, 1 ), Point( 0, 1.01 ) ) ==
203 			Bounds( 0, 1, 0, 1.1 ) );
204 	assert( adjustedBounds( Bounds( 0, 1, 0, 1 ), Point( 0, 1.5 ) ) ==
205 			Bounds( 0, 1, 0, 1.5 ) );
206 	assert( adjustedBounds( Bounds( 0, 1, 0, 1 ), Point( -1, 1.01 ) ) ==
207 			Bounds( -1, 1, 0, 1.1 ) );
208 	assert( adjustedBounds( Bounds( 0, 1, 0, 1 ), Point( 1.2, -0.01 ) ) ==
209 			Bounds( 0, 1.2, -0.1, 1 ) );
210 }
211 
212 /// Can we construct valid bounds given these points
213 bool validBounds( Point[] points ) {
214 	if (points.length < 2)
215 		return false;
216 
217 	bool validx = false;
218 	bool validy = false;
219 	double x = points[0].x;
220 	double y = points[0].y;
221 
222 	foreach( point; points[1..$] ) {
223 		if ( point.x != x )
224 			validx = true;
225 		if ( point.y != y )
226 			validy = true;
227 		if (validx && validy)
228 			return true;
229 	}
230 	return false;
231 }
232 
233 unittest {
234 	assert( validBounds( [ Point( 0, 1 ), Point( 1, 0 ) ] ) );
235 	assert( !validBounds( [ Point( 0, 1 ) ] ) );
236 	assert( !validBounds( [ Point( 0, 1 ), Point( 0, 0 ) ] ) );
237 	assert( !validBounds( [ Point( 0, 1 ), Point( 1, 1 ) ] ) );
238 }
239 
240 Bounds minimalBounds( Point[] points ) {
241 	if (points.length == 0)
242 		return Bounds( -1,1,-1,1 );
243 
244 	double min_x = points[0].x;
245 	double max_x = points[0].x;
246 	double min_y = points[0].y;
247 	double max_y = points[0].y;
248 	if (points.length > 1) {
249 		foreach( point; points[1..$] ) {
250 			if ( point.x < min_x )
251 				min_x = point.x;
252 			else if ( point.x > max_x )
253 				max_x = point.x;
254 			if ( point.y < min_y )
255 				min_y = point.y;
256 			else if ( point.y > max_y )
257 				max_y = point.y;
258 		}
259 	}
260 	if (min_x == max_x) {
261 		min_x = min_x - 0.5;
262 		max_x = max_x + 0.5;
263 	}
264 	if (min_y == max_y) {
265 		min_y = min_y - 0.5;
266 		max_y = max_y + 0.5;
267 	}
268 	return Bounds( min_x, max_x, min_y, max_y );
269 }
270 
271 unittest {
272 	assert( minimalBounds( [] ) == Bounds( -1, 1, -1, 1 ) );
273 	assert( minimalBounds( [Point(0,0)] ) == Bounds( -0.5, 0.5, -0.5, 0.5 ) );
274 	assert( minimalBounds( [Point(0,0),Point(0,0)] ) == Bounds( -0.5, 0.5, -0.5, 0.5 ) );
275 	assert( minimalBounds( [Point(0.1,0),Point(0,0.2)] ) == Bounds( 0, 0.1, 0, 0.2 ) );
276 }
277 
278 struct Point {
279     double x;
280     double y;
281     this( double my_x, double my_y ) {
282         x = my_x;
283         y = my_y;
284     }
285 
286     this( string value ) {
287         auto coords = value.split( "," );
288         assert( coords.length == 2 );
289         x = to!double(coords[0]); 
290         y = to!double(coords[1]);
291     }
292 
293     unittest {
294         assert( Point( "1.0,0.1" ) == Point( 1.0, 0.1 ) );
295     }
296 
297     bool opEquals( const Point point ) {
298         return point.x == x && point.y == y;
299     }
300 }
301 
302 Point convertCoordinates( const Point point, const Bounds orig_bounds, 
303         const Bounds new_bounds ) {
304     double new_x = new_bounds.min_x + (new_bounds.max_x-new_bounds.min_x)*(point.x-orig_bounds.min_x)/(orig_bounds.max_x-orig_bounds.min_x);
305     double new_y = new_bounds.min_y + (new_bounds.max_y-new_bounds.min_y)*(point.y-orig_bounds.min_y)/(orig_bounds.max_y-orig_bounds.min_y);
306     return Point( new_x, new_y );
307 }
308 
309 unittest {
310     assert( convertCoordinates( Point( 0.5, 0.1 ), Bounds( -1, 1, -1, 1 ),
311                 Bounds( 50, 100, -100, -50 ) ) == Point( 87.5, -72.5 ) );
312 }
313 
314 alias int LineId;
315 
316 class LineState {
317     Color color;
318     Point end_point;
319     LineId id;
320 }
321 
322 class Lines {
323 
324     /// Returns an unused (new) line_id
325     LineId newLineId() {
326         last_id++;
327         return last_id;
328     }
329 
330     /// Add a new line with begin point, or add to an existing line
331     void addLine( LineId id, Point point ) {
332         LineState state;
333         lines.get( id, state );
334         if ( state is null ) {
335             state = new LineState;
336             state.color = Color.black;
337             state.id = id;
338             lines[id] = state;
339         }
340         lines[id].end_point = point;
341         mylastUsedId = id; // Keeping track of the last used line id
342     }
343 
344     unittest {
345         auto lines = new Lines;
346         lines.addLine( 1, Point( 1, 2 ) );
347         assert( lines.lines.length == 1 );
348         assert( lines.lines[1].color == Color.black ); // Should implement equals for color
349         assert( lines.lines[1].end_point == Point( 1, 2 ) );
350         lines.addLine( 1, Point( 2, 2 ) );
351         assert( lines.lines[1].end_point == Point( 2, 2 ) );
352     }
353 
354     void color( LineId id, Color color ) {
355         lines[id].color = color;
356         mylastUsedId = id; // Keeping track of the last used line id
357     }
358 
359     unittest {
360         auto lines = new Lines;
361         lines.addLine( 1, Point( 1, 2 ) );
362         assert( lines.lines.length == 1 );
363         assert( lines.lines[1].color == Color.black ); 
364         lines.color( 1, new Color( 0.5, 0.5, 0.5, 0.5 ) );
365         assert( lines.lines[1].color == new Color( 0.5, 0.5, 0.5, 0.5 ) ); 
366     }
367 
368     /// Return last used id
369     @property LineId lastUsedId() {
370         return mylastUsedId;
371     }
372 
373     unittest {
374         auto lines = new Lines;
375         lines.addLine( 1, Point( 1, 2 ) );
376         assert( lines.lastUsedId == 1 );
377         lines.addLine( 2, Point( 1, 2 ) );
378         assert( lines.lastUsedId == 2 );
379         lines.addLine( 1, Point( 1, 1 ) );
380         assert( lines.lastUsedId == 1 );
381         lines.color( 2, new Color( 0.5, 0.5, 0.5, 0.5 ) );
382         assert( lines.lastUsedId == 2 );
383     }
384 
385     private:
386         LineState[LineId] lines;
387         LineId last_id = 0;
388         LineId mylastUsedId = 0;
389 }
390 
391 class Axis {
392     this( double newmin, double newmax ) {
393         min = newmin;
394         max = newmax;
395         min_tick = min;
396     }
397     string label;
398     double min = -1;
399     double max = 1;
400     double min_tick = -1;
401     double tick_width = 0.2;
402 }
403 
404 /**
405     Calculate optimal tick width given an axis and an approximate number of ticks
406     */
407 Axis adjustTickWidth( Axis axis, size_t approx_no_ticks ) {
408     auto axis_width = axis.max-axis.min;
409     auto scale = cast(int) floor(log10( axis_width ));
410     auto acceptables = [ 0.1, 0.2, 0.5, 1.0, 2.0, 5.0 ]; // Only accept ticks of these sizes
411     auto approx_width = pow(10.0, -scale)*(axis_width)/approx_no_ticks;
412     // Find closest acceptable value
413     double best = acceptables[0];
414     double diff = abs( approx_width - best );
415     foreach ( accept; acceptables[1..$] ) { 
416         if (abs( approx_width - accept ) < diff) {
417             best = accept;
418             diff = abs( approx_width - accept );
419         }
420     }
421 
422     axis.tick_width = best*pow(10.0, scale);
423 
424     // Find good min_tick
425     axis.min_tick = ceil(axis.min*pow(10.0, -scale))*pow(10.0, scale);
426 
427 		//debug writeln( "Here 120 ", axis.min_tick, " ", axis.min, " ", 
428 		//		axis.max,	" ", axis.tick_width, " ", scale );
429     while (axis.min_tick - axis.tick_width > axis.min)
430         axis.min_tick -= axis.tick_width;
431     return axis;
432 }
433 
434 unittest {
435     adjustTickWidth( new Axis( 0, .4 ), 5 );
436     adjustTickWidth( new Axis( 0, 4 ), 8 );
437     assert( adjustTickWidth( new Axis( 0, 4 ), 5 ).tick_width == 1.0 );
438     assert( adjustTickWidth( new Axis( 0, 4 ), 8 ).tick_width == 0.5 );
439     assert( adjustTickWidth( new Axis( 0, 0.4 ), 5 ).tick_width == 0.1 );
440     assert( adjustTickWidth( new Axis( 0, 40 ), 8 ).tick_width == 5 );
441     assert( adjustTickWidth( new Axis( -0.1, 4 ), 8 ).tick_width == 0.5 );
442     
443    
444     assert( adjustTickWidth( new Axis( -0.1, 4 ), 8 ).min_tick == 0.0 );
445     assert( adjustTickWidth( new Axis( 0.1, 4 ), 8 ).min_tick == 0.5 );
446     assert( adjustTickWidth( new Axis( 1, 40 ), 8 ).min_tick == 5 );
447 
448     assert( adjustTickWidth( new Axis( 3, 4 ), 5 ).min_tick == 3 );
449     assert( adjustTickWidth( new Axis( 3, 4 ), 5 ).tick_width == 0.2 );
450 
451 		assert( adjustTickWidth( 
452 					new Axis( 1.79877e+07, 1.86788e+07 ), 5).min_tick == 1.8e+07 );
453 		assert( adjustTickWidth( 
454 					new Axis( 1.79877e+07, 1.86788e+07 ), 5).tick_width == 100000 );
455 }
456 
457 /// Calculate tick length
458 double tickLength( const Axis axis ) {
459     return (axis.max-axis.min)/25.0;
460 }
461 
462 unittest {
463     auto axis = new Axis( -1, 1 );
464     assert( tickLength( axis ) == 0.08);
465 }