package com.evsuite.abrp; import android.content.Context; import android.graphics.Canvas; import android.graphics.Paint; import android.graphics.Path; import android.os.SystemClock; import android.view.View; /** * The map under "Grid with map": the roads round the car and a ground grid, straight from * above, heading up — drawn once into a hardware layer (a GPU texture) and then only moved and * turned, by the view's own translation and rotation, once per display frame. Nothing is * redrawn while driving until new tiles arrive or the car nears the layer's edge; then it is * drawn again round the car. {@link GridView} draws everything else on top. * * The layer is a square about twice the distance from the car to the farthest screen corner, * plus a margin, so it covers the screen at any angle; the car sits {@link #CAR_FROM_BOTTOM} * layout units above the bottom edge. */ final class MapLayerView extends View { /** Pixels (layout units) per metre, the car's height above the bottom edge, grid spacing. */ static final float MAP_SCALE = 1.2f, CAR_FROM_BOTTOM = 150f; private static final float GRID_M = 50f; /** How far the car may move from the layer's centre before it is drawn again, in layout units. */ private static final float MARGIN = 280f; private final MapRoads map; private final Paint line = new Paint(Paint.ANTI_ALIAS_FLAG); private final Path[] roads = new Path[RoadTile.CLASSES]; private final Path grid = new Path(); private int main = 0xFF18E6FF, light = 0xFFC8FCFF; /** The layer's centre (world tile units) and what it was drawn from. */ private double anchorX = Double.NaN, anchorY; private int drawnVersion = -1; private float drawnScale; /** The car as shown, eased towards where it is (world tile units), and the heading. */ private double shownX = Double.NaN, shownY; private float heading = Float.NaN; private boolean showing, ticking; /** The car's speed, from the readings; NaN when unread. */ private float speedMps = Float.NaN; MapLayerView(Context context, MapRoads map) { super(context); this.map = map; for (int i = 0; i < roads.length; i++) roads[i] = new Path(); line.setStyle(Paint.Style.STROKE); line.setStrokeJoin(Paint.Join.ROUND); line.setStrokeCap(Paint.Cap.ROUND); setLayerType(LAYER_TYPE_HARDWARE, null); setVisibility(INVISIBLE); setWillNotDraw(false); } void setColours(int main, int light) { this.main = main; this.light = light; invalidate(); } /** Whether the map is there to be seen (a fix and roads). */ boolean isShowing() { return showing; } /** Layout units, as GridView uses them, for this size. */ private float unit() { View parent = (View) getParent(); int w = parent == null ? getWidth() : parent.getWidth() - parent.getPaddingLeft() - parent.getPaddingRight(); int h = parent == null ? getHeight() : parent.getHeight() - parent.getPaddingTop() - parent.getPaddingBottom(); return Math.min(h / 640f, w / 1100f); } @Override protected void onMeasure(int widthSpec, int heightSpec) { int w = MeasureSpec.getSize(widthSpec), h = MeasureSpec.getSize(heightSpec); float u = Math.min(h / 640f, w / 1100f); int side = (int) (2 * (Math.hypot(w / 2f, h - CAR_FROM_BOTTOM * u) + MARGIN * u)); setMeasuredDimension(Math.max(side, 1), Math.max(side, 1)); } /** The car's speed in km/h, or null when unread; GridView passes it with every reading. */ void setSpeed(Float kmh) { speedMps = kmh == null ? Float.NaN : kmh / 3.6f; } /** Starts the frame loop if it is not running; GridView calls it with every reading. */ void kick() { if (!ticking && isAttachedToWindow()) { ticking = true; postOnAnimation(this::tick); } } /** One display frame: where the car is, which way, and the layer moved there. */ private void tick() { View pane = (View) getParent(); if (pane == null || !pane.isShown()) { ticking = false; return; } MapRoads.Fix fix = map.fix(); boolean ready = fix != null && !map.tiles().isEmpty() && getWidth() > 0; if (ready != showing) { showing = ready; setVisibility(ready ? VISIBLE : INVISIBLE); } if (!ready) { ticking = false; return; } float target = Float.isNaN(fix.bearing) ? (Float.isNaN(heading) ? 0f : heading) : fix.bearing; if (Float.isNaN(heading)) heading = target; else heading = (heading + (((target - heading) % 360f + 540f) % 360f - 180f) * 0.12f + 360f) % 360f; // Between fixes the car runs on with the speed (at most 2.5 s), and what is shown glides // there rather than jumping with each fix. double mpu = MapGeometry.metresPerUnit(fix.lat); float ran = Float.isNaN(fix.bearing) || Float.isNaN(speedMps) ? 0f : speedMps * Math.min(2.5f, (SystemClock.elapsedRealtime() - fix.elapsedMs) / 1000f); double run = Math.toRadians(Float.isNaN(fix.bearing) ? 0f : fix.bearing); double carX = MapGeometry.worldX(fix.lon) + ran * Math.sin(run) / mpu; double carY = MapGeometry.worldY(fix.lat) - ran * Math.cos(run) / mpu; if (Double.isNaN(shownX) || Math.hypot(carX - shownX, carY - shownY) * mpu > 200) { shownX = carX; shownY = carY; } else { shownX += (carX - shownX) * 0.15; shownY += (carY - shownY) * 0.15; } float u = unit(), px = MAP_SCALE * u; if (drawnVersion != map.version() || Double.isNaN(anchorX) || drawnScale != px || Math.hypot(shownX - anchorX, shownY - anchorY) * mpu * px > MARGIN * u) { build(shownX, shownY, mpu); drawnScale = px; invalidate(); // the only redraw of the layer } // The car's spot in the layer goes to its spot on screen, turned round it. float half = getWidth() / 2f; float inX = half + (float) ((shownX - anchorX) * mpu) * px; float inY = half + (float) ((shownY - anchorY) * mpu) * px; float carSx = pane.getPaddingLeft() + (pane.getWidth() - pane.getPaddingLeft() - pane.getPaddingRight()) / 2f; float carSy = pane.getHeight() - pane.getPaddingBottom() - CAR_FROM_BOTTOM * u; setPivotX(inX); setPivotY(inY); setRotation(-heading); setTranslationX(carSx - getLeft() - inX); setTranslationY(carSy - getTop() - inY); postOnAnimation(this::tick); } /** The roads and the grid in metres east and south of an anchor at the car. */ private void build(double atX, double atY, double mpu) { anchorX = atX; anchorY = atY; drawnVersion = map.version(); float m = (float) mpu; for (Path p : roads) p.reset(); float u = unit(); double reach = getWidth() / 2f / (MAP_SCALE * u); double range = reach / mpu; for (RoadTile tile : map.tiles()) { double ox = (double) tile.x * MapGeometry.EXTENT - atX, oy = (double) tile.y * MapGeometry.EXTENT - atY; if (ox > range || oy > range || ox + MapGeometry.EXTENT < -range || oy + MapGeometry.EXTENT < -range) { continue; } float fx = (float) ox, fy = (float) oy; for (int i = 0; i < tile.points.length; i++) { int[] b = tile.bounds[i]; if (fx + b[2] < -range || fx + b[0] > range || fy + b[3] < -range || fy + b[1] > range) continue; Path p = roads[tile.classes[i]]; int[] pts = tile.points[i]; p.moveTo((fx + pts[0]) * m, (fy + pts[1]) * m); for (int k = 2; k < pts.length; k += 2) p.lineTo((fx + pts[k]) * m, (fy + pts[k + 1]) * m); } } // Grid lines on whole multiples of GRID_M east and north, so they stay put on the ground. grid.reset(); float r = (float) reach; float offE = (float) ((atX * mpu) % GRID_M), offS = (float) ((atY * mpu) % GRID_M); int lines = (int) (r / GRID_M) + 1; for (int k = -lines; k <= lines; k++) { float x = k * GRID_M - offE, y = k * GRID_M - offS; grid.moveTo(x, -r); grid.lineTo(x, r); grid.moveTo(-r, y); grid.lineTo(r, y); } } @Override protected void onDraw(Canvas c) { if (Double.isNaN(anchorX) || drawnScale <= 0f) return; float u = unit(), px = drawnScale; c.save(); c.translate(getWidth() / 2f, getHeight() / 2f); c.scale(px, px); stroke(c, grid, AccentColor.withAlpha(main, 0x26), 1f * u / px); stroke(c, roads[3], AccentColor.withAlpha(main, 0xA0), 1.6f * u / px); stroke(c, roads[2], AccentColor.withAlpha(main, 0xD8), 2.2f * u / px); glow(c, roads[1], main, 2.6f * u / px); glow(c, roads[0], light, 3.4f * u / px); c.restore(); } private void stroke(Canvas c, Path p, int colour, float width) { line.setColor(colour); line.setStrokeWidth(width); c.drawPath(p, line); } /** As GridView's glow: two wide faint strokes under a sharp one. */ private void glow(Canvas c, Path p, int colour, float width) { stroke(c, p, AccentColor.withAlpha(colour, 0x18), width * 6f); stroke(c, p, AccentColor.withAlpha(colour, 0x40), width * 3f); stroke(c, p, colour, width); } }