/* CanZE Take a closer look at your ZE car Copyright (C) 2015 - The CanZE Team http://canze.fisch.lu This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see <http://www.gnu.org/licenses/>. */ package lu.fisch.canze.widgets; import com.google.gson.Gson; import com.google.gson.reflect.TypeToken; import java.lang.reflect.Type; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Calendar; import java.util.HashMap; import lu.fisch.awt.Color; import lu.fisch.awt.Graphics; import lu.fisch.awt.Polygon; import lu.fisch.canze.activities.MainActivity; import lu.fisch.canze.actors.Field; import lu.fisch.canze.actors.Fields; import lu.fisch.canze.classes.TimePoint; import lu.fisch.canze.database.CanzeDataSource; import lu.fisch.canze.interfaces.DrawSurfaceInterface; /** * * @author robertfisch */ public class Timeplot extends Drawable { protected HashMap<String,ArrayList<TimePoint>> values = new HashMap<>(); private boolean backward = true; public Timeplot() { super(); } public Timeplot(int x, int y, int width, int height) { this.x = x; this.y = y; this.width = width; this.height = height; // test } public Timeplot(DrawSurfaceInterface drawSurface, int x, int y, int width, int height) { this.drawSurface=drawSurface; this.x = x; this.y = y; this.width = width; this.height = height; } public void addValue(String fieldSID, double value) { //MainActivity.debug(values.size()+""); if(!values.containsKey(fieldSID)) values.put(fieldSID,new ArrayList<TimePoint>()); values.get(fieldSID).add(new TimePoint(Calendar.getInstance().getTimeInMillis(), value)); /* if(value<min) setMin((int) value - 1); else if(value>max) setMax((int) value + 1); */ /*setMinorTicks(0); setMajorTicks(1); if(getMax()-getMin()>100) setMajorTicks(10); else if(getMax()-getMin()>1000) setMajorTicks(100); else if(getMax()-getMin()>10000) setMajorTicks(1000); /**/ } private Color getColor(int i) { if(i==0) return Color.RENAULT_RED; else if (i==1) return Color.BLUE; else return Color.GREEN_DARK; } @Override public void draw(Graphics g) { // background g.setColor(getBackground()); g.fillRect(x, y, width, height); // black border g.setColor(getForeground()); g.drawRect(x, y, width, height); // calculate fill height //int fillHeight = (int) ((value-min)/(double)(max-min)*(height-1)); int barWidth = width-Math.max(g.stringWidth(min+""),g.stringWidth(max+""))-10-10; int spaceAlt = Math.max(g.stringWidth(minAlt+""),g.stringWidth(maxAlt+""))+10+10; // reduce with if second y-axe is used //MainActivity.debug("Alt: "+minAlt+" - "+maxAlt); if (minAlt==0 && maxAlt==0) { spaceAlt=0; } barWidth-=spaceAlt; SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss"); int graphHeight = height-g.stringHeight(sdf.format(Calendar.getInstance().getTime()))-5; // draw the ticks double realMaxAlt=getMaxAlt(); if(minorTicks>0 || majorTicks>0) { int toTicks = minorTicks; if(toTicks==0) toTicks=majorTicks; double intervals = ((max-min)/(double)toTicks); double accel = (double)graphHeight/intervals; double ax,ay,bx=0,by=0; int actual = min; int actualAlt = minAlt; int sum = 0; for(double i=graphHeight; i>=0; i-=accel) { if(minorTicks>0) { g.setColor(getForeground()); ax = x+width-barWidth-spaceAlt-5; ay = y+i; bx = x+width-barWidth-spaceAlt; by = y+i; g.drawLine((int)ax, (int)ay, (int)bx, (int)by); if(spaceAlt!=0) { ax = x+width-spaceAlt; ay = y+i; bx = ax+5; by = y+i; g.drawLine((int)ax, (int)ay, (int)bx, (int)by); } } // draw majorTicks if(majorTicks!=0 && sum % majorTicks == 0) { if(majorTicks>0) { g.setColor(getIntermediate()); ax = x+width-spaceAlt-barWidth-10; ay = y+i; bx = x+width-spaceAlt+(spaceAlt!=0?10:0); by = y+i; g.drawLine((int) ax, (int) ay, (int) bx, (int) by); g.setColor(getForeground()); ax = x+width-barWidth-spaceAlt-10; ay = y+i; bx = x+width-barWidth-spaceAlt; by = y+i; g.drawLine((int)ax, (int)ay, (int)bx, (int)by); if(spaceAlt!=0) { ax = x+width-spaceAlt; ay = y+i; bx = ax+10; by = y+i; g.drawLine((int)ax, (int)ay, (int)bx, (int)by); } } // draw String if(showLabels) { g.setColor(getForeground()); String text = (actual)+""; double sw = g.stringWidth(text); bx = x+width-barWidth-16-sw-spaceAlt; by = y+i; g.drawString(text, (int)(bx), (int)(by+g.stringHeight(text)*(1-i/graphHeight))); // alternative labels if(spaceAlt!=0) { text = (actualAlt)+""; sw = g.stringWidth(text); bx = x+width-spaceAlt+16; by = y+i; g.drawString(text, (int)(bx), (int)(by+g.stringHeight(text)*(1-i/graphHeight))); } } actual+=majorTicks; actualAlt+= Math.round((double) (maxAlt - minAlt) / (max - min) * majorTicks); } sum+=minorTicks; } // calculate real max alt double altTicks = Math.round((double) (maxAlt - minAlt) / (max - min) * majorTicks)/((double)majorTicks/toTicks); realMaxAlt=intervals*altTicks+getMinAlt(); } // draw the vertical grid g.setColor(getIntermediate()); long start = Calendar.getInstance().getTimeInMillis()/1000; int interval = 60/timeSale; try { if(backward) { ArrayList<TimePoint> list = this.values.get(sids.get(0)); start = list.get(list.size()-1).date; for(int s=0; s<sids.size(); s++) { list = this.values.get(sids.get(s)); long thisDate = list.get(list.size()-1).date; if(thisDate>start) start=thisDate; } //MainActivity.debug("Start: "+sdf.format(start)); for(long x=width-(start%interval)-spaceAlt; x>=width-barWidth-spaceAlt; x-=interval) { g.drawLine(x, 1, x, graphHeight + 5); } } else { ArrayList<TimePoint> list = this.values.get(sids.get(0)); start = list.get(0).date; for(int s=0; s<sids.size(); s++) { list = this.values.get(sids.get(s)); long thisDate = list.get(0).date; if(thisDate<start) start=thisDate; } for(long x=width-barWidth-spaceAlt; x<width-spaceAlt; x+=interval) { g.drawLine(x, 1, x, graphHeight + 5); } } } catch(Exception e) { //MainActivity.debug("Exception: "+e.getMessage()); for(long x=width-(start%interval)-spaceAlt; x>=width-barWidth-spaceAlt; x-=interval) { g.drawLine(x, 1, x, graphHeight + 5); } } // draw the graph for(int s=0; s<sids.size(); s++) { String sid = sids.get(s); ArrayList<TimePoint> values = this.values.get(sid); // setup an empty list if no list has been found if(values==null) { values = new ArrayList<>(); this.values.put(sid,values); } g.setColor(getForeground()); g.drawRect(x + width - barWidth-spaceAlt, y, barWidth, graphHeight); if (values.size() > 0) { double w = (double) barWidth / values.size(); double h = (double) graphHeight / (getMax() - getMin()); double hAlt = (double) graphHeight / (realMaxAlt - getMinAlt()); double lastX = Double.NaN; double lastY = Double.NaN; g.setColor(getColor(s)); if(isBackward()) { long maxTime = start; //values.get(values.size() - 1).date; for (int i = values.size() - 1; i >= 0; i--) { TimePoint tp = values.get(i); if (tp != null) { g.setColor(colorRanges.getColor(sid, tp.value, getColor(s))); double mx = barWidth - ((maxTime - tp.date) / timeSale / 1000); if (mx < 0) { values.remove(i); } else { // determine Y double my; // distinct "alt" vs "normal" if (getOptions().getOption(sid) != null && getOptions().getOption(sid).contains("alt")) my = graphHeight - (tp.value - minAlt) * hAlt; else my = graphHeight - (tp.value - min) * h; // check if y should be fixed: colorline[value-of-y] if ((getOptions().getOption(sid) != null && !getOptions().getOption(sid).isEmpty() && getOptions().getOption(sid).contains("colorline"))) { // parse out position of line String options = getOptions().getOption(sid); int index = options.indexOf("colorline"); index += ("colorline").length() + 1; String value = ""; while (index < options.length() && options.charAt(index) != ']') { value += options.charAt(index); index++; } if (getOptions().getOption(sid) != null && getOptions().getOption(sid).contains("alt")) my = graphHeight - (Double.valueOf(value) - minAlt) * hAlt; else my = graphHeight - (Double.valueOf(value) - min) * h; } // now get ZY double zy; if (getOptions().getOption(sid) != null && getOptions().getOption(sid).contains("alt")) zy = graphHeight - (0 - minAlt) * hAlt; else zy = graphHeight - (0 - min) * h; int rayon = 2; //MainActivity.debug("HERE: "+sid+" / "+getOptions().getOption(sid)); if (getOptions().getOption(sid) == null || (getOptions().getOption(sid) != null && (getOptions().getOption(sid).isEmpty() || getOptions().getOption(sid).contains("dot")))) { g.fillOval(getX() + getWidth() - barWidth + (int) mx - rayon - spaceAlt, getY() + (int) my - rayon, 2 * rayon + 1, 2 * rayon + 1); } if (i < values.size() - 1) { if (getOptions().getOption(sid) != null && getOptions().getOption(sid).contains("full")) { if ((lastY != Double.NaN) && (lastX != Double.NaN) && (lastY != 0.0) && (lastX != 0.0)) { Polygon p = new Polygon(); p.addPoint(getX() + getWidth() - barWidth + (int) lastX - spaceAlt, getY() + (int) lastY); p.addPoint(getX() + getWidth() - barWidth + (int) mx - spaceAlt, getY() + (int) my); p.addPoint(getX() + getWidth() - barWidth + (int) mx - spaceAlt, (int) (getY() + zy)); p.addPoint(getX() + getWidth() - barWidth + (int) lastX - spaceAlt, (int) (getY() + zy)); g.fillPolygon(p); } } else if (getOptions().getOption(sid) != null && getOptions().getOption(sid).contains("gradient")) { if (i < values.size() && values.get(i + 1) != null) { if ((lastY != Double.NaN) && (lastX != Double.NaN) && (lastY != 0.0) && (lastX != 0.0)) { Polygon p = new Polygon(); p.addPoint(getX() + getWidth() - barWidth + (int) lastX - spaceAlt, getY() + (int) lastY); p.addPoint(getX() + getWidth() - barWidth + (int) mx - spaceAlt, getY() + (int) my); p.addPoint(getX() + getWidth() - barWidth + (int) mx - spaceAlt, (int) (getY() + zy)); p.addPoint(getX() + getWidth() - barWidth + (int) lastX - spaceAlt, (int) (getY() + zy)); int[] colors = colorRanges.getColors(sid); float[] spacings = colorRanges.getSpacings(sid, min, max); if (colors.length == spacings.length) g.setGradient(0, graphHeight, 0, 0, colors, spacings); g.fillPolygon(p); g.clearGradient(); //else MainActivity.debug("size not equal: "+colors.length+"=="+spacings.length); } } } else { if(lastX!=Double.NaN && lastY!=Double.NaN && lastX!=0.0 && lastY!=0.0) { g.drawLine(getX() + getWidth() - barWidth + (int) lastX - spaceAlt, getY() + (int) lastY, getX() + getWidth() - barWidth + (int) mx - spaceAlt, getY() + (int) my); } } } lastX = mx; lastY = my; } } } } else // forward { long minTime = start; //values.get(0).date; for (int i = 0; i < values.size() ; i++) { TimePoint tp = values.get(i); if (tp != null) { g.setColor(colorRanges.getColor(sid, tp.value, getColor(s))); double mx = ((tp.date-minTime) / timeSale / 1000); if (mx > barWidth) { // ignore point that are out of scope but do not delete them //values.remove(i); } else { double my = graphHeight - (tp.value - min) * h; // check if y should be fixed: colorline[value-of-y] if ((getOptions().getOption(sid) != null && !getOptions().getOption(sid).isEmpty() && getOptions().getOption(sid).contains("colorline"))) { // parse out position of line String options = getOptions().getOption(sid); int index = options.indexOf("colorline"); index += ("colorline").length() + 1; String value = ""; while (index < options.length() && options.charAt(index) != ']') { value += options.charAt(index); index++; } my = graphHeight - (Double.valueOf(value) - min) * h; } double zy = graphHeight - (0 - min) * h; // draw on alternate scale if requested if (getOptions().getOption(sid) != null && getOptions().getOption(sid).contains("alt")) { my = graphHeight - (tp.value - minAlt) * hAlt; zy = graphHeight - (0 - minAlt) * hAlt; } int rayon = 2; //MainActivity.debug("HERE: "+sid+" / "+getOptions().getOption(sid)); if (getOptions().getOption(sid) == null || (getOptions().getOption(sid) != null && (getOptions().getOption(sid).isEmpty() || getOptions().getOption(sid).contains("dot")))) { g.fillOval(getX() + getWidth() - barWidth + (int) mx - rayon - spaceAlt, getY() + (int) my - rayon, 2 * rayon + 1, 2 * rayon + 1); } if (i > 0) { if (getOptions().getOption(sid) != null && getOptions().getOption(sid).contains("full")) { if ((lastY != Double.NaN) && (lastX != Double.NaN) && (lastY != 0.0) && (lastX != 0.0)) { Polygon p = new Polygon(); p.addPoint(getX() + getWidth() - barWidth + (int) lastX - spaceAlt, getY() + (int) lastY); p.addPoint(getX() + getWidth() - barWidth + (int) mx - spaceAlt, getY() + (int) my); p.addPoint(getX() + getWidth() - barWidth + (int) mx - spaceAlt, (int) (getY() + zy)); p.addPoint(getX() + getWidth() - barWidth + (int) lastX - spaceAlt, (int) (getY() + zy)); g.fillPolygon(p); } } else if (getOptions().getOption(sid) != null && getOptions().getOption(sid).contains("gradient")) { //if (i < values.size() && values.get(i - 1) != null) { if ((lastY != Double.NaN) && (lastX != Double.NaN) && (lastY != 0.0) && (lastX != 0.0)) { Polygon p = new Polygon(); p.addPoint(getX() + getWidth() - barWidth + (int) lastX - spaceAlt, getY() + (int) lastY); p.addPoint(getX() + getWidth() - barWidth + (int) mx - spaceAlt, getY() + (int) my); p.addPoint(getX() + getWidth() - barWidth + (int) mx - spaceAlt, (int) (getY() + zy)); p.addPoint(getX() + getWidth() - barWidth + (int) lastX - spaceAlt, (int) (getY() + zy)); int[] colors = colorRanges.getColors(sid); float[] spacings = colorRanges.getSpacings(sid, min, max); if (colors.length == spacings.length) g.setGradient(0, graphHeight, 0, 0, colors, spacings); g.fillPolygon(p); g.clearGradient(); //else MainActivity.debug("size not equal: "+colors.length+"=="+spacings.length); } //} } else { if(lastX!=Double.NaN && lastY!=Double.NaN && (lastY != 0.0) && (lastX != 0.0)) { g.drawLine(getX() + getWidth() - barWidth + (int) lastX - spaceAlt, getY() + (int) lastY, getX() + getWidth() - barWidth + (int) mx - spaceAlt, getY() + (int) my); } } } lastX = mx; lastY = my; } } } } } } // clean bottom g.setColor(getBackground()); g.fillRect(width - barWidth - 2, graphHeight + 1, barWidth + 1, height - graphHeight - 2); // draw bottom axis int c = 0; int ts = (int) timeSale; //MainActivity.debug("Start : "+sdf.format(start)); if(backward) { for(long x=width-(start%interval)-spaceAlt; x>=width-barWidth-spaceAlt; x-=interval) { if(c%(5*ts)==0) { g.setColor(getForeground()); g.drawLine(x, graphHeight, x, graphHeight + 10); String date = sdf.format((start - ((start % interval))*timeSale - interval * c*timeSale*1000)); g.drawString(date, x - g.stringWidth(date) - 4, height - 2); } else { g.setColor(getForeground()); g.drawLine(x, graphHeight, x, graphHeight + 3); } c++; } } else { //MainActivity.debug("START: "+sdf.format(start)); for(long x=width-barWidth-spaceAlt; x<width-spaceAlt; x+=interval) { if(c%(5*ts)==0) { g.setColor(getForeground()); g.drawLine(x, graphHeight, x, graphHeight + 10); String date = sdf.format( start + (interval * c*timeSale*1000)); g.drawString(date, x - g.stringWidth(date) - 4, height - 2); } else { g.setColor(getForeground()); g.drawLine(x, graphHeight, x, graphHeight + 3); } c++; } } // draw the title if(title!=null && !title.equals("")) { g.setColor(getTitleColor()); g.setTextSize(20); int th = g.stringHeight(title); int tx = getX()+width-barWidth+8-spaceAlt; int ty = getY()+th+4; g.drawString(title,tx,ty); } // draw the value if(showValue) { for(int s=0; s<sids.size(); s++) { String sid = sids.get(s); ArrayList<TimePoint> values = this.values.get(sid); Field field = Fields.getInstance().getBySID(sid); String text; if(field !=null) { text = String.format("%." + String.valueOf(field.getDecimals()) + "f", field.getValue()); } else { if(values.size()==0) text="N/A"; else text = String.valueOf(values.get(values.size()-1).value); } g.setTextSize(40); if(values.isEmpty()) g.setColor(getColor(s)); else g.setColor(colorRanges.getColor(sid, values.get(values.size()-1).value, getColor(s))); int tw = g.stringWidth(text); int th = g.stringHeight(text); int tx = getX()+width-tw-8-spaceAlt; int ty = getY()+(s+1)*(th+4); g.drawString(text, tx, ty); } } // black border g.setColor(getForeground()); g.drawRect(x, y, width, height); } @Override public void onFieldUpdateEvent(Field field) { addValue(field.getSID(),field.getValue()); super.onFieldUpdateEvent(field); } @Override public String dataToJson() { Gson gson = new Gson(); return gson.toJson(values.clone()); } @Override public void dataFromJson(String json) { Gson gson = new Gson(); Type fooType = new TypeToken<HashMap<String,ArrayList<TimePoint>>>() {}.getType(); values = gson.fromJson(json,fooType); } @Override public void loadValuesFromDatabase() { super.loadValuesFromDatabase(); //values.clear(); // not needed as items will be replaced anyway! for(int s=0; s<sids.size(); s++) { String sid = sids.get(s); values.put(sid, CanzeDataSource.getInstance().getData(sid)); } } public void addField(String sid) { super.addField(sid); if(!values.containsKey(sid)) { values.put(sid, new ArrayList<TimePoint>()); } } public void setValues(HashMap<String, ArrayList<TimePoint>> values) { sids.clear(); for (String key : values.keySet()) { sids.add(key); } this.values = values; } public boolean isBackward() { return backward; } public void setBackward(boolean backward) { this.backward = backward; } }