/* * MessageTemplate.java - Copyright(c) 2014 Joe Pasqua * Provided under the MIT License. See the LICENSE file for details. * Created: May 31, 2014 */ package org.noroomattheinn.visibletesla; import java.util.ArrayList; import java.util.Calendar; import java.util.Date; import java.util.List; import java.util.Map; import org.noroomattheinn.utils.GeoUtils; import org.noroomattheinn.utils.Utils; import org.noroomattheinn.visibletesla.vehicle.VTVehicle; import static org.noroomattheinn.tesla.Tesla.logger; /** * MessageTemplate * * @author Joe Pasqua <joe at NoRoomAtTheInn dot org> */ public class MessageTemplate { /*------------------------------------------------------------------------------ * * Internal State * *----------------------------------------------------------------------------*/ // The overall message template is represented by a list of MsgComponents private List<MsgComponent> components; /*============================================================================== * ------- ------- * ------- Public Interface To This Class ------- * ------- ------- *============================================================================*/ public MessageTemplate(String format) { components = new ArrayList<>(); if (format == null) { return; } parse(format); } public String getMessage(AppAPI api, VTVehicle v, Map<String,String> contextSpecific) { StringBuilder sb = new StringBuilder(); for (MsgComponent mc : components) { sb.append(mc.asString(api, v, contextSpecific)); } return sb.toString(); } /*------------------------------------------------------------------------------ * * PRIVATE Methods that parse a template string * *----------------------------------------------------------------------------*/ private void parse(String input) { while (input != null) { input = next(input); } } private String next(String input) { if (input == null) return null; if (input.isEmpty()) return null; int length = input.length(); for (int i = 0; i < length; i++) { if (input.charAt(i) == '{') { if (i+1 != length && input.charAt(i+1) == '{') { // Matched {{ if (i != 0) { components.add(new MsgComponent.StrComponent(input.substring(0, i))); return input.substring(i); } else { // Search for matching }} for (int j = i+2; j < length; j++) { if (input.charAt(j) == '}') { if (j+1 != length && input.charAt(j+1) == '}') { // Founding matching }} components.add(new MsgComponent.VarComponent(input.substring(i+2, j))); return input.substring(j+2); } } } } } } } components.add(new MsgComponent.StrComponent(input)); return null; } /*------------------------------------------------------------------------------ * * PRIVATE class that implements an individual component of a message * *----------------------------------------------------------------------------*/ private static abstract class MsgComponent { abstract String asString(AppAPI api, VTVehicle v, Map<String,String> contextSpecific); // A String component is very simple - it's just a literal String static class StrComponent extends MsgComponent { public String string; StrComponent(String s) { this.string = s; } @Override String asString(AppAPI api, VTVehicle v, Map<String,String> cs) { return string; } } // A Variable component represents a formatted reading from the car static class VarComponent extends MsgComponent { public String varName; VarComponent(String v) { this.varName = v; } @Override String asString( AppAPI api, VTVehicle v, Map<String,String> contextSpecific) { String val; switch (varName) { case "SPEED": val = String.format( "%3.1f", v.inProperUnits(v.streamState.get().speed)); break; case "SOC": val = String.valueOf(v.streamState.get().soc); break; case "IDEAL": val = String.format( "%3.1f", v.inProperUnits(v.chargeState.get().idealRange)); break; case "RATED": val = String.format( "%3.1f", v.inProperUnits(v.chargeState.get().range)); break; case "ESTIMATED": val = String.format( "%3.1f", v.inProperUnits(v.chargeState.get().estimatedRange)); break; case "CHARGE_STATE": val = v.chargeState.get().chargingState.name(); break; case "D_UNITS": val = v.unitType() == Utils.UnitType.Imperial ? "mi" : "km"; break; case "S_UNITS": val = v.unitType() == Utils.UnitType.Imperial ? "mph" : "km/h"; break; case "DATE": val = String.format("%1$tY-%1$tm-%1$td", new Date()); break; case "TIME": val = String.format("%1$tH:%1$tM:%1$tS", new Date()); break; case "LOC": case "HT_LOC": String lat = String.valueOf(v.streamState.get().estLat); String lng = String.valueOf(v.streamState.get().estLng); val = GeoUtils.getAddrForLatLong(lat, lng); if (val == null || val.isEmpty()) { val = String.format("(%s, %s)", lat, lng); } if (varName.equals("HT_LOC")) { try { val = String.format( "<a href='http://maps.google.com/maps?z=12&t=m&q=@%s,%s'>%s</a>", lat, lng, val); } catch (Exception e) { // In case something goes wrong with the format logger.severe(e.getMessage()); } } break; case "I_STATE": val = api.state.get().name(); break; case "I_MODE": val = api.mode.get().name(); break; case "P_CURRENT": val = String.valueOf(v.chargeState.get().chargerPilotCurrent); break; case "TIME_TO_FULL": val = v.chargeState.get().timeToFull(); break; case "CHARGE_ETA": long msToFull = (long)(v.chargeState.get().timeToFullCharge * (60*60*1000)); Calendar when = Calendar.getInstance(); when.setTimeInMillis(System.currentTimeMillis() + msToFull); val = String.format("%02d:%02d:%02d", when.get(Calendar.HOUR_OF_DAY), when.get(Calendar.MINUTE), when.get(Calendar.SECOND)); break; case "C_RATE": val = String.format("%.1f", v.inProperUnits(v.chargeState.get().chargeRate)); break; case "C_AMP": val = String.format("%.1f", v.chargeState.get().batteryCurrent); break; case "C_VLT": val = String.valueOf(v.chargeState.get().chargerVoltage); break; case "C_PWR": val = String.valueOf(v.chargeState.get().chargerPower); break; case "HT_SOC_G": val = genSOCGauge(v); break; case "HT_ODO": val = genODO(v); break; case "HT_RATED_G": val = genGaugeWrapper(v, "Rated", v.chargeState.get().range); break; case "HT_IDEAL_G": val = genGaugeWrapper(v, "Ideal", v.chargeState.get().idealRange); break; case "HT_ESTIMATED_G": val = genGaugeWrapper(v, "Estimated", v.chargeState.get().estimatedRange); break; case "HT_SPEEDO": val = genSpeedo(v); break; case "HT_CARVIEW": val = genCarView(v); break; case "ODO": val = String.format("%.1f", v.inProperUnits(v.streamState.get().odometer)); break; default: val = (contextSpecific == null) ? null : contextSpecific.get(varName); if (val == null) { val = "Unknown variable: " + varName; } break; } return val; } private static final String SpeedoTemplate = "<canvas id='speedo' width='150' height='150'></canvas>" + "<script src='../scripts/CanvasUtils.js' type='text/javascript'></script>" + "<script src='../scripts/SpeedGauge.js' type='text/javascript'></script>" + "<script type='text/javascript'>" + "speedGauge(document.getElementById('speedo').getContext('2d'), 150, 150, %f, %f);" + "</script>"; private String genSpeedo(VTVehicle v) { double speed = v.inProperUnits(v.streamState.get().speed); double power = v.inProperUnits(v.streamState.get().power); return String.format(SpeedoTemplate, speed, power); } private static final String CarViewFormat = "<div class=\"carview\">\n"+ " <canvas id=\"cb\" width=\"540\" height=\"330\"> </canvas>\n" + " <script src=\"../scripts/CarView.js\" type=\"text/javascript\"></script>\n" + " <script>\n" + " var ctx = document.getElementById(\"cb\").getContext(\"2d\");\n" + " var carDetails = %s;\n"+ " var carState = %s;\n" + " carView(ctx, carDetails, carState);\n" + " </script>"+ "</div>"; private String genCarView(VTVehicle v) { return String.format( CarViewFormat, v.carDetailsAsJSON(), v.carStateAsJSON()); } private static final String SOCGaugeTemplate = "<canvas id='bg' width='140' height='50'></canvas>" + "<script src='../scripts/CanvasUtils.js' type='text/javascript'></script>" + "<script src='../scripts/BatteryGauge.js' type='text/javascript'></script>" + "<script type='text/javascript'>" + "batteryGauge(document.getElementById('bg').getContext('2d'), 100, 50, %d, %b);" + "</script>"; private String genSOCGauge(VTVehicle v) { int soc = v.streamState.get().soc; boolean showPlug = false; switch (v.chargeState.get().chargingState) { case Charging: case Complete: showPlug = true; break; } return String.format(SOCGaugeTemplate, soc, showPlug); } private static String genODO(VTVehicle v) { String punc = v.unitType() == Utils.UnitType.Imperial ? "," : "."; StringBuilder sb = new StringBuilder(); double odo = v.inProperUnits(v.streamState.get().odometer); double modulus = 100000; for (int i = 0; i < 6; i++) { int digit = (int)(odo / modulus); if (i == 3) { sb.append("<span class='punc_box'>"); sb.append(punc); sb.append("</span>"); } sb.append("<span class='dark_box'>"); sb.append(digit); sb.append("</span>"); odo = odo % modulus; modulus /= 10; } int tenths = (int)(odo * 10); sb.append("<span class='light_box'>"); sb.append(tenths); sb.append("</span>"); return sb.toString(); } private static String genGaugeWrapper(VTVehicle v, String label, double val) { return genGauge( label, v.inProperUnits(val), v.unitType() == Utils.UnitType.Imperial ? "Miles" : "Km", val < 25); } private static String genGauge( String label, double val, String units, boolean critical) { StringBuilder sb = new StringBuilder(); sb.append("<div class='inset' style='width:140px'>"); sb.append("<table border='0' width='100%' style='margin:0px;padding:0px'>"); sb.append("<tr width='100%'> <td width='100%' colspan='3' align='center' class='gaugeLabel'>"); sb.append(label); sb.append("</td> </tr> <tr> <td class='gaugeSymbol'>"); if (critical) sb.append("❗"); sb.append("</td> <td class='gaugeReadout'>"); sb.append(String.format("%.1f", val)); sb.append("</td> <td class='gaugeUnits'>"); sb.append(units); sb.append("</td></tr></table></div>"); return sb.toString(); } } } }