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.drawing;
25 import std.conv;
26 
27 import cairo = cairo;
28 
29 import plotd.primitives;
30 import plotd.binning;
31 
32 version( unittest ) {
33 	import std.stdio;
34 }
35 version( assert ) {
36 	import std.stdio;
37 }
38 // Design: One surface per plot (this makes it easier for PDFSurface support
39 // Get axes context
40 // Get plot context ( probably by first getting a subsurface from the main surface )
41 
42 /// Create the plot surface with given width and height in pixels
43 cairo.Surface createPlotSurface( int width = 400, int height = 400 ) {
44     auto surface = new cairo.ImageSurface(
45             cairo.Format.CAIRO_FORMAT_ARGB32, width, height );
46     auto context = cairo.Context( surface );
47     clearContext( context );
48     return surface;
49 }
50 
51 /// Save surface to a file
52 void save( cairo.Surface surface, string name = "example.png" ) {
53     (cast(cairo.ImageSurface)( surface )).writeToPNG( name );
54 }
55 
56 /// Get axesContext from a surface
57 cairo.Context axesContextFromSurface( cairo.Surface surface, Bounds plotBounds,
58 			Bounds marginBounds = Bounds( 100,400,100,400 )) {
59     auto context = cairo.Context( surface );
60 
61     context.translate( marginBounds.min_x, height( marginBounds ) );
62     context.scale( marginBounds.width/plotBounds.width, 
63             -marginBounds.height/plotBounds.height );
64     context.translate( -plotBounds.min_x, -plotBounds.min_y );
65     context.setFontSize( 14.0 );
66     return context;
67 }
68 
69 /// Get plotContext from a surface
70 cairo.Context plotContextFromSurface( cairo.Surface surface, Bounds plotBounds,
71 			Bounds marginBounds = Bounds( 100,400,100,400 ) ) {
72     // Create a sub surface. Makes sure everything is plotted within plot surface
73     auto plotSurface = cairo.Surface.createForRectangle( surface, 
74             cairo.Rectangle!double( marginBounds.min_x, 0, // No support for margin at top yet. Would need to know the surface dimensions
75 							marginBounds.width, marginBounds.height ) );
76     auto context = cairo.Context( plotSurface );
77     context.translate( 0, marginBounds.height );
78     context.scale( marginBounds.width/plotBounds.width, 
79             -marginBounds.height/plotBounds.height );
80     context.translate( -plotBounds.min_x, -plotBounds.min_y );
81     context.setFontSize( 14.0 );
82     return context;
83 }
84 
85 /** Draw point onto context
86     
87   Template function to make it Mockable
88 
89   */
90 CONTEXT drawPoint(CONTEXT)( const Point point, CONTEXT context ) {
91 	auto width_height = context.deviceToUserDistance( 
92 			cairo.Point!double( 6.0, 6.0 ) );
93 	context.rectangle(
94 			point.x-width_height.x/2.0, point.y-width_height.y/2.0, 
95 			width_height.x, width_height.y );
96 	context.fill();
97 	return context;
98 }
99 
100 CONTEXT drawLine(CONTEXT)( const Point from, const Point to, CONTEXT context ) {
101     context.moveTo( from.x, from.y );
102     context.lineTo( to.x, to.y );
103     context.save();
104     context.identityMatrix();
105     context.stroke();
106     context.restore();
107     return context;
108 }
109 
110 unittest {
111     import dmocks.mocks;
112     auto mocker = new Mocker();
113 
114     auto surface = createPlotSurface();
115     auto mock = mocker.mockStruct!(cairo.Context, cairo.Surface )(
116             surface ); 
117 
118     mocker.expect(mock.moveTo( 0.0, 0.0 )).repeat(1);
119     mocker.expect(mock.lineTo( -1.0, -1.0 )).repeat(1);
120     mocker.expect(mock.stroke()).repeat(1);
121     mocker.expect(mock.save()).repeat(1);
122     mocker.expect(mock.identityMatrix()).repeat(1);
123     mocker.expect(mock.restore()).repeat(1);
124     mocker.replay;
125     drawLine( Point( 0, 0 ), Point( -1, -1 ), mock );
126     mocker.verify;
127 }
128 
129 /**
130   Draw axes onto the given context
131   */
132 CONTEXT drawAxes(CONTEXT)( const Bounds bounds, CONTEXT context ) {
133 
134     auto xaxis = new Axis( bounds.min_x, bounds.max_x );
135     xaxis = adjustTickWidth( xaxis, 5 );
136 
137     auto yaxis = new Axis( bounds.min_y, bounds.max_y );
138     yaxis = adjustTickWidth( yaxis, 5 );
139 
140     // Draw xaxis
141     context = drawLine( Point( xaxis.min, yaxis.min ), 
142             Point( xaxis.max, yaxis.min ), context );
143     // Draw ticks
144     auto tick_x = xaxis.min_tick;
145     auto tick_size = tickLength(yaxis);
146     while( tick_x < xaxis.max ) {
147         context = drawLine( Point( tick_x, yaxis.min ),
148             Point( tick_x, yaxis.min + tick_size ), context );
149 
150 				context.save;
151 				context.identityMatrix;
152 				auto extents = context.textExtents( tick_x.to!string );
153 				auto textSize = cairo.Point!double( 0.5*extents.width, 
154 						-extents.height );
155 				context.restore;
156 				textSize = context.deviceToUserDistance( textSize );
157         context = drawText( tick_x.to!string, 
158                 Point( tick_x - textSize.x, yaxis.min - 1.5*textSize.y ), context );
159         tick_x += xaxis.tick_width;
160     }
161 
162     // Draw yaxis
163     context = drawLine( Point( xaxis.min, yaxis.min ), 
164             Point( xaxis.min, yaxis.max ), context );
165     // Draw ticks
166     auto tick_y = yaxis.min_tick;
167     tick_size = tickLength(xaxis);
168     while( tick_y < yaxis.max ) {
169         context = drawLine( Point( xaxis.min, tick_y ),
170             Point( xaxis.min + tick_size, tick_y ), context );
171 				context.save;
172 				context.identityMatrix;
173 				auto extents = context.textExtents( tick_y.to!string );
174 				auto textSize = cairo.Point!double( extents.height, 
175 						-0.5*extents.width );
176 				context.restore;
177 				textSize = context.deviceToUserDistance( textSize );
178         context = drawRotatedText( tick_y.to!string, 
179 							Point( xaxis.min - 0.5*textSize.x, tick_y-textSize.y ), 
180 							1.5*3.14, context );
181         tick_y += yaxis.tick_width;
182     }
183 
184     return context;
185 }
186 
187 /// Draw xlabel
188 CONTEXT drawXLabel(CONTEXT)( string label, Bounds bounds, CONTEXT context ) {
189 	auto extents = context.textExtents( label );
190 	auto textSize = cairo.Point!double( 0.5*extents.width, 
191 			-extents.height );
192 	textSize = context.deviceToUserDistance( textSize );
193 	context = drawText( label, 
194 			Point( bounds.min_x + bounds.width/2.0 - textSize.x, 
195 					bounds.min_y - 3.0*textSize.y ),
196 			context );
197 	return context;
198 }
199 
200 /// Draw ylabel
201 CONTEXT drawYLabel(CONTEXT)( string label, Bounds bounds, CONTEXT context ) {
202 	auto extents = context.textExtents( label );
203 	auto textSize = cairo.Point!double( -extents.height, 
204 			0.5*extents.width );
205 	textSize = context.deviceToUserDistance( textSize );
206 	context = drawRotatedText( label, 
207 			Point( bounds.min_x + 2.0*textSize.x,
208 				bounds.min_y + bounds.height/2.0 + textSize.y ),
209 			1.5*3.14, context );
210 	return context;
211 }
212 
213 
214 /// Draw text at given location
215 CONTEXT drawText(CONTEXT)( string text, const Point location, CONTEXT context ) {
216     context.moveTo( location.x, location.y ); 
217     context.save();
218     context.identityMatrix();
219     context.showText( text );
220     context.restore();
221     return context;
222 }
223 
224 /// Draw rotated text on plot
225 CONTEXT drawRotatedText(CONTEXT)( string text, const Point location, 
226 		double radians, CONTEXT context ) {
227     context.moveTo( location.x, location.y ); 
228     context.save();
229     context.identityMatrix();
230 		context.rotate( radians );
231     context.showText( text );
232     context.restore();
233     return context;
234 }
235 unittest {
236     import dmocks.mocks;
237     auto mocker = new Mocker();
238 
239     auto surface = createPlotSurface();
240     auto mock = mocker.mockStruct!(cairo.Context, cairo.Surface )(
241             surface ); 
242 
243     mocker.expect(mock.moveTo( 0.0, 0.0 )).repeat(1);
244     mocker.expect(mock.save()).repeat(1);
245     mocker.expect(mock.identityMatrix()).repeat(1);
246     mocker.expect(mock.showText( "text" )).repeat(1);
247     mocker.expect(mock.restore()).repeat(1);
248     mocker.replay;
249     drawText( "text", Point( 0, 0 ), mock );
250     mocker.verify;
251 }
252 
253 CONTEXT drawBins( T : size_t, CONTEXT )( CONTEXT context, Bins!T bins ) {
254     foreach( x, count; bins ) {
255         context = drawLine( Point( x, 0 ), 
256                 Point( x, count.to!double ),
257                 context );
258         context = drawLine( Point( x, count.to!double ), 
259                 Point( x + bins.width, count.to!double ),
260                 context );
261         context = drawLine( 
262                 Point( x + bins.width, count.to!double ), 
263                 Point( x + bins.width, 0 ),
264                 context );
265       }
266     return context;
267 }
268 
269 CONTEXT clearContext( CONTEXT )( CONTEXT context ) {
270     context.save();
271     context = color( context, Color.white );
272     context.paint();
273     context.restore();
274     return context;
275 }
276 
277 unittest {
278     import dmocks.mocks;
279     auto mocker = new Mocker();
280 
281     auto surface = createPlotSurface();
282     auto mock = mocker.mockStruct!(cairo.Context, cairo.Surface )(
283             surface );
284     mocker.expect( mock.save() ).repeat(1);
285     mocker.expect( mock.setSourceRGBA( 1, 1, 1, 1 ) ).repeat(1);
286     mocker.expect( mock.paint() ).repeat(1);
287     mocker.expect( mock.restore() ).repeat(1);
288     mocker.replay;
289     clearContext( mock );
290     mocker.verify;
291 }
292 
293 CONTEXT color( CONTEXT )( CONTEXT context, const Color color ) {
294     context.setSourceRGBA( color.r, color.g, color.b, color.a );
295     return context;
296 }