diff --git a/SPECS.md b/SPECS.md index 5df513d..f984757 100644 --- a/SPECS.md +++ b/SPECS.md @@ -138,6 +138,14 @@ tyre pressures, GPS (lat, lon, elevation, heading) and the is_* flags. It does * confirmed car facts, app structure, UI conventions, build/test/ship, documents. It replaces upstream's EVABRPUploader text. The test emulator is now in the flake: `nix run .#car-emulator` (Android 9 AOSP at 1920×720, 160 dpi; the car build installs as the system user — checked). +- 2026-10-27 — App 2.40.0: **Grid with map straight from above, full height** (owner: 2.39.2 + "does not look right"; wanted the map over the whole height and fully top-down). The map is + the whole background in map mode: orthographic, heading up, 1.2 layout units per metre (about + 400 m ahead, 125 m behind, 450 m to each side on the car's screen), the car 150 units above + the bottom edge; ground grid every 50 m fixed to the ground; roads from 250 m ahead fainter; + the camera, the sky and the floor are gone there. The speed moves to a panel under the gears + (120 units), and the battery wall, the clock, the gears and the disc get dark panels. The + plain Grid is unchanged. Emulator with the Berlin tiles, both layouts. Not tried on the car. - 2026-10-27 — App 2.39.2 published (`car-2.39.2`). - 2026-10-27 — App 2.39.2: **Grid with map seen from higher up** (owner: the map looked like a fish-eye; wanted it more top-down). The map has its own pinhole camera now: 45° down, 300 m diff --git a/THEMES.md b/THEMES.md index 43e3bff..7778973 100644 --- a/THEMES.md +++ b/THEMES.md @@ -165,11 +165,11 @@ films' other side. ## Grid with map -The Grid, with the roads round the car from OpenStreetMap (through OpenFreeMap) laid on its floor as light lanes, seen -from a camera high above and behind the car, tilted down, so the streets keep their shape some -400 m ahead, and turned with the direction you drive: motorways glow brightest, then main -roads, then the smaller streets, the far ones fainter. A ground grid every 50 m turns and slides -with them. The car is the chevron on the +The Grid over a map: the roads round the car from OpenStreetMap (through OpenFreeMap) fill the +whole screen as light lanes, straight from above and turned with the direction you drive, about +400 m ahead and 450 m to each side: motorways glow brightest, then main roads, then the smaller +streets, the far ones fainter, over a ground grid every 50 m that turns and slides with them. +The speed, the battery wall, the power disc and the clock stand on dark panels over the map. The car is the chevron on the floor, and between GPS fixes it runs on with the speed. The roads come through the car's own server, which fetches only the map tiles you drive through and keeps them (see the README); the car also keeps the last few hundred tiles itself. Without GPS, the server or the roads, the floor says so and diff --git a/app/build.gradle b/app/build.gradle index 3d29ebd..d7a75cd 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -34,8 +34,8 @@ android { applicationId "com.stinnesbeck.nilsmg4" minSdk 27 targetSdk 36 - versionCode 144 - versionName "2.39.2" + versionCode 145 + versionName "2.40.0" // Nothing about the server ships in the APK: its URL and ingest token are entered in // the app's Server tab (ServerConfig), and the database is only reachable by the server. diff --git a/app/src/main/java/com/evsuite/abrp/GridView.java b/app/src/main/java/com/evsuite/abrp/GridView.java index 997bf5c..ba6f8e8 100644 --- a/app/src/main/java/com/evsuite/abrp/GridView.java +++ b/app/src/main/java/com/evsuite/abrp/GridView.java @@ -23,12 +23,12 @@ import android.os.SystemClock; * the trail; the floor stands still when the speed is unread. * * With a map ("Grid with map", {@link MapRoads}): the roads round the car from OpenStreetMap lie - * on the floor in perspective, heading up, the bigger ones glowing brighter, with the car - * as a light marker a little way up the floor; between GPS fixes the car's position runs on - * with the speed. The map has its own camera, higher and steeper than the plain - * floor's, so far roads shrink less; the floor starts higher, the speed is a little smaller, its - * grid is fixed to the ground (a line every 50 m) and turns with the roads, and the trail makes - * way for the map. Without a fix, a server or the roads, the plain floor with a line + * under everything, heading up, the bigger ones glowing brighter, with the car as a light + * marker low on the screen; between GPS fixes the car's position runs on + * with the speed. The map is then the whole background, straight from above (about + * 400 m ahead, 450 m to each side), over a ground grid (a line every 50 m) that turns and slides + * with the roads; the speed, the battery wall, the disc and the clock stand on dark panels over + * it, and the trail makes way for the map. Without a fix, a server or the roads, the plain floor with a line * saying why. * * Two palettes: the original cyan, or the chosen accent colour ({@link #setAccentPalette}). A @@ -42,13 +42,10 @@ final class GridView extends ClusterCanvasView { private static final int WALL = 20, DISC = 60; private static final float TILE_M = 4f; /** - * The map's camera: tilted 45° down from about 300 m up, the car on its axis, the top of the - * floor some 420 m ahead; nothing nearer to the lens than NEAR_M is drawn, nothing beyond - * MAP_RANGE_M looked at, and roads from FADE_M ahead are fainter. The ground grid under the - * roads has a line every GRID_M. + * The map, straight from above: pixels (in layout units) per metre, the car's height above + * the bottom edge, roads from FADE_M ahead fainter, a ground-grid line every GRID_M. */ - private static final float PITCH = (float) Math.toRadians(45), CAM_H = 300f, AHEAD_M = 420f, - NEAR_M = 40f, MAP_RANGE_M = 1300f, FADE_M = 250f, GRID_M = 50f; + private static final float MAP_SCALE = 1.2f, CAR_FROM_BOTTOM = 150f, FADE_M = 250f, GRID_M = 50f; private final Paint line = new Paint(Paint.ANTI_ALIAS_FLAG); private final Paint fill = new Paint(Paint.ANTI_ALIAS_FLAG); @@ -66,13 +63,9 @@ final class GridView extends ClusterCanvasView { /** Per road class, near and far (from {@link #FADE_M} ahead). */ private final Path[] roads = new Path[RoadTile.CLASSES * 2]; private float heading = Float.NaN; - /** The pen while a road is added: the last point, and whether it continues from there. */ - private float penX, penY; - private boolean penDown; - private Path penPath; private final Path gridPath = new Path(); - /** The camera of this frame: focal length and centre in pixels. */ - private float camF, camCx, camCy; + /** This frame's map: pixels per metre and where the car is on screen. */ + private float mapPx, carSx, carSy; private int main, light, dim, alert; /** The floor's position within one tile (0..1) and the disc's inner ring angle. */ @@ -115,16 +108,22 @@ final class GridView extends ClusterCanvasView { advance(); c.drawColor(0xFF000000); - // With a map the floor starts higher, to give the roads room. + // With a map, the map is the whole background, seen from straight above, and the rest + // stands on dark panels over it. boolean mapShown = map != null && map.fix() != null && !map.tiles().isEmpty(); - float horizon = h * (mapShown ? 0.5f : 0.55f); - drawSky(c, w, horizon); - if (mapShown) drawMap(c, w, h, horizon, u); - else drawFloor(c, w, h, horizon, u); + float horizon = h * 0.55f; + if (mapShown) { + drawMap(c, w, h, u); + } else { + drawSky(c, w, horizon); + drawFloor(c, w, h, horizon, u); + } if (map != null && !mapShown) drawMapState(c, w, h, horizon, u); + if (mapShown) drawPanels(c, w, horizon, u); drawGears(c, w, u); drawClock(c, w, u); - drawSpeed(c, w, horizon, mapShown ? 170f : 210f, u); + if (mapShown) drawMapSpeed(c, w, u); + else drawSpeed(c, w, horizon, u); drawWall(c, horizon, u); drawDisc(c, w, mapShown, u); if (!mapShown) drawTrail(c, w, h, u); @@ -181,8 +180,8 @@ final class GridView extends ClusterCanvasView { // ---------- Map ---------- - /** The roads round the car on the floor, seen by the map's camera, with a ground grid. */ - private void drawMap(Canvas c, float w, float h, float horizon, float u) { + /** The roads round the car from straight above, heading up, over a ground grid. */ + private void drawMap(Canvas c, float w, float h, float u) { MapRoads.Fix fix = map.fix(); java.util.Collection tiles = map.tiles(); if (fix == null) return; @@ -201,27 +200,24 @@ final class GridView extends ClusterCanvasView { double carX = MapGeometry.worldX(fix.lon) + ran * Math.sin(run) / mpu; double carY = MapGeometry.worldY(fix.lat) - ran * Math.cos(run) / mpu; - // The camera: the car where the chevron stands, the top of the floor AHEAD_M beyond it. - float carScreenY = h - 96f * u; - float z0 = CAM_H / (float) Math.tan(PITCH); - camF = (carScreenY - horizon) / (up(z0 + AHEAD_M) - up(z0)); - camCy = carScreenY + camF * up(z0); - camCx = w / 2f; - + mapPx = MAP_SCALE * u; + carSx = w / 2f; + carSy = h - CAR_FROM_BOTTOM * u; float sin = (float) Math.sin(Math.toRadians(heading)), cos = (float) Math.cos(Math.toRadians(heading)); float m = (float) mpu; - double range = MAP_RANGE_M / mpu; + // Everything on screen is within this many metres of the car. + float reach = (float) Math.hypot(w / 2f, carSy) / mapPx + GRID_M; + double range = reach / mpu; // The ground grid, fixed to the ground: it turns and slides with the roads. gridPath.reset(); - penPath = null; double carEast = carX * mpu, carNorth = -carY * mpu; float offE = (float) (carEast % GRID_M), offN = (float) (carNorth % GRID_M); - int lines = (int) (MAP_RANGE_M / GRID_M); + int lines = (int) (reach / GRID_M) + 1; for (int k = -lines; k <= lines; k++) { float e = k * GRID_M - offE, n = k * GRID_M - offN; - gridLine(e, -MAP_RANGE_M, e, MAP_RANGE_M, sin, cos); - gridLine(-MAP_RANGE_M, n, MAP_RANGE_M, n, sin, cos); + gridLine(e, -reach, e, reach, sin, cos); + gridLine(-reach, n, reach, n, sin, cos); } for (Path p : roads) p.reset(); @@ -236,102 +232,95 @@ final class GridView extends ClusterCanvasView { if (fx + b[2] < -range || fx + b[0] > range || fy + b[3] < -range || fy + b[1] > range) continue; int cls = tile.classes[i]; int[] pts = tile.points[i]; - penDown = false; float pa = 0f, pf = 0f; + Path prev = null; for (int k = 0; k < pts.length; k += 2) { float east = (fx + pts[k]) * m, north = -(fy + pts[k + 1]) * m; float fwd = east * sin + north * cos, side = east * cos - north * sin; - if (k > 0) segment(roads[cls * 2 + (Math.min(pf, fwd) > FADE_M ? 1 : 0)], pa, pf, side, fwd); + if (k > 0) { + Path p = roads[cls * 2 + (Math.min(pf, fwd) > FADE_M ? 1 : 0)]; + if (p != prev) p.moveTo(screenX(pa), screenY(pf)); + p.lineTo(screenX(side), screenY(fwd)); + prev = p; + } pa = side; pf = fwd; } } } - c.save(); - c.clipRect(0, horizon, w, h); line.setStrokeJoin(Paint.Join.ROUND); line.setStrokeCap(Paint.Cap.ROUND); - line.setColor(AccentColor.withAlpha(main, 0x2A)); + line.setColor(AccentColor.withAlpha(main, 0x26)); line.setStrokeWidth(1f * u); c.drawPath(gridPath, line); // Far roads thinner and fainter. for (int cls = RoadTile.CLASSES - 1; cls >= 0; cls--) { - line.setColor(AccentColor.withAlpha(cls == 0 ? light : main, cls >= 3 ? 0x50 : 0x80)); - line.setStrokeWidth((cls == 0 ? 2f : cls == 3 ? 1f : 1.4f) * u); + line.setColor(AccentColor.withAlpha(cls == 0 ? light : main, cls >= 3 ? 0x60 : 0x90)); + line.setStrokeWidth((cls == 0 ? 2.4f : cls == 3 ? 1.2f : 1.8f) * u); c.drawPath(roads[cls * 2 + 1], line); } - line.setColor(AccentColor.withAlpha(main, 0x90)); - line.setStrokeWidth(1.5f * u); + line.setColor(AccentColor.withAlpha(main, 0xA0)); + line.setStrokeWidth(1.8f * u); c.drawPath(roads[3 * 2], line); - line.setColor(AccentColor.withAlpha(main, 0xD0)); - line.setStrokeWidth(2.2f * u); + line.setColor(AccentColor.withAlpha(main, 0xD8)); + line.setStrokeWidth(2.4f * u); c.drawPath(roads[2 * 2], line); - glow(c, roads[2], main, 2.6f * u); - glow(c, roads[0], light, 3.4f * u); + glow(c, roads[2], main, 2.8f * u); + glow(c, roads[0], light, 3.6f * u); line.setStrokeJoin(Paint.Join.MITER); line.setStrokeCap(Paint.Cap.SQUARE); - c.restore(); - glowLine(c, 0, horizon, w, horizon, main, 1.5f * u); // The car: a light-cycle chevron pointing ahead. path.reset(); - path.moveTo(camCx, carScreenY - 16f * u); - path.lineTo(camCx + 11f * u, carScreenY + 10f * u); - path.lineTo(camCx, carScreenY + 4f * u); - path.lineTo(camCx - 11f * u, carScreenY + 10f * u); + path.moveTo(carSx, carSy - 18f * u); + path.lineTo(carSx + 12f * u, carSy + 11f * u); + path.lineTo(carSx, carSy + 5f * u); + path.lineTo(carSx - 12f * u, carSy + 11f * u); path.close(); fill.setColor(light); c.drawPath(path, fill); glow(c, path, light, 1.5f * u); } - /** The camera's height of a ground point z metres ahead of the spot below it, over its depth. */ - private static float up(float z) { - float cos = (float) Math.cos(PITCH), sin = (float) Math.sin(PITCH); - return (z * sin - CAM_H * cos) / (z * cos + CAM_H * sin); + private float screenX(float side) { + return carSx + side * mapPx; + } + + private float screenY(float ahead) { + return carSy - ahead * mapPx; } /** A grid line between two ground points given east and north of the car, in metres. */ private void gridLine(float e0, float n0, float e1, float n1, float sin, float cos) { - penDown = false; - segment(gridPath, e0 * cos - n0 * sin, e0 * sin + n0 * cos, e1 * cos - n1 * sin, e1 * sin + n1 * cos); + gridPath.moveTo(screenX(e0 * cos - n0 * sin), screenY(e0 * sin + n0 * cos)); + gridPath.lineTo(screenX(e1 * cos - n1 * sin), screenY(e1 * sin + n1 * cos)); } - /** - * One stretch of ground from (side, ahead) a0, f0 to a1, f1 in metres from the car, through - * the camera, cut where it comes nearer to the lens than NEAR_M. - */ - private void segment(Path p, float a0, float f0, float a1, float f1) { - float cos = (float) Math.cos(PITCH), sin = (float) Math.sin(PITCH), z0 = CAM_H / (float) Math.tan(PITCH); - float za = z0 + f0, zb = z0 + f1; - float d0 = za * cos + CAM_H * sin, d1 = zb * cos + CAM_H * sin; - if (d0 < NEAR_M && d1 < NEAR_M) { - penDown = false; - return; - } - boolean cut = false; - if (d0 < NEAR_M || d1 < NEAR_M) { - float t = (NEAR_M - d0) / (d1 - d0); - float a = a0 + (a1 - a0) * t, z = za + (zb - za) * t; - if (d0 < NEAR_M) { - a0 = a; - za = z; - d0 = NEAR_M; - cut = true; - } else { - a1 = a; - zb = z; - d1 = NEAR_M; - } - } - float x0 = camCx + camF * a0 / d0, y0 = camCy - camF * (za * sin - CAM_H * cos) / d0; - float x1 = camCx + camF * a1 / d1, y1 = camCy - camF * (zb * sin - CAM_H * cos) / d1; - if (!penDown || cut || p != penPath || x0 != penX || y0 != penY) p.moveTo(x0, y0); - p.lineTo(x1, y1); - penX = x1; - penY = y1; - penPath = p; - penDown = true; + /** Dark panels under the battery wall and the clock, so the map does not run through them. */ + private void drawPanels(Canvas c, float w, float horizon, float u) { + chamfer(16f * u, 78f * u, 360f * u, horizon + 86f * u, 14f * u); + fill.setColor(0xC8000000); + c.drawPath(path, fill); + chamfer(w - 210f * u, 12f * u, w - 12f * u, 82f * u, 10f * u); + c.drawPath(path, fill); + // Behind the gears as well. + chamfer(w / 2f - 150f * u, 12f * u, w / 2f + 150f * u, 72f * u, 10f * u); + c.drawPath(path, fill); + } + + /** The speed over the map: smaller, on a panel under the gears. */ + private void drawMapSpeed(Canvas c, float w, float u) { + chamfer(w / 2f - 130f * u, 80f * u, w / 2f + 130f * u, 232f * u, 14f * u); + fill.setColor(0xC8000000); + c.drawPath(path, fill); + line.setColor(dim); + line.setStrokeWidth(1.5f * u); + c.drawPath(path, line); + text.setTypeface(thin); + text.setTextSize(120f * u); + text.setTextAlign(Paint.Align.CENTER); + glowText(c, fmt(v.speedKmh, "%.0f", "—"), w / 2f, 194f * u, light, u); + caption(c, "KM/H", w / 2f, 220f * u, u, Paint.Align.CENTER); } /** Why the map is not there, on the floor. */ @@ -391,9 +380,9 @@ final class GridView extends ClusterCanvasView { // ---------- Speed ---------- - private void drawSpeed(Canvas c, float w, float horizon, float size, float u) { + private void drawSpeed(Canvas c, float w, float horizon, float u) { text.setTypeface(thin); - text.setTextSize(size * u); + text.setTextSize(210f * u); text.setTextAlign(Paint.Align.CENTER); glowText(c, fmt(v.speedKmh, "%.0f", "—"), w / 2f, horizon - 62f * u, light, u); caption(c, "KM/H", w / 2f, horizon - 22f * u, u, Paint.Align.CENTER); diff --git a/screenshots/themes/grid-map-panel.png b/screenshots/themes/grid-map-panel.png index 7975cbd..0eb9d3b 100644 Binary files a/screenshots/themes/grid-map-panel.png and b/screenshots/themes/grid-map-panel.png differ diff --git a/screenshots/themes/grid-map.png b/screenshots/themes/grid-map.png index f1f6faa..cdeebd7 100644 Binary files a/screenshots/themes/grid-map.png and b/screenshots/themes/grid-map.png differ