/*
* CycleExporter.java - Copyright(c) 2014 Joe Pasqua
* Provided under the MIT License. See the LICENSE file for details.
* Created: Oct 23, 2014
*/
package org.noroomattheinn.visibletesla.data;
import com.google.common.collect.Range;
import java.io.File;
import java.io.IOException;
import java.util.List;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.DoubleProperty;
import jxl.Workbook;
import jxl.write.WritableCellFormat;
import jxl.write.WritableFont;
import jxl.write.WritableSheet;
import jxl.write.WritableWorkbook;
import jxl.write.WriteException;
import static org.noroomattheinn.tesla.Tesla.logger;
import org.noroomattheinn.utils.MailGun;
/**
* CycleExporter: Does most of the heavy lifting of exporting Cycles. Subclasses
* implement a few methods to make it all work.
*
* Notes:
* + jxl.WritableCellFormat can't be static. Once they are used they are bound
* to a sheet and can't be used in other sheets. It's tempting to want to make
* these static or even instance variables, but new instances need to be
* created at each doExport() call.
* @author Joe Pasqua <joe at NoRoomAtTheInn dot org>
*/
abstract class CycleExporter<C extends BaseCycle> {
/*------------------------------------------------------------------------------
*
* Constants, Enums, and Types
*
*----------------------------------------------------------------------------*/
protected static class StandardFormats {
final WritableCellFormat headerFormat;
final WritableCellFormat standardFormat;
final WritableCellFormat dateFormat;
StandardFormats() {
WritableFont stdFont = new WritableFont(WritableFont.ARIAL, 12);
standardFormat = new WritableCellFormat(stdFont);
headerFormat = new WritableCellFormat(
new WritableFont(WritableFont.ARIAL, 12, WritableFont.BOLD));
dateFormat = new jxl.write.WritableCellFormat(
new jxl.write.DateFormat("M/d/yy H:mm:ss"));
dateFormat.setFont(stdFont);
}
}
private static final String VTDataAddress = "data@visibletesla.com";
/*------------------------------------------------------------------------------
*
* Internal State
*
*----------------------------------------------------------------------------*/
protected final String cycleType;
protected final String[] columns;
protected final BooleanProperty submitData;
protected final BooleanProperty includeLoc;
protected final DoubleProperty ditherLocAmt;
/*==============================================================================
* ------- -------
* ------- Public Interface To This Class -------
* ------- -------
*============================================================================*/
/**
* Create a new CycleExporter
* @param cycleType The name of this type of Cycle (eg Charge or Rest)
* @param columns The names of the columns of data being exported
* @param submitData A property that indicates whether to submit anonymous
* data for this cycle type
*/
CycleExporter(String cycleType, String[] columns,
BooleanProperty submitData, BooleanProperty includeLoc,
DoubleProperty ditherLocAmt) {
this.cycleType = cycleType;
this.columns = columns;
this.submitData = submitData;
this.includeLoc = includeLoc;
this.ditherLocAmt = ditherLocAmt;
}
/**
* Export a set of Cycle data to an Excel file.
* @param provider An object that can provide a list of Cycles for a
* specified range of times.
*/
boolean export(CycleStore<C> provider, File file, Range<Long> exportPeriod) {
List<C> cycles = provider.getCycles(exportPeriod);
return doExport(file, cycles);
}
/**
* Submit a Cycle anonymously to a central repository
* @param cycle The Cycle to be submitted
*/
void submitData(C cycle) {
if (!submitData.get()) return;
ditherLocation(cycle);
String jsonRep = cycle.toJSONString();
jsonRep = filterSubmissionData(jsonRep);
// Send the notification and log the body
String subject = cycleType + " Data Submission";
MailGun.get().send(VTDataAddress, subject, jsonRep);
logger.info(subject + ": " + jsonRep);
}
/*------------------------------------------------------------------------------
*
* Protected methods that may (or must) be overridden by subclasses
*
*----------------------------------------------------------------------------*/
/**
* Add a row of header information for an exported Excel spreadsheet. This
* is normally not overridden.
* @param sheet The sheet in question
* @param row The row number of where to put the header row
* @param sf Standard Excel formats that can be shared across the export
* @throws WriteException
*/
protected void addTableHeader(WritableSheet sheet, int row, StandardFormats sf)
throws WriteException {
for (int column = 0; column < columns.length; column++) {
String label = columns[column];
sheet.setColumnView(column, label.length()+3);
sheet.addCell(new jxl.write.Label(column, row, label, sf.headerFormat));
}
// Make the header row stationary
sheet.getSettings().setVerticalFreeze(1);
}
/**
* Emit a row of data, appropriately formatted, given the input Cycle
* @param sheet The sheet that will contain the row
* @param row The row number
* @param cycle The cycle to be written to the row
* @param sf Standard Excel formats that can be shared across the export
* @throws WriteException
*/
protected abstract void emitRow(WritableSheet sheet, int row, C cycle, StandardFormats sf)
throws WriteException;
/**
* This method provides an opportunity to filter (or add to) the data being
* submitted anonymously about a Cycle. If this method is not overridden,
* then no filtering will be performed.
* @param data The data to be submitted
* @return A filtered version of the data to be submitted
*/
protected String filterSubmissionData(String data) { return data; }
/**
* Randomize (dither) the location of this cycle for privacy purposes.
* @param cycle The cycle whose location should be dithered.
*/
protected void ditherLocation(C cycle) {
if (!includeLoc.get()) { cycle.lat = cycle.lng = 0; return; }
if (cycle.lat == 0 && cycle.lng == 0) return;
double random, offset;
double ditherAmt = ditherLocAmt.get();
double pow = Math.pow(10, ditherAmt); // 10^ditherAmt
random = 0.5 + (Math.random()/2); // value in [0.5, 1]
offset = (random/pow) * (Math.random() > 0.5 ? -1 : 1);
cycle.lat += offset;
random = 0.5 + (Math.random()/2); // value in [0.5, 1]
offset = (random/pow) * (Math.random() > 0.5 ? -1 : 1);
cycle.lng += offset;
}
/*------------------------------------------------------------------------------
*
* Private Utility Methods
*
*----------------------------------------------------------------------------*/
/**
* The standard process of exporting Cycles to a file
* @param file The file to export to
* @param cycles The list of cycles to export
* @return
*/
protected boolean doExport(File file, List<C> cycles) {
StandardFormats sf = new StandardFormats();
try {
WritableWorkbook workbook = Workbook.createWorkbook(file);
WritableSheet sheet = workbook.createSheet("Sheet1", 0);
int row = 0;
addTableHeader(sheet, row++, sf);
for (C cycle : cycles) {
emitRow(sheet, row++, cycle, sf);
}
workbook.write();
workbook.close();
return true;
} catch (IOException | WriteException ex) {
return false;
}
}
}