package charts.builder.spreadsheet; import java.util.List; import java.util.Map; import java.util.SortedMap; import java.util.concurrent.ExecutionException; import org.apache.commons.lang3.StringUtils; import play.Logger; import charts.ChartType; import charts.Region; import charts.builder.DataSource.MissingDataException; import charts.builder.Value; import charts.graphics.ProgressTable; import com.google.common.cache.CacheBuilder; import com.google.common.cache.CacheLoader; import com.google.common.cache.LoadingCache; import com.google.common.collect.ImmutableMap; import com.google.common.collect.Lists; import com.google.common.collect.Maps; public abstract class AbstractProgressTableBuilder extends AbstractBuilder { protected static final ImmutableMap<Region, Integer> ROWS = new ImmutableMap.Builder<Region, Integer>() .put(Region.GBR, 3) .put(Region.CAPE_YORK, 4) .put(Region.WET_TROPICS, 5) .put(Region.BURDEKIN, 6) .put(Region.MACKAY_WHITSUNDAY, 7) .put(Region.FITZROY, 8) .put(Region.BURNETT_MARY, 9) .build(); // Type interpolation would be great here protected final LoadingCache< SpreadsheetDataSource, Map<java.awt.Color, ProgressTable.Condition> > conditionColorCache = CacheBuilder.newBuilder() .weakKeys() .maximumSize(100) .build( new CacheLoader< SpreadsheetDataSource, Map<java.awt.Color, ProgressTable.Condition> >() { @Override public Map<java.awt.Color, ProgressTable.Condition> load( SpreadsheetDataSource ds) { final Map<java.awt.Color, ProgressTable.Condition> m = Maps.newHashMap(); try { for (int row = 0;; row++) { final Value v = ds.select("condition", row, 0); if (StringUtils.isBlank(v.asString())) { break; } m.put(v.asColor(), getCondition(v.asString())); } } catch (MissingDataException e) {} return m; } }); public AbstractProgressTableBuilder(List<ChartType> chartTypes) { super(chartTypes); } @Override public boolean canHandle(SpreadsheetDataSource datasource) { try { return "progress towards targets indicators".equalsIgnoreCase(datasource .select("A1").getValue()); } catch (MissingDataException e) { return false; } } protected ProgressTable.Dataset getDataset(SpreadsheetDataSource datasource, Region region) throws MissingDataException { List<ProgressTable.Column> columns = getColumns(datasource, region); List<ProgressTable.Row> rows = getRows(columns, datasource, region); return new ProgressTable.Dataset(columns, rows); } protected List<ProgressTable.Column> getColumns(SpreadsheetDataSource ds, Region region) throws MissingDataException { List<ProgressTable.Column> columns = Lists.newArrayList(); int col = 2; while (StringUtils.isNotBlank(ds.select(0, col).asString())) { String header = getIndicator(ds.select(0, col).asString(), region).getLabel(); columns.add(new ProgressTable.Column(header, ds.select(1, col).asString(), ds.select(2, col).asString())); col++; } return columns; } private List<ProgressTable.Row> getRows(List<ProgressTable.Column> columns, SpreadsheetDataSource datasource, Region region) throws MissingDataException { List<ProgressTable.Row> rows = Lists.newArrayList(); if(region == null) { for (Region r : Region.values()) { rows.add(getRow(columns, datasource, r)); } } else { rows.add(getRow(columns, datasource, region)); } return rows; } protected ProgressTable.Row getRow(List<ProgressTable.Column> columns, SpreadsheetDataSource ds, Region region) throws MissingDataException { Integer row = ROWS.get(region); if (row == null) { throw new RuntimeException("row not configured for region " + region.getProperName()); } String r = ds.select(row, 0).asString(); if (!StringUtils.equals(region.getProperName(), r)) { throw new RuntimeException(String.format( "expected %s in row %s column 0 but found %s", region.getProperName(), row, r)); } List<ProgressTable.Cell> cells = Lists.newArrayList(); for (ProgressTable.Column column : columns) { Double progress = ds.select(row, columns.indexOf(column) + 2) .asPercent(); if (progress != null) { ProgressTable.Indicator indicator = getIndicator(column.header, region); ProgressTable.Condition condition = getCondition(ds, columns, indicator, region); cells.add(new ProgressTable.Cell(indicator, condition, formatProgress(progress))); } else { cells.add(new ProgressTable.Cell()); } } return new ProgressTable.Row(region.getProperName(), ds.select(row, 1) .asString(), cells); } private String formatProgress(double value) { return Math.round(value * 100.0) + "%"; } protected ProgressTable.Indicator getIndicator(String name, Region region) { ProgressTable.Indicator i = ProgressTable.Indicator.valueOf(name .toUpperCase()); if (i == null) { throw new RuntimeException(String.format("no indicator found for %s", name)); } if (region == Region.FITZROY && i == ProgressTable.Indicator.SUGARCANE) { return ProgressTable.Indicator.GRAIN; } return i; } private ProgressTable.Condition getCondition(SpreadsheetDataSource ds, List<ProgressTable.Column> columns, ProgressTable.Indicator indicator, Region region) throws MissingDataException { if (indicator == ProgressTable.Indicator.GRAIN) { indicator = ProgressTable.Indicator.SUGARCANE; } return getCondition(ds.select("progress", region.ordinal() + 3, getConditionColumn(ds, indicator)).asColor(), getConditionColors(ds)); } private ProgressTable.Condition getCondition( java.awt.Color c, Map<java.awt.Color, ProgressTable.Condition> conditions) { if (c == null) { return ProgressTable.Condition.UNDECIDED; } if (conditions.containsKey(c)) { return conditions.get(c); } // Not an exact match, so pick closest colour final SortedMap<Double, java.awt.Color> m = Maps.newTreeMap(); for (java.awt.Color otherColor : conditions.keySet()) { m.put(distance(c, otherColor), otherColor); } final java.awt.Color closest = m.get(m.firstKey()); Logger.trace(String.format("Closest match for %s is %s => %s", c, closest, conditions.get(closest))); // Get the closest colour return conditions.get(closest); } private ProgressTable.Condition getCondition(String str) { return ProgressTable.Condition.valueOf(StringUtils.upperCase(StringUtils .replace(str, " ", ""))); } private Map<java.awt.Color, ProgressTable.Condition> getConditionColors( SpreadsheetDataSource ds) { try { return conditionColorCache.get(ds); } catch (ExecutionException e) { // Shouldn't happen throw new RuntimeException(e); } } private int getConditionColumn(SpreadsheetDataSource ds, ProgressTable.Indicator indicator) throws MissingDataException { for (int col = 2;; col++) { String s = ds.select("progress", 0, col).asString(); if (StringUtils.equalsIgnoreCase(s, indicator.toString())) { return col; } else if (StringUtils.isBlank(s)) { throw new RuntimeException(String.format("indicator %s not found", indicator.toString())); } } } // From: http://stackoverflow.com/a/2103608/701439 private double distance(java.awt.Color c1, java.awt.Color c2){ final double rmean = ( c1.getRed() + c2.getRed() )/2; final int r = c1.getRed() - c2.getRed(); final int g = c1.getGreen() - c2.getGreen(); final int b = c1.getBlue() - c2.getBlue(); final double weightR = 2 + rmean/256; final double weightG = 4.0; final double weightB = 2 + (255-rmean)/256; return Math.sqrt(weightR*r*r + weightG*g*g + weightB*b*b); } }