001    package org.hackystat.projectbrowser.page.telemetry.datapanel;
002    
003    import java.io.Serializable;
004    import java.text.SimpleDateFormat;
005    import java.util.ArrayList;
006    import java.util.Date;
007    import java.util.HashMap;
008    import java.util.List;
009    import java.util.Locale;
010    import java.util.Map;
011    import java.util.Map.Entry;
012    import java.util.logging.Logger;
013    import org.apache.wicket.model.IModel;
014    import org.hackystat.projectbrowser.googlechart.ChartType;
015    import org.hackystat.projectbrowser.googlechart.GoogleChart;
016    import org.hackystat.projectbrowser.page.loadingprocesspanel.Processable;
017    import org.hackystat.sensorbase.resource.projects.jaxb.Project;
018    import org.hackystat.telemetry.service.client.TelemetryClient;
019    import org.hackystat.telemetry.service.client.TelemetryClientException;
020    import org.hackystat.telemetry.service.resource.chart.jaxb.TelemetryPoint;
021    import org.hackystat.telemetry.service.resource.chart.jaxb.TelemetryStream;
022    import org.hackystat.utilities.logger.HackystatLogger;
023    import org.hackystat.utilities.tstamp.Tstamp;
024    
025    /**
026     * Data model to hold state of the telemetry chart.
027     * 
028     * @author Shaoxuan Zhang
029     */
030    public class TelemetryChartDataModel implements Serializable, Processable {
031      /** Support serialization. */
032      private static final long serialVersionUID = 1L;
033      /** the width of this chart. */
034      private int width = 850;
035      /** the height of this chart. */
036      private int height = 350;
037      
038      /** The start date this user has selected. */
039      private long startDate = 0;
040      /** The end date this user has selected. */
041      private long endDate = 0;
042      /** The granularity of the chart. Either Day, Week, or Month. */
043      private String granularity = "Day";
044      /** The projects this user has selected. */
045      private List<Project> selectedProjects = new ArrayList<Project>();
046      /** The analysis this user has selected. */
047      private String telemetryName = null;
048      /** The parameters for this telemetry chart. */
049      private List<String> parameters = new ArrayList<String>();
050      /** Store the data retrieved from telemetry service. */
051      private final Map<Project, List<SelectableTelemetryStream>> projectStreamData = 
052        new HashMap<Project, List<SelectableTelemetryStream>>();
053      /** Chart with selected project streams. */
054      private String selectedChart = null;
055      /** state of data loading process. */
056      private volatile boolean inProcess = false;
057      /** result of data loading. */
058      private volatile boolean complete = false;
059      /** message to display when data loading is in process.*/
060      private String processingMessage = "";
061      /** host of the telemetry host. */
062      private String telemetryHost;
063      /** email of the user. */
064      private String email;
065      /** password of the user. */
066      private String password;
067      /** The URL of google chart. */
068      // private String chartUrl = "";
069      
070      /**
071       * @param startDate the start date of this model..
072       * @param endDate the end date of this model..
073       * @param selectedProjects the project ofs this model.
074       * @param telemetryName the telemetry name of this model.
075       * @param granularity the granularity of this model, Day or Week or Month.
076       * @param parameters the list of parameters
077       * @param telemetryHost the telemetry host
078       * @param email the user's email
079       * @param password the user's passowrd
080       */
081      public void setModel(Date startDate, Date endDate, List<Project> selectedProjects,
082          String telemetryName, String granularity, List<IModel> parameters,
083          String telemetryHost, String email, String password) {
084        this.processingMessage = "";
085        this.telemetryHost = telemetryHost;
086        this.email = email;
087        this.password = password;
088        this.startDate = startDate.getTime();
089        this.endDate = endDate.getTime();
090        this.granularity = granularity;
091        this.selectedProjects = selectedProjects;
092        this.telemetryName = telemetryName;
093        //clear old data.
094        this.projectStreamData.clear();
095        this.parameters.clear();
096        for (IModel model : parameters) {
097          if (model.getObject() != null) {
098            this.parameters.add(model.getObject().toString());
099          }
100        }
101        this.selectedChart = null;
102        // this.chartUrl = this.getChartUrl(project);
103      }
104    
105      /**
106       * Load data from Hackystat service.
107       */
108      public void loadData() {
109        this.inProcess = true;
110        this.complete = false;
111        this.processingMessage = "Retrieving telemetry chart <" + getTelemetryName() + 
112        "> from Hackystat Telemetry service.\n";
113        Logger logger = HackystatLogger.getLogger("org.hackystat.projectbrowser", "projectbrowser");
114        try {
115          TelemetryClient client = new TelemetryClient(this.telemetryHost, this.email, this.password);
116          //for each selected project
117          for (int i = 0; i < this.selectedProjects.size() && inProcess; i++) {
118            //prepare
119            Project project = this.selectedProjects.get(i);
120            this.processingMessage += "Retrieving data for project: " + project.getName() + 
121                " (" + (i + 1) + " of " + this.selectedProjects.size() + ").\n";
122            
123            logger.info("Retrieving chart <" + getTelemetryName()
124                + "> for project: " + project.getName());
125            
126            List<SelectableTelemetryStream> streamList = new ArrayList<SelectableTelemetryStream>();
127            //retrieve data from server.
128            List<TelemetryStream> streams = client.getChart(this.getTelemetryName(),
129                project.getOwner(), project.getName(), granularity,
130                Tstamp.makeTimestamp(startDate), Tstamp.makeTimestamp(endDate), 
131                this.getParameterAsString()).getTelemetryStream();
132            for (TelemetryStream stream : streams) {
133              streamList.add(new SelectableTelemetryStream(stream));
134            }
135            this.projectStreamData.put(project, streamList);
136    
137            logger.info("Finished retrieving chart <" + getTelemetryName() + 
138                "> for project: " + project.getName());
139          }
140        }
141        catch (TelemetryClientException e) {
142          String errorMessage = "Errors when retrieving " + getTelemetryName() + 
143          " telemetry data: " + e.getMessage() + ". Please try again.";
144          this.processingMessage += errorMessage + "\n";
145    
146          logger.warning(errorMessage);
147          this.complete = false;
148          this.inProcess = false;
149          return;
150        }
151        this.complete = inProcess;
152        if (this.complete) {
153          this.processingMessage += "All done.\n";
154        }
155        this.inProcess = false;
156      }
157      
158      /**
159       * Cancel the data loading process.
160       */
161      public void cancelDataLoading() {
162        this.processingMessage += "Process Cancelled.\n";
163        this.inProcess = false;
164      }
165      
166      /**
167       * @return the telemetryName
168       */
169      public String getTelemetryName() {
170        return telemetryName;
171      }
172    
173      /**
174       * Returns true if this model does not contain any data.
175       * 
176       * @return True if no data.
177       */
178      public boolean isEmpty() {
179        return this.selectedProjects.isEmpty();
180      }
181    
182      /**
183       * Return the list of TelemetryStream associated with the given project.
184       * 
185       * @param project the given project.
186       * @return the list of TelemetryStream.
187       */
188      public List<SelectableTelemetryStream> getTelemetryStream(Project project) {
189        List<SelectableTelemetryStream> streamList = this.projectStreamData.get(project);
190        if (streamList == null) {
191          streamList = new ArrayList<SelectableTelemetryStream>();
192        }
193        return streamList;
194      }
195    
196      /**
197       * update the selectedChart.
198       * @return true if the chart is successfully updated.
199       */
200      public boolean updateSelectedChart() {
201        List<SelectableTelemetryStream> streams = new ArrayList<SelectableTelemetryStream>();
202        boolean dashed = false;
203        for (Project project : this.selectedProjects) {
204          //assign a same marker and line style to streams of the same project.
205          List<SelectableTelemetryStream> streamList = this.getTelemetryStream(project);
206          String projectMarker = null;
207          double thickness = 2;
208          double lineLength = 1;
209          double blankLength = 0;
210          boolean isLineStylePicked = false;
211          for (SelectableTelemetryStream stream : streamList) {
212            if (stream.isSelected() && !stream.isEmpty()) {
213              //marker.
214              if (projectMarker == null) {
215                projectMarker = GoogleChart.getNextMarker();
216              }
217              //line style, dash or solid
218              if (!isLineStylePicked) {
219                isLineStylePicked = true;
220                if (dashed) {
221                  lineLength = 6;
222                  blankLength = 3;
223                }
224                dashed = !dashed;
225              }
226              //add marker and line style to the stream.
227              stream.setMarker(projectMarker);
228              stream.setThickness(thickness);
229              stream.setLineLength(lineLength);
230              stream.setBlankLength(blankLength);
231              streams.add(stream);
232            }
233            else {
234              stream.setColor("");
235              stream.setMarker("");
236            }
237          }
238        }
239        if (streams.isEmpty()) {
240          selectedChart = "";
241          return false;
242        }
243        selectedChart = this.getChartUrl(streams);
244        return true;
245      }
246      
247      /**
248       * Return the google chart url that present all streams within the given list.
249       * 
250       * @param streams the given stream list.
251       * @return the URL string of the chart.
252       */
253      public String getChartUrl(List<SelectableTelemetryStream> streams) {
254        GoogleChart googleChart = new GoogleChart(ChartType.LINE, this.getWidth(), this.getHeight());
255        
256        Map<String, TelemetryStreamYAxis> streamAxisMap = new HashMap<String, TelemetryStreamYAxis>();
257        
258        //combine streams Y axes.
259        for (SelectableTelemetryStream stream : streams) {
260          if (stream.isEmpty()) {
261            continue;
262          }
263          double streamMax = stream.getMaximum();
264          double streamMin = stream.getMinimum();
265          String streamUnitName = stream.getUnitName();
266          TelemetryStreamYAxis axis = streamAxisMap.get(streamUnitName);
267          if (axis == null) {
268            axis = newYAxis(streamUnitName, streamMax, streamMin);
269            streamAxisMap.put(streamUnitName, axis);
270          }
271          else {
272            double axisMax = axis.getMaximum();
273            double axisMin = axis.getMinimum();
274            axis.setMaximum((axisMax > streamMax) ? axisMax : streamMax);
275            axis.setMinimum((axisMin < streamMin) ? axisMin : streamMin);
276          }
277        }
278        
279        //Assign colors.
280        //IF there are more than 1 unit axis, streams will be colored according to the axes.
281        if (streamAxisMap.size() > 1 || streams.size() == 1) {
282          //generate enough random color.
283          List<String> colors = RandomColorGenerator.generateRandomColorInHex(streamAxisMap.size());
284          //color the axis.
285          int index = 0;
286          for (TelemetryStreamYAxis axis : streamAxisMap.values()) {
287            axis.setColor(colors.get(index++));
288          }
289          //color the streams.
290          for (SelectableTelemetryStream stream : streams) {
291            stream.setColor(streamAxisMap.get(stream.getUnitName()).getColor());
292          }
293        }
294        else {
295          //if there is only one axis, but have multiple stream names, streams will be colored
296          //according to the stream name.
297          for (TelemetryStreamYAxis axis : streamAxisMap.values()) {
298            axis.setColor("000000");
299          }
300          Map<String, String> streamColorMap = new HashMap<String, String>();
301          for (SelectableTelemetryStream stream : streams) {
302            streamColorMap.put(stream.getStreamName(), "");
303          }
304          if (streamColorMap.size() > 1) {
305            List<String> colors = RandomColorGenerator.generateRandomColorInHex(streamColorMap.size());
306            int index = 0;
307            for (Entry<String, String> streamName : streamColorMap.entrySet()) {
308              streamName.setValue(colors.get(index++));
309            }
310            for (SelectableTelemetryStream stream : streams) {
311              stream.setColor(streamColorMap.get(stream.getStreamName()));
312            }
313          }
314          else {
315            //if there is only one stream name as well. All stream will be colored separately.
316            List<String> colors = RandomColorGenerator.generateRandomColorInHex(streams.size());
317            int index = 0;
318            for (SelectableTelemetryStream stream : streams) {
319              stream.setColor(colors.get(index++));
320            }
321          }
322        }
323        
324        
325        //add the y axis.
326        for (TelemetryStreamYAxis axis : streamAxisMap.values()) {
327          String axisType = "r";
328          if (googleChart.isYAxisEmpty()) {
329            axisType = "y";
330          }
331          List<Double> rangeList = new ArrayList<Double>();
332          //extend the range of the axis if it contains only one vertical straight line.
333          if (axis.getMaximum() - axis.getMinimum() < 0.1) {
334            axis.setMaximum(axis.getMaximum() + 1.0);
335            axis.setMinimum(axis.getMinimum() - 1.0);
336          }
337          rangeList.add(axis.getMinimum());
338          rangeList.add(axis.getMaximum());
339          //add the axis to the chart.
340          googleChart.addAxisRange(axisType, rangeList, axis.getColor());
341        }
342    
343        //add the streams to the chart.
344        for (SelectableTelemetryStream stream : streams) {
345          TelemetryStreamYAxis axis = streamAxisMap.get(stream.getUnitName());
346          this.addStreamToChart(stream, axis.getMinimum(), axis.getMaximum(), googleChart);
347        }
348        
349        //add the x axis.
350        if (!streams.isEmpty()) {
351          googleChart.addAxisLabel("x", 
352              getDateList(streams.get(0).getTelemetryStream().getTelemetryPoint()), "");
353        }
354        return googleChart.getUrl();
355      }
356    
357    
358      /**
359       * Add the given stream to the google chart. And return the maximum value of the stream.
360       * 
361       * @param stream the given stream.
362       * @param maximum the maximum value of the range this stream will be associated to.
363       * @param minimum the minimum value of the range this stream will be associated to.
364       * @param googleChart the google chart.
365       */
366      public void addStreamToChart(SelectableTelemetryStream stream,
367          double minimum, double maximum, GoogleChart googleChart) {
368        // add the chart only if the stream is not empty.
369        if (!stream.isEmpty()) {
370          //add stream data
371          googleChart.getChartData().add(stream.getStreamData());
372          //prepare the range data.
373          List<Double> range = getRangeList(minimum, maximum);
374          //add range
375          googleChart.getChartDataScale().add(range);
376          //add stream color
377          googleChart.addColor(stream.getStreamColor());
378          //add line style;
379          googleChart.addLineStyle(stream.getThickness(), 
380              stream.getLineLength(), stream.getBlankLength());
381          /*googleChart.addAxisRange(axisType, range, randomColor);*/
382          //add markers
383          /*stream.setMarker(GoogleChart.getRandomMarker());*/
384          googleChart.getChartMarker().add(stream.getMarker());
385          googleChart.getMarkerColors().add(stream.getMarkerColor());
386        }
387      }
388    
389      /**
390       * create a new TelemetryStreamYAxis instance with given parameters.
391       * @param streamUnitName the unit name
392       * @param streamMax the maximum value
393       * @param streamMin the minimum value
394       * @return the new object
395       */
396      private TelemetryStreamYAxis newYAxis(String streamUnitName, double streamMax, double streamMin) {
397        TelemetryStreamYAxis axis = new TelemetryStreamYAxis(streamUnitName);
398        axis.setMaximum(streamMax);
399        axis.setMinimum(streamMin);
400        axis.setColor(GoogleChart.getRandomColor());
401        return axis;
402      }
403      
404      /**
405       * return a list that contains the range minimum and maximum.
406       * The minimum value and maximum value will be normalized accordingly.
407       * @param min the minimum value
408       * @param max the maximum value
409       * @return the list
410       */
411      private List<Double> getRangeList(double min, double max) {
412        double minimum = min;
413        double maximum = max;
414        List<Double> rangeList = new ArrayList<Double>();
415        if (minimum == maximum) {
416          minimum -= 0.5;
417          minimum = (minimum < 0) ? 0 : minimum;
418          maximum += 0.5;
419        }
420        minimum = Math.floor(minimum);
421        maximum = Math.ceil(maximum);
422        rangeList.add(minimum);
423        rangeList.add(maximum);
424        return rangeList;
425      }
426    
427      /**
428       * Return the date list within the list of points.
429       * 
430       * @param points the point list.
431       * @return the date list.
432       */
433      public List<String> getDateList(List<TelemetryPoint> points) {
434        List<String> dates = new ArrayList<String>();
435        SimpleDateFormat dateFormat = new SimpleDateFormat("MM-dd", Locale.US);
436        for (TelemetryPoint point : points) {
437          dates.add(dateFormat.format(point.getTime().toGregorianCalendar().getTime()));
438        }
439        return dates;
440      }
441    
442      /**
443       * Return the date list inside this model.
444       * 
445       * @return the date list.
446       */
447      public List<String> getDateList() {
448        if (this.selectedProjects.isEmpty()) {
449          return new ArrayList<String>();
450        }
451        List<SelectableTelemetryStream> streamList = this.getTelemetryStream(this.selectedProjects
452            .get(0));
453        if (streamList.isEmpty()) {
454          return new ArrayList<String>();
455        }
456        return getDateList(streamList.get(0).getTelemetryStream().getTelemetryPoint());
457      }
458    
459      /**
460       * @return the selectedProjects
461       */
462      public List<Project> getSelectedProjects() {
463        return selectedProjects;
464      }
465    
466      /**
467       * Return the comma-separated list of parameters in String.
468       * 
469       * @return the parameters as String
470       */
471      public String getParameterAsString() {
472        StringBuffer stringBuffer = new StringBuffer();
473        for (String model : this.parameters) {
474          stringBuffer.append(model);
475          stringBuffer.append(',');
476        }
477        String param = stringBuffer.toString();
478        if (param.length() >= 1) {
479          param = param.substring(0, param.length() - 1);
480        }
481        return param;
482      }
483      
484      /**
485       * @return the width
486       */
487      public int getWidth() {
488        return width;
489      }
490    
491      /**
492       * @return the height
493       */
494      public int getHeight() {
495        return height;
496      }
497      
498      /**
499       * Check if the size of the chart is within range.
500       * @return true if multiple of width and height is not bigger than MAX_SIZE.
501       */
502      public boolean isSizeWithinRange() {
503        return this.width <= GoogleChart.MAX_SIZE / this.height;
504      }
505    
506      /**
507       * @return the selectedChart
508       */
509      public String getSelectedChart() {
510        return selectedChart;
511      }
512    
513      /**
514       * @return true if the chart is empty.
515       */
516      public boolean isChartEmpty() {
517        return selectedChart == null || "".equals(selectedChart);
518      }
519    
520      /**
521       * Change all selected flags of streams to the given flag.
522       * 
523       * @param flag the boolean flag.
524       */
525      public void changeSelectionForAll(boolean flag) {
526        for (List<SelectableTelemetryStream> streamList : this.projectStreamData.values()) {
527          for (SelectableTelemetryStream stream : streamList) {
528            stream.setSelected(flag);
529          }
530        }
531      }
532    
533      /**
534       * @return the inProcess
535       */
536      public boolean isInProcess() {
537        return inProcess;
538      }
539    
540      /**
541       * @return the isComplete
542       */
543      public boolean isComplete() {
544        return complete;
545      }
546    
547      /**
548       * @return the message
549       */
550      public String getProcessingMessage() {
551        return processingMessage;
552      }
553    
554      /**
555       * @param width the width to set
556       */
557      public void setWidth(int width) {
558        this.width = width;
559      }
560    
561      /**
562       * @param height the height to set
563       */
564      public void setHeight(int height) {
565        this.height = height;
566      }
567    }