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 }