|
|
|
@@ -1,233 +0,0 @@
|
|
|
|
|
package com.evsuite.abrp;
|
|
|
|
|
|
|
|
|
|
import android.content.Context;
|
|
|
|
|
import android.graphics.Canvas;
|
|
|
|
|
import android.graphics.Paint;
|
|
|
|
|
import android.graphics.Path;
|
|
|
|
|
import android.graphics.RectF;
|
|
|
|
|
import android.graphics.Typeface;
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* The Aviation Overview style: a pilot's primary flight display.
|
|
|
|
|
*
|
|
|
|
|
* The speed on a scrolling tape at the left and the battery on one at the right (where the
|
|
|
|
|
* altitude would be), each with a readout box. In the middle, a horizon that "pitches" with the
|
|
|
|
|
* power: drawing power raises the nose (the horizon drops), regenerating lowers it; a ladder of
|
|
|
|
|
* kW marks and the yellow aircraft symbol over it. Green annunciators at the top (gear, power,
|
|
|
|
|
* clock), boxes of range, trip, usage, climate and odometer along the bottom. Unread values
|
|
|
|
|
* show "—" and leave the horizon level.
|
|
|
|
|
*
|
|
|
|
|
* The tapes and the horizon ease to new values and redraw only until settled, only on screen.
|
|
|
|
|
*/
|
|
|
|
|
final class AviationView extends ClusterCanvasView {
|
|
|
|
|
|
|
|
|
|
private static final long FRAME_MS = 33;
|
|
|
|
|
|
|
|
|
|
private final Paint fill = new Paint(Paint.ANTI_ALIAS_FLAG);
|
|
|
|
|
private final Paint line = new Paint(Paint.ANTI_ALIAS_FLAG);
|
|
|
|
|
private final Paint text = new Paint(Paint.ANTI_ALIAS_FLAG);
|
|
|
|
|
private final Path path = new Path();
|
|
|
|
|
private final RectF rect = new RectF();
|
|
|
|
|
private final int sky = 0xFF1E4C80, ground = 0xFF5A3A1E, white = 0xFFF2F4F7, green = 0xFF3CE07A,
|
|
|
|
|
cyan = 0xFF4FD8F0, magenta = 0xFFE05CE0, yellow = 0xFFFFD21F, tape = 0xCC1A1F26, amber = 0xFFFFB000;
|
|
|
|
|
private float speedShown = Float.NaN, socShown = Float.NaN, powerShown = Float.NaN;
|
|
|
|
|
|
|
|
|
|
AviationView(Context context) {
|
|
|
|
|
super(context);
|
|
|
|
|
line.setStyle(Paint.Style.STROKE);
|
|
|
|
|
line.setStrokeCap(Paint.Cap.ROUND);
|
|
|
|
|
text.setTypeface(Typeface.create("sans-serif-condensed", Typeface.BOLD));
|
|
|
|
|
text.setFontFeatureSettings("tnum");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@Override
|
|
|
|
|
protected void onDraw(Canvas c) {
|
|
|
|
|
super.onDraw(c);
|
|
|
|
|
float w = getWidth(), h = getHeight();
|
|
|
|
|
if (w <= 0 || h <= 0) return;
|
|
|
|
|
speedShown = ease(speedShown, v.speedKmh, 180f);
|
|
|
|
|
socShown = ease(socShown, v.socPercent, 100f);
|
|
|
|
|
powerShown = ease(powerShown, v.powerKw, 200f);
|
|
|
|
|
boolean moving = (v.speedKmh != null && speedShown != v.speedKmh)
|
|
|
|
|
|| (v.socPercent != null && socShown != v.socPercent)
|
|
|
|
|
|| (v.powerKw != null && powerShown != v.powerKw);
|
|
|
|
|
|
|
|
|
|
c.drawColor(0xFF05070A);
|
|
|
|
|
float top = h * 0.10f, bottom = h * 0.84f;
|
|
|
|
|
float tapeW = w * 0.10f;
|
|
|
|
|
float leftTape = w * 0.04f, rightTape = w - w * 0.04f - tapeW;
|
|
|
|
|
drawHorizon(c, leftTape + tapeW + 16f * dp, top, rightTape - 16f * dp, bottom);
|
|
|
|
|
drawTape(c, leftTape, top, tapeW, bottom, v.speedKmh == null ? null : speedShown, 0f, 250f, 10f, 20f,
|
|
|
|
|
fmt(v.speedKmh, "%.0f", "—"), "KM/H", true);
|
|
|
|
|
drawTape(c, rightTape, top, tapeW, bottom, v.socPercent == null ? null : socShown, 0f, 100f, 5f, 10f,
|
|
|
|
|
fmt(v.socPercent, "%.0f", "—"), "BATT %", false);
|
|
|
|
|
drawAnnunciators(c, w, top);
|
|
|
|
|
drawBoxes(c, w, h, bottom);
|
|
|
|
|
keepAnimating(moving, FRAME_MS);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** Sky and ground split by a horizon that moves with the power, a kW ladder, the aircraft. */
|
|
|
|
|
private void drawHorizon(Canvas c, float l, float t, float r, float b) {
|
|
|
|
|
float cx = (l + r) / 2f, cy = (t + b) / 2f, half = (b - t) / 2f;
|
|
|
|
|
// 150 kW lifts the nose by 0.8 of the half height; regeneration lowers it, 50 kW as far.
|
|
|
|
|
float pitch = v.powerKw == null ? 0f
|
|
|
|
|
: powerShown >= 0f ? Math.min(150f, powerShown) / 150f : Math.max(-50f, powerShown) / 50f * 0.6f;
|
|
|
|
|
float horizon = cy + pitch * half * 0.8f;
|
|
|
|
|
float perKw = half * 0.8f / 150f;
|
|
|
|
|
c.save();
|
|
|
|
|
rect.set(l, t, r, b);
|
|
|
|
|
c.clipRect(rect);
|
|
|
|
|
fill.setColor(sky);
|
|
|
|
|
c.drawRect(l, t, r, horizon, fill);
|
|
|
|
|
fill.setColor(ground);
|
|
|
|
|
c.drawRect(l, horizon, r, b, fill);
|
|
|
|
|
line.setColor(white);
|
|
|
|
|
line.setStrokeWidth(2.5f * dp);
|
|
|
|
|
c.drawLine(l, horizon, r, horizon, line);
|
|
|
|
|
// The ladder: a mark every 25 kW above the horizon (drawing), below it (regenerating).
|
|
|
|
|
text.setTextSize(13f * dp);
|
|
|
|
|
for (int kw = -50; kw <= 150; kw += 25) {
|
|
|
|
|
if (kw == 0) continue;
|
|
|
|
|
float y = horizon - kw * (kw > 0 ? perKw : perKw * 150f / 50f * 0.6f);
|
|
|
|
|
float half2 = kw % 50 == 0 ? (r - l) * 0.10f : (r - l) * 0.05f;
|
|
|
|
|
line.setStrokeWidth(1.5f * dp);
|
|
|
|
|
c.drawLine(cx - half2, y, cx + half2, y, line);
|
|
|
|
|
if (kw % 50 == 0) {
|
|
|
|
|
text.setColor(white);
|
|
|
|
|
text.setTextAlign(Paint.Align.LEFT);
|
|
|
|
|
c.drawText(String.valueOf(kw), cx + half2 + 6f * dp, y + 5f * dp, text);
|
|
|
|
|
text.setTextAlign(Paint.Align.RIGHT);
|
|
|
|
|
c.drawText(String.valueOf(kw), cx - half2 - 6f * dp, y + 5f * dp, text);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
c.restore();
|
|
|
|
|
// The aircraft symbol, fixed: two wings and a dot, yellow outlined in black.
|
|
|
|
|
float wing = (r - l) * 0.14f;
|
|
|
|
|
for (int pass = 0; pass < 2; pass++) {
|
|
|
|
|
line.setColor(pass == 0 ? 0xFF000000 : yellow);
|
|
|
|
|
line.setStrokeWidth(pass == 0 ? 9f * dp : 5f * dp);
|
|
|
|
|
c.drawLine(cx - wing * 2f, cy, cx - wing, cy, line);
|
|
|
|
|
c.drawLine(cx - wing, cy, cx - wing, cy + wing * 0.25f, line);
|
|
|
|
|
c.drawLine(cx + wing * 2f, cy, cx + wing, cy, line);
|
|
|
|
|
c.drawLine(cx + wing, cy, cx + wing, cy + wing * 0.25f, line);
|
|
|
|
|
}
|
|
|
|
|
fill.setColor(yellow);
|
|
|
|
|
c.drawCircle(cx, cy, 5f * dp, fill);
|
|
|
|
|
// The power readout in magenta under the symbol.
|
|
|
|
|
text.setTextAlign(Paint.Align.CENTER);
|
|
|
|
|
text.setTextSize(22f * dp);
|
|
|
|
|
text.setColor(v.powerKw != null && v.powerKw < 0f ? cyan : magenta);
|
|
|
|
|
c.drawText(fmt(v.powerKw, "%.1f kW", "— kW"), cx, b - 14f * dp, text);
|
|
|
|
|
line.setColor(0xFF2A3038);
|
|
|
|
|
line.setStrokeWidth(2f * dp);
|
|
|
|
|
c.drawRect(l, t, r, b, line);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* A vertical tape scrolling with its value: ticks every {@code minor}, numbers every
|
|
|
|
|
* {@code major}, and a readout box at the middle pointing in (right for the left tape).
|
|
|
|
|
*/
|
|
|
|
|
private void drawTape(Canvas c, float l, float t, float tw, float b, Float value, float min, float max,
|
|
|
|
|
float minor, float major, String readout, String title, boolean left) {
|
|
|
|
|
float cy = (t + b) / 2f, perUnit = (b - t) / (max - min) * (max > 150f ? 3.2f : 2.2f);
|
|
|
|
|
fill.setColor(tape);
|
|
|
|
|
c.drawRect(l, t, l + tw, b, fill);
|
|
|
|
|
c.save();
|
|
|
|
|
c.clipRect(l, t, l + tw, b);
|
|
|
|
|
float centre = value == null ? min : value;
|
|
|
|
|
text.setTextSize(16f * dp);
|
|
|
|
|
text.setColor(white);
|
|
|
|
|
line.setColor(white);
|
|
|
|
|
// No value, no scale: a tape centred on zero would read as 0.
|
|
|
|
|
for (float u = min; value != null && u <= max; u += minor) {
|
|
|
|
|
float y = cy - (u - centre) * perUnit;
|
|
|
|
|
if (y < t - 20f * dp || y > b + 20f * dp) continue;
|
|
|
|
|
boolean isMajor = Math.round(u) % Math.round(major) == 0;
|
|
|
|
|
float len = isMajor ? tw * 0.22f : tw * 0.12f;
|
|
|
|
|
line.setStrokeWidth(2f * dp);
|
|
|
|
|
float edge = left ? l + tw : l;
|
|
|
|
|
c.drawLine(edge, y, edge + (left ? -len : len), y, line);
|
|
|
|
|
if (isMajor) {
|
|
|
|
|
text.setTextAlign(left ? Paint.Align.RIGHT : Paint.Align.LEFT);
|
|
|
|
|
c.drawText(String.valueOf(Math.round(u)), left ? edge - len - 6f * dp : edge + len + 6f * dp, y + 6f * dp, text);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
c.restore();
|
|
|
|
|
// The readout box with its pointer.
|
|
|
|
|
boolean low = !left && v.socPercent != null && v.socPercent < 20f;
|
|
|
|
|
float bh = 44f * dp;
|
|
|
|
|
path.reset();
|
|
|
|
|
if (left) {
|
|
|
|
|
path.moveTo(l + 2f * dp, cy - bh / 2f);
|
|
|
|
|
path.lineTo(l + tw - 12f * dp, cy - bh / 2f);
|
|
|
|
|
path.lineTo(l + tw + 2f * dp, cy);
|
|
|
|
|
path.lineTo(l + tw - 12f * dp, cy + bh / 2f);
|
|
|
|
|
path.lineTo(l + 2f * dp, cy + bh / 2f);
|
|
|
|
|
} else {
|
|
|
|
|
path.moveTo(l + tw - 2f * dp, cy - bh / 2f);
|
|
|
|
|
path.lineTo(l + 12f * dp, cy - bh / 2f);
|
|
|
|
|
path.lineTo(l - 2f * dp, cy);
|
|
|
|
|
path.lineTo(l + 12f * dp, cy + bh / 2f);
|
|
|
|
|
path.lineTo(l + tw - 2f * dp, cy + bh / 2f);
|
|
|
|
|
}
|
|
|
|
|
path.close();
|
|
|
|
|
fill.setColor(0xFF000000);
|
|
|
|
|
c.drawPath(path, fill);
|
|
|
|
|
line.setColor(low ? amber : white);
|
|
|
|
|
line.setStrokeWidth(2f * dp);
|
|
|
|
|
c.drawPath(path, line);
|
|
|
|
|
text.setTextAlign(Paint.Align.CENTER);
|
|
|
|
|
text.setTextSize(30f * dp);
|
|
|
|
|
text.setColor(low ? amber : green);
|
|
|
|
|
c.drawText(readout, l + tw / 2f + (left ? -4f * dp : 4f * dp), cy + 11f * dp, text);
|
|
|
|
|
text.setTextSize(14f * dp);
|
|
|
|
|
text.setColor(cyan);
|
|
|
|
|
c.drawText(title, l + tw / 2f, t - 8f * dp, text);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** The annunciators: gear, drive state and the clock, green on black in boxes. */
|
|
|
|
|
private void drawAnnunciators(Canvas c, float w, float top) {
|
|
|
|
|
String gear = Gear.label(v.gear);
|
|
|
|
|
String state = v.powerKw == null ? "—" : v.powerKw < -0.3f ? "REGEN" : v.powerKw > 0.3f ? "DRIVE PWR" : "IDLE";
|
|
|
|
|
String[] items = { "GEAR " + (gear.isEmpty() ? "—" : gear), state, v.clock.isEmpty() ? "—" : v.clock };
|
|
|
|
|
float bw = w * 0.16f, gap = w * 0.02f, x = (w - 3 * bw - 2 * gap) / 2f, bt = 4f * dp, bb = top - 26f * dp;
|
|
|
|
|
if (bb - bt < 18f * dp) bb = bt + 22f * dp;
|
|
|
|
|
text.setTextSize((bb - bt) * 0.6f);
|
|
|
|
|
text.setTextAlign(Paint.Align.CENTER);
|
|
|
|
|
for (String item : items) {
|
|
|
|
|
line.setColor(0xFF2A3038);
|
|
|
|
|
line.setStrokeWidth(1.5f * dp);
|
|
|
|
|
c.drawRect(x, bt, x + bw, bb, line);
|
|
|
|
|
text.setColor(green);
|
|
|
|
|
c.drawText(item, x + bw / 2f, bb - (bb - bt) * 0.25f, text);
|
|
|
|
|
x += bw + gap;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** Data boxes along the bottom, cyan labels over white values. */
|
|
|
|
|
private void drawBoxes(Canvas c, float w, float h, float top) {
|
|
|
|
|
String[][] boxes = {
|
|
|
|
|
{ "RANGE", fmt(v.rangeKm, "%.0f km", "—") },
|
|
|
|
|
{ "TRIP", fmt(v.tripKm, "%.1f km", "—") },
|
|
|
|
|
{ "USAGE 5 MIN", fmt(v.usage5Min, "%.1f", "—") },
|
|
|
|
|
{ "SINCE START", fmt(v.usageTrip, "%.1f", "—") },
|
|
|
|
|
{ "CLIMATE", fmt(v.climateCelsius, "%.0f °C", "—") },
|
|
|
|
|
{ "ODOMETER", fmt(v.odometerKm, "%,.0f", "—") },
|
|
|
|
|
};
|
|
|
|
|
float gap = 8f * dp, bt = top + 14f * dp, bb = h - 4f * dp;
|
|
|
|
|
float bw = (w - gap * (boxes.length - 1)) / boxes.length;
|
|
|
|
|
for (int i = 0; i < boxes.length; i++) {
|
|
|
|
|
float x = i * (bw + gap);
|
|
|
|
|
line.setColor(0xFF2A3038);
|
|
|
|
|
line.setStrokeWidth(1.5f * dp);
|
|
|
|
|
c.drawRect(x, bt, x + bw, bb, line);
|
|
|
|
|
text.setTextAlign(Paint.Align.CENTER);
|
|
|
|
|
text.setTextSize(12f * dp);
|
|
|
|
|
text.setColor(cyan);
|
|
|
|
|
c.drawText(boxes[i][0], x + bw / 2f, bt + 17f * dp, text);
|
|
|
|
|
text.setTextSize(Math.min(24f * dp, (bb - bt) * 0.42f));
|
|
|
|
|
text.setColor(white);
|
|
|
|
|
c.drawText(boxes[i][1], x + bw / 2f, bb - 10f * dp, text);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|