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 cli.parsing;
25 
26 import std.algorithm;
27 import std.conv : ConvException, to;
28 import std.math : isNaN;
29 import std.range;
30 import std.stdio : write, writeln;
31 import std.string;
32 
33 import std.regex : ctRegex, match, split;
34 
35 import docopt;
36 
37 import plotd.binning;
38 import plotd.drawing;
39 import plotd.plot;
40 import plotd.primitives;
41 
42 import cli.algorithm : groupBy;
43 import cli.column;
44 import cli.figure : Figure;
45 import cli.options : helpText, Settings, updateSettings;
46 
47 version( unittest ) {
48 	import std.stdio;
49 }
50 
51 alias void delegate( PlotState plot ) Event;
52 
53 private auto csvRegex = ctRegex!(`,\s*|\s`);
54 
55 double[] toRange( string line ) {
56 	try {
57 		return line.split( csvRegex )
58 			.map!( (d) => d.strip( ' ' ).to!double )
59 			.array;
60 	} catch (ConvException exp) { 
61 		return [];
62 	}
63 }
64 
65 unittest {
66 	assert( "1,2".toRange == [1,2] );
67 	assert( "0.5, 2".toRange == [0.5,2] );
68 	assert( "bla, 2".toRange == [] );
69 	assert( "1\t2".toRange == [1,2] );
70 	assert( "1 2".toRange == [1,2] );
71 }
72 
73 Point[] toPoints( double[] coords ) {
74 	Point[] points;
75 	if (coords.length >= 2)
76 		points ~= Point( coords[0], coords[1] );
77 	return points;
78 }
79 
80 unittest {
81 	assert( [1.0].toPoints.length == 0 );
82 	assert( equal([1.0,2.0].toPoints, [Point( 1, 2 )] ) );
83 }
84 
85 Event[] toEvents( Point[] points ) {
86 	Event[] events;
87 
88 	ColorRange colorRange;
89 
90 	// Workaround point not properly copied in foreach loop
91   void delegate( PlotState ) createEvent( Point point, Color col ) {
92 		return delegate( PlotState plot ) {	
93 			plot.plotContext = color( plot.plotContext, col );
94 			colorRange.popFront;
95 			point.draw( plot ); 
96 		};
97   }
98 
99 	foreach( point; points ) {
100 		events ~= createEvent( point, colorRange.front ); 
101 		colorRange.popFront;
102 	}
103 	return events;
104 }
105 
106 Event[] toLineEvents( Point[] points, Point[] previousPoints ) {
107 	Event[] events;
108 
109 	ColorRange colorRange;
110 
111 	// Workaround point not properly copied in foreach loop
112   void delegate( PlotState ) createEvent( size_t i, Color col ) {
113 		return delegate( PlotState plot ) {	
114 			plot.plotContext = color( plot.plotContext, col );
115 			plot.plotContext = drawLine( previousPoints[i], points[i], plot.plotContext ); 
116 		};
117   }
118 
119 	foreach( i; 0..points.length ) {
120 		events ~= createEvent( i, colorRange.front ); 
121 		colorRange.popFront;
122 	}
123 	return events;
124 }
125 
126 /// Struct to hold the different points etc
127 struct ParsedRow {
128 	Point[] points;
129 	Point[] linePoints;
130 	double[] histData;
131 }
132 
133 // Warning assumes array with either one x or one y value.
134 private Point[] columnDataToPoints( ColumnData[] cMs, double defaultCoord )
135 {
136 	Point[] pnts;
137 	if (cMs.length == 0)
138 		return pnts;
139 
140 	auto coords = cMs.groupBy!( (cm) { 
141 			if (cm.xCoord) 
142 				return "x"; 
143 			return "y"; } );
144 
145 	if ( "x" !in coords ) {
146 		return coords["y"]
147 			.map!( (cmy) => Point( defaultCoord, cmy.value ) ).array;
148 	} else if ( "y" !in coords ) {
149 		return coords["x"]
150 			.map!( (cmx) => Point( cmx.value, defaultCoord ) ).array;
151 	}
152 	else if ( coords["x"].length == 1 ) {
153 		return coords["y"]
154 			.map!( (cmy) => Point( coords["x"].front.value, cmy.value ) ).array;
155 	}
156  	else if ( coords["y"].length == 1 ) {
157 		return coords["x"]
158 			.map!( (cmx) => Point( cmx.value, coords["y"].front.value ) ).array;
159 	}
160 	assert( 0, "Invalid input for columnModeToPoints " ~ cMs.to!string );
161 }
162 
163 /** Turn columns into drawable results. If no x or y value is present then columnID is used as the x or y value;
164 
165 	This function tries to be relative human like in parsing and the logic is difficult to follow. See the unittests for its behaviour
166 	*/
167 ParsedRow applyColumnData( ColumnData[] cMs, size_t columnID ) {
168 	ParsedRow parsed;
169 	foreach ( type, groupedCMs;
170 			cMs.groupBy!( (cm) {
171 				if ( cm.mode.to!string == "" )
172 					return "none";
173 				if ( cm.mode.front.to!string == "l" )
174 					return "line";
175 				if ( cm.mode.front.to!string == "h" )
176 					return "hist";
177 				return "point";
178 			}
179 		) ) 
180 	{
181 		if ( type == "none" )
182 			break;
183 		ColumnData[] xyGroup;
184 		size_t xs = 0;
185 		size_t ys = 0;
186 		double lastX; // If x is used set lastX to isNaN?
187 		double lastY;
188 		Point[] addRange;
189 		foreach ( cM; groupedCMs ) {
190 			if ( cM.xCoord || cM.yCoord ) {
191 				if ( xs > 1 && ys > 1 ) { 
192 					// We never want a group with more than 1 x coord or y coord
193 					xs = 0;
194 					ys = 0;
195 					addRange ~= columnDataToPoints( 
196 							xyGroup[0..$-1], columnID );
197 					xyGroup = [xyGroup.back];
198 				} else if ( xs >= 1 && ys >= 1 && xyGroup.back.mode != cM.mode ) {
199 					xs = 0;
200 					ys = 0;
201 					addRange ~= columnDataToPoints( xyGroup, columnID );
202 					xyGroup = [cM];
203 				} else
204 					xyGroup ~= cM;
205 				if (cM.xCoord) {
206 					lastX = cM.value;
207 					xs++;
208 				} else if (cM.yCoord) {
209 					lastY = cM.value;
210 					ys++;
211 				}
212 			} else {
213 				if (type == "hist")
214 					parsed.histData ~= cM.value;
215 			}
216 		}
217 		if (xyGroup.length > 0) {
218 			// If we found no x or y coord at all then use columnID
219 			if (lastX.isNaN || lastY.isNaN)
220 				addRange ~= columnDataToPoints( xyGroup, columnID ); 
221 			else if ( xyGroup.front.xCoord )
222 				addRange ~= columnDataToPoints( xyGroup, lastY ); 
223 			else
224 				addRange ~= columnDataToPoints( xyGroup, lastX ); 
225 		}
226 		if ( type == "line" ) {
227 			parsed.linePoints ~= addRange;
228 		} else if ( type == "point" )
229 			parsed.points ~= addRange;
230 	}
231 	return parsed;
232 }
233 
234 unittest {
235 	ColumnData cm( string mode, double value ) {
236 		return ColumnData( mode, -1, "", value );
237 	}
238 	
239 	auto pr = applyColumnData( [cm("x",1), cm("y",2)], 0 );
240 	assert( pr.points == [Point( 1, 2 )] );
241 
242 	pr = applyColumnData( [cm("x",3), cm("y",2), cm("y",4)], 0 );
243 	assert( pr.points == [Point( 3, 2 ), Point( 3, 4 )] );
244 
245 	pr = applyColumnData( [cm("x",1)], 5 );
246 	assert( pr.points == [Point( 1, 5 )] );
247 
248 	pr = applyColumnData( [cm("x",1), cm("x",3)], 5 );
249 	assert( pr.points == [Point( 1, 5 ),Point( 3, 5 )] );
250 
251 	pr = applyColumnData( [cm("y",2)], 5 );
252 	assert( pr.points == [Point( 5, 2 )] );
253 
254 	pr = applyColumnData( [cm("y",2), cm("y",4)], 5 );
255 	assert( pr.points == [Point( 5, 2 ),Point( 5, 4 )] );
256 
257 	pr = applyColumnData( [cm("y",2), cm("x",1), cm("y",4), cm("y",6)], 5 );
258 	assert( pr.points == [Point( 1, 2 ),Point( 1, 4 ),Point( 1, 6 )] );
259 	pr = applyColumnData( [cm("x",2), cm("y",1), cm("x",4), cm("x",6)], 5 );
260 	assert( pr.points == [Point( 2, 1 ),Point( 4, 1 ),Point( 6, 1 )] );
261 
262 	pr = applyColumnData( [cm("y",2), cm("x",1), cm("y",4), cm("x",3)], 5 );
263 	assert( pr.points == [Point( 1, 2 ),Point( 3, 4 )] );
264 	pr = applyColumnData( [cm("y",2), cm("y",8), cm("x",1), cm("y",4), cm("y",6), cm("x",3)], 5 );
265 	assert( pr.points == [Point( 1, 2 ),Point( 1, 8 ),Point( 3, 4 ),Point( 3, 6 )] );
266 	pr = applyColumnData( [cm("x",2), cm("y",8), cm("y",1), cm("x",4), cm("y",6), cm("y",3)], 5 );
267 	assert( pr.points == [Point( 2, 8 ),Point( 2, 1 ),Point( 4, 6 ),Point( 4, 3 )] );
268 
269 	// Lines
270 	// Should really be more indepth, but since same code is used, as for
271 	// points should be ok
272 	pr = applyColumnData( [cm("lx",1), cm("ly",2)], 0 );
273 	assert( pr.linePoints == [Point( 1, 2 )] );
274 
275 	pr = applyColumnData( [cm("x",2), cm("y",8), cm("lx",11), cm("y",1), cm("x",4), cm("y",6), cm("y",3)], 5 );
276 	assert( pr.points == [Point( 2, 8 ),Point( 2, 1 ),Point( 4, 6 ),Point( 4, 3 )] );
277 	assert( pr.linePoints == [Point( 11, 5 )] );
278 
279 	// Hist
280 	pr = applyColumnData( [cm("h",1.1), cm("h",2.1)], 0 );
281 	assert( pr.histData == [1.1,2.1] );
282 
283 	pr = applyColumnData( [cm("x",2), cm("h",1.1), cm("y",8), cm("lx",11), cm("y",1), cm("x",4), cm("h",2.1), cm("y",6), cm("y",3)], 5 );
284 	assert( pr.points == [Point( 2, 8 ),Point( 2, 1 ),Point( 4, 6 ),Point( 4, 3 )] );
285 	assert( pr.linePoints == [Point( 11, 5 )] );
286 	assert( pr.histData == [1.1,2.1] );
287 }
288 
289 /// Check whether current RowMode makes sense for new data.
290 Formats updateFormat( double[] floats, Formats formats ) {
291 	if ( floats.length == 0 )
292 		return formats;
293 	if ( formats.validFormat( floats.length ) )
294 		return formats;
295 	else 
296 		return Formats( floats.length );
297 }
298 
299 
300 Figure[string] figures;
301 
302 // High level functionality for handlingMessages
303 void handleMessage( string msg, ref Settings settings ) {
304 	if ( "" !in figures )
305 		figures[""] = new Figure;
306 
307 	debug write( "Received message: ", msg );
308 
309 	auto m = msg.match( r"^#plotcli (.*)" );
310 	if (m) {
311 		settings = settings.updateSettings( 
312 				docopt.docopt(helpText, 
313 					std..string.split( m.captures[1], " " ), true, "plotcli") );
314 		//writeln( settings );
315 	}
316 
317 	auto floats = msg.strip
318 		.toRange;
319 
320 	debug writeln( "Converted to doubles: ", floats );
321 
322 	settings.formats = updateFormat( floats, settings.formats );
323 
324 	auto columnData = settings.formats.zip(floats).map!( 
325 			(mv) { auto cD = ColumnData( mv[0] ); cD.value = mv[1]; return cD; } );
326 
327 	foreach( plotID, cMs1; columnData.groupBy!( (cm) => cm.plotID ) )
328 	{
329 		if ( plotID !in figures )
330 			figures[plotID] = new Figure;
331 		foreach( dataID, cMs; cMs1.groupBy!( (cm) => cm.dataID ) ) {
332 			debug writeln( "Plotting data: ", cMs );
333 			auto parsedRow = applyColumnData( cMs, figures[plotID].columnCount );
334 			bool needAdjusting = false;
335 			if ( !figures[plotID].validBound ) {
336 				needAdjusting = true;
337 				figures[plotID].pointCache ~= parsedRow.points ~ parsedRow.linePoints;
338 				figures[plotID].validBound = validBounds( figures[plotID].pointCache );
339 				figures[plotID].plot.plotBounds = minimalBounds( figures[plotID].pointCache );
340 				if (figures[plotID].validBound)
341 					figures[plotID].pointCache = [];
342 			} else {
343 				foreach( point; parsedRow.points ~ parsedRow.linePoints ) {
344 					if (!figures[plotID].plot.plotBounds.withinBounds( point )) {
345 						figures[plotID].plot.plotBounds = adjustedBounds( figures[plotID].plot.plotBounds, point );
346 						needAdjusting = true;
347 					}
348 				}
349 			}
350 
351 			if (needAdjusting) {
352 
353 				figures[plotID].plot = createPlotState( figures[plotID].plot.plotBounds, 
354 						figures[plotID].plot.marginBounds );
355 				foreach( event; figures[plotID].eventCache )
356 					event( figures[plotID].plot );
357 			}
358 
359 			auto events = parsedRow.points.toEvents;
360 
361 			if ( figures[plotID].previousLines.length == parsedRow.linePoints.length )
362 				events ~= parsedRow.linePoints.toLineEvents( figures[plotID].previousLines );
363 
364 			if (parsedRow.linePoints.length > 0)
365 				figures[plotID].previousLines = parsedRow.linePoints;
366 
367 
368 			foreach( event; events )
369 				event( figures[plotID].plot );
370 
371 			figures[plotID].eventCache ~= events;
372 
373 			// Histograms
374 			foreach( data; parsedRow.histData ) {
375 				if ( data < figures[plotID].histRange[0] || isNaN(figures[plotID].histRange[0]) ) {
376 					figures[plotID].histRange[0] = data;
377 				} 
378 				if ( data > figures[plotID].histRange[1] || isNaN(figures[plotID].histRange[1]) ) {
379 					figures[plotID].histRange[1] = data;
380 				}
381 				figures[plotID].histData ~= data;
382 			}
383 
384 			if (figures[plotID].histData.length > 0) {
385 				// Create bin
386 				Bins!size_t bins;
387 				bins.min = figures[plotID].histRange[0];
388 				bins.width = 0.5;
389 				bins.length = max( 11, figures[plotID].histData.length/100 ); 
390 				if( figures[plotID].histRange[0] != figures[plotID].histRange[1] )
391 					bins.width = (figures[plotID].histRange[1]-figures[plotID].histRange[0])/10.0;
392 				// add all data to bin
393 				foreach( data; figures[plotID].histData )
394 					bins = bins.addDataToBin( [bins.binId( data )] );
395 
396 				auto histBounds = Bounds( bins.min, bins.max, 0,
397 							figures[plotID].histData.length );
398 
399 				debug writeln( "Adjusting histogram to bounds: ", histBounds );
400 				// Adjust plotBounds 
401 				figures[plotID].plot = createPlotState( histBounds,
402 						figures[plotID].plot.marginBounds );
403 				// Plot Bins
404 				figures[plotID].plot.plotContext = drawBins( figures[plotID].plot.plotContext, bins );
405 				debug writeln( "Drawn bins to histogram: ", bins );
406 			}
407 
408 		}
409 		figures[plotID].columnCount += 1;
410 	}
411 }
412 
413 void saveFigures( string baseName ) {
414 	foreach ( plotID, figure; figures ) {
415 		figure.plot.save( baseName ~ plotID ~ ".png" );
416 	}
417 }