From 65dc1ba847e961510bff7d7ab23ae7a12b1befd2 Mon Sep 17 00:00:00 2001 From: Nils Stinnesbeck Date: Mon, 26 Oct 2026 23:34:21 +0100 Subject: [PATCH] App 2.39.0, server 2.7.0: Grid with map A new Overview style: the Grid over the roads round the car from OpenStreetMap, drawn in the floor's perspective, heading up, the bigger roads glowing brighter, the car as a chevron. Server: POST /v1/map/roads {z:14,x,y} fetches a tile's drivable roads from Overpass (OVERPASS_URL; one request at a time, at least a second apart), simplifies them to tile-local points in four classes, caches them for 30 days and never logs the tile. App: MapRoads keeps the 3x3 tiles round the car from its own cache or the server; GPS only while the style is shown; bearing from the GPS or from successive positions. Without GPS, server or roads the plain floor says why. OSM attribution on screen and in LICENSE.md. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01LhzAVaUyqcw5PkGjz4mWW6 --- AGENTS.md | 7 +- LICENSE.md | 6 + README.md | 9 +- SPECS.md | 24 ++ THEMES.md | 19 +- app/build.gradle | 4 +- .../main/java/com/evsuite/abrp/GridView.java | 174 ++++++++- .../java/com/evsuite/abrp/MainActivity.java | 61 ++- .../java/com/evsuite/abrp/MapGeometry.java | 63 +++ .../main/java/com/evsuite/abrp/MapRoads.java | 198 ++++++++++ .../java/com/evsuite/abrp/OverviewStyle.java | 4 +- .../main/java/com/evsuite/abrp/RoadTile.java | 76 ++++ .../java/com/evsuite/abrp/StylePalette.java | 3 +- app/src/main/res/values/strings.xml | 1 + .../com/evsuite/abrp/MapGeometryTest.java | 65 ++++ .../com/evsuite/abrp/OverviewStyleTest.java | 3 + k8s/server.yaml | 3 + screenshots/themes/grid-map-panel.png | Bin 0 -> 71214 bytes screenshots/themes/grid-map.png | Bin 0 -> 67579 bytes server/README.md | 16 + server/api.go | 4 + server/main.go | 20 +- server/maps.go | 358 ++++++++++++++++++ server/maps_test.go | 212 +++++++++++ 24 files changed, 1312 insertions(+), 18 deletions(-) create mode 100644 app/src/main/java/com/evsuite/abrp/MapGeometry.java create mode 100644 app/src/main/java/com/evsuite/abrp/MapRoads.java create mode 100644 app/src/main/java/com/evsuite/abrp/RoadTile.java create mode 100644 app/src/test/java/com/evsuite/abrp/MapGeometryTest.java create mode 100644 screenshots/themes/grid-map-panel.png create mode 100644 screenshots/themes/grid-map.png create mode 100644 server/maps.go create mode 100644 server/maps_test.go diff --git a/AGENTS.md b/AGENTS.md index 0074b11..b36a6bc 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -20,7 +20,8 @@ MG4 head unit (Android 9, AAOS, MT2712) server and to ABRP, and updates itself. - **server/** — Go 1.27 API at `https://car.k8s.stinnesbeck.com`: stores telemetry, consumption and charging in InfluxDB, pairs cars by QR code, carries settings typed on a phone, relays - app releases from the private forge. It does **not** talk to ABRP. See `server/README.md`. + app releases from the private forge, and serves the roads for the Grid's map from + OpenStreetMap (Overpass). It does **not** talk to ABRP. See `server/README.md`. - **k8s/** — plain Kubernetes manifests (InfluxDB, server, Grafana with three provisioned dashboards, Traefik routes). See `k8s/README.md`. - **EVHardware/** — vendored shared vehicle layer (PolyForm Noncommercial, see `LICENSE.md`). @@ -75,11 +76,11 @@ Repository: `ssh://git@git.stinnesbeck.com:2222/nils/mg4.git` (remote `origin`), | Tab | Contents | |---|---| -| Overview | value tiles (battery, range, range/usage/energy since start, 5-min averages of range, usage, power, speed), vehicle info (odometer, lease allowance and the odometer against it), tyre pressures; read every 0.2 s on a background thread. Styles (Settings → Overview style): Tiles, Gauges (`GaugeView` needles), Cockpit (`RingView` rings, `TraceView` power trace), Retro (`RetroView`, seven-segment VFD), Futuristic (`FlowView`, animated energy flow), Sci-fi (`SciFiView`, starship console), Cartoon (`CartoonView`), MG (`OctagonView`, octagonal dials after the classic MG dashboards), and on `ClusterCanvasView`: Minimal, Terminal, Nixie tubes, Chronograph, Grid (`GridView`, neon lines over a moving grid floor, power as a light-cycle trail from `ClusterValues.powerTrail`), Time circuits (`TimeCircuitsView`, the time machine's LED rows with `Segments14` letters and a flux capacitor), Quest (`QuestView`, a fantasy game interface), Visor (`VisorView`, a helmet display with a motion tracker of the power trail), Empire (`EmpireView`, a strategy game with an isometric world and ages by odometer, `EmpireAge`); the drawn styles share `ClusterValues`. A style shown in its own palette (Sci-fi, Cartoon, Nixie, Time circuits, Quest, Empire; Retro, Terminal, Grid and Visor with "Original") gives the whole app its colours (`StylePalette`, `res/values/style_palettes.xml`) instead of the accent colour. A button in the navigation's header hides the navigation for a full-width Overview (`PREF_OVERVIEW_FULLSCREEN`); a swipe in from the left edge (`EdgeSwipe`) brings it back; without the app's own full screen the car's sidebar takes that edge, so a tab at the left edge (`edge_tab`) is tapped or swiped instead. Pages change only by the navigation (the pager's own swiping is off) | +| Overview | value tiles (battery, range, range/usage/energy since start, 5-min averages of range, usage, power, speed), vehicle info (odometer, lease allowance and the odometer against it), tyre pressures; read every 0.2 s on a background thread. Styles (Settings → Overview style): Tiles, Gauges (`GaugeView` needles), Cockpit (`RingView` rings, `TraceView` power trace), Retro (`RetroView`, seven-segment VFD), Futuristic (`FlowView`, animated energy flow), Sci-fi (`SciFiView`, starship console), Cartoon (`CartoonView`), MG (`OctagonView`, octagonal dials after the classic MG dashboards), and on `ClusterCanvasView`: Minimal, Terminal, Nixie tubes, Chronograph, Grid (`GridView`, neon lines over a moving grid floor, power as a light-cycle trail from `ClusterValues.powerTrail`), Grid with map (the same `GridView` over the roads round the car: GPS while it is shown, tiles from the server's `/v1/map/roads` through `MapRoads`, cached in `files/map`; `RoadTile`, `MapGeometry`), Time circuits (`TimeCircuitsView`, the time machine's LED rows with `Segments14` letters and a flux capacitor), Quest (`QuestView`, a fantasy game interface), Visor (`VisorView`, a helmet display with a motion tracker of the power trail), Empire (`EmpireView`, a strategy game with an isometric world and ages by odometer, `EmpireAge`); the drawn styles share `ClusterValues`. A style shown in its own palette (Sci-fi, Cartoon, Nixie, Time circuits, Quest, Empire; Retro, Terminal, Grid and Visor with "Original") gives the whole app its colours (`StylePalette`, `res/values/style_palettes.xml`) instead of the accent colour. A button in the navigation's header hides the navigation for a full-width Overview (`PREF_OVERVIEW_FULLSCREEN`); a swipe in from the left edge (`EdgeSwipe`) brings it back; without the app's own full screen the car's sidebar takes that edge, so a tab at the left edge (`edge_tab`) is tapped or swiped instead. Pages change only by the navigation (the pager's own swiping is off) | | Consumption / Charging | period totals, daily log, charging sessions and curve | | Live data | on/off, ABRP API key and user token, upload frequency | | Server | URL, pairing by QR, ingest token, installation ID (read-only), config import | -| Settings | drive mode (Eco, Normal, Sport, Snow, Custom; the lit one is what the car reports; changes only in P at a standstill, `DriveModeWrite`; "Set at start" sets the last confirmed choice again when the service starts with the head unit), full screen, voice key, Overview style with the accent colour beside it (offered only for styles drawn in it, `OverviewStyle.usesAccent`; Retro, Terminal, Grid and Visor add "Original" for their own palette, `hasOriginal`), tyre unit, battery and charging price (one card on wide screens), version, update check, install from USB, About | +| Settings | drive mode (Eco, Normal, Sport, Snow, Custom; the lit one is what the car reports; changes only in P at a standstill, `DriveModeWrite`; "Set at start" sets the last confirmed choice again when the service starts with the head unit), full screen, voice key, Overview style with the accent colour beside it (offered only for styles drawn in it, `OverviewStyle.usesAccent`; Retro, Terminal, Grid (and Grid with map, sharing Grid's choice) and Visor add "Original" for their own palette, `hasOriginal`), tyre unit, battery and charging price (one card on wide screens), version, update check, install from USB, About | | Logs | signal recorder (`SignalRecorder`/`SignalRecording`: every property and vendor getter via `VendorSurfaceProbe.capture`, again and again for 3 minutes, plus steering-wheel key broadcasts; lists what changed, rarest first; saved to the app and a USB stick; how to find where the car publishes something), update log (every install step, with the platform's words), upload log | Key classes (`app/src/main/java/com/evsuite/abrp/`): `AbrpUploadService` (foreground service, diff --git a/LICENSE.md b/LICENSE.md index a4aa48e..13ca00b 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -18,6 +18,12 @@ Nils' MG4 is a fork of a chain of forks, all MIT: MIT requires the copyright notice to travel with any copy or substantial portion of the software. `LICENSE` names all of them; keep every line. +## Map data + +The roads of the "Grid with map" style are © OpenStreetMap contributors, under the +[Open Database Licence](https://opendatacommons.org/licenses/odbl/); the style shows that line +whenever it draws them. The server fetches them from an Overpass API (`server/maps.go`). + ## The EVHardware library `EVHardware/` (the shared vehicle layer from EVSuite) is source-available under the diff --git a/README.md b/README.md index d0acff3..26e1031 100644 --- a/README.md +++ b/README.md @@ -39,8 +39,8 @@ APIs. ## Screenshots -**Overview styles:** the Overview comes in seventeen looks, from an 80s fluorescent display to a -starship console, a watch face, a neon grid, a time machine, a fantasy game, a helmet display and a strategy game. [THEMES.md](THEMES.md) shows each one, with and without the +**Overview styles:** the Overview comes in eighteen looks, from an 80s fluorescent display to a +starship console, a watch face, a neon grid (also over a map of the roads round the car), a time machine, a fantasy game, a helmet display and a strategy game. [THEMES.md](THEMES.md) shows each one, with and without the navigation, and what inspired it.

@@ -81,14 +81,15 @@ car ──live telemetry───▶ car.k8s.stinnesbeck.com (Go) ``` The car sends live data to ABRP itself, with the ABRP credentials set on the Live data tab, so -ABRP and the server do not depend on each other. The server only stores. The car authenticates +ABRP and the server do not depend on each other. The server stores, relays the app's updates +from the forge and serves the roads for the Grid's map (from OpenStreetMap). The car authenticates with the server using a token of its own, obtained by pairing with a phone (below). ## The app | Tab | What it shows | |---|---| -| **Overview** | Battery, range, power and speed; range, usage and energy since start; 5-minute averages of range, usage, power and speed; vehicle info (odometer, lease allowance, climate setpoint) and tyre pressures. Read five times a second. Looks to choose from in Settings → Overview style: Tiles, Gauges, Cockpit, Retro, Futuristic, Sci-fi, Cartoon, MG, Minimal, Terminal, Nixie tubes, Chronograph, Grid, Time circuits, Quest, Visor and Empire ([THEMES.md](THEMES.md) shows them all). A style with its own colours (Sci-fi, Cartoon, Nixie tubes, Time circuits, Quest, Empire, and Retro, Terminal, Grid and Visor with "Original") colours the whole app to match, e.g. turquoise for Retro. The small button next to the MG4 logo in the navigation hides the navigation for a full-width Overview; swipe in from the left edge (a faint handle marks it) to bring it back; with Settings → Full screen off, the car's own sidebar sits at that edge, so tap the small tab at the left edge (›) instead. Pages change only by the navigation, not by swiping sideways | +| **Overview** | Battery, range, power and speed; range, usage and energy since start; 5-minute averages of range, usage, power and speed; vehicle info (odometer, lease allowance, climate setpoint) and tyre pressures. Read five times a second. Looks to choose from in Settings → Overview style: Tiles, Gauges, Cockpit, Retro, Futuristic, Sci-fi, Cartoon, MG, Minimal, Terminal, Nixie tubes, Chronograph, Grid, Grid with map, Time circuits, Quest, Visor and Empire ([THEMES.md](THEMES.md) shows them all). A style with its own colours (Sci-fi, Cartoon, Nixie tubes, Time circuits, Quest, Empire, and Retro, Terminal, Grid and Visor with "Original"). Grid with map lays the roads round the car on the Grid's floor: the app asks the car's server for the map tile it is in (not its position), the server fetches the roads from OpenStreetMap's Overpass API and keeps them, and the GPS runs for it only while that style is on screen colours the whole app to match, e.g. turquoise for Retro. The small button next to the MG4 logo in the navigation hides the navigation for a full-width Overview; swipe in from the left edge (a faint handle marks it) to bring it back; with Settings → Full screen off, the car's own sidebar sits at that edge, so tap the small tab at the left edge (›) instead. Pages change only by the navigation, not by swiping sideways | | **Consumption** | Lifetime, last 7 and 30 days, Trip A and B (distance, energy, average consumption and speed, SOC change), and a daily log of the last four months | | **Charging** | The current session (SOC, voltage, current, power, energy into the battery and from the grid, time, cost) and its SOC/power curve | | **Live data** | Live data on/off, ABRP credentials for sending directly from the car, upload frequency | diff --git a/SPECS.md b/SPECS.md index 3ee15f4..03cad87 100644 --- a/SPECS.md +++ b/SPECS.md @@ -138,6 +138,30 @@ 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-26 — App 2.39.0 and server 2.7.0: **Grid with map**, a new Overview style — the Grid + over the roads round the car from OpenStreetMap (owner's choice of design: roads through the + car's own server, drawn as the floor). Server: `POST /v1/map/roads` `{z:14, x, y}` (the tile, + not the position, and in the body so it stays out of access logs) fetches the tile's drivable + roads from Overpass (`OVERPASS_URL`, default the public instance, `off` disables; https only; + one request at a time, ≥ 1 s apart, identifying User-Agent), turns them into tile-local points + in 4 classes simplified with Douglas–Peucker (~1.5 m), caches them in memory (30 days, 3000 + tiles; failures not cached), gzips when accepted, answers 503 when Overpass fails and never logs + the tile. Tests with a fake Overpass (classes, split at missing points, simplification, cache, + gzip, validation, 503 without caching, no tile in the log, config). App: `MapRoads` keeps the + 3×3 tiles round the car (files/map, 30 days, 400 tiles; one at a time on a background thread; + a failed tile waits a minute), `RoadTile` parses them failing closed, `MapGeometry` does the + web-mercator arithmetic (both tested). GPS only while that style is shown and the app is in + front; the bearing from the GPS, or worked out from positions 8 m apart when it gives none. + `GridView` with a map draws the roads in the floor's perspective, heading up, 40 m floor steps, + up to 900 m ahead, far roads fainter, the car as a chevron, its position run on with the speed + between fixes, 20 fps while moving; the trail gives way to the map; "© OpenStreetMap + contributors" shown; otherwise the plain floor saying why (waiting for GPS, set up the server, + loading, not available). `OVERPASS_URL` in `k8s/server.yaml`; LICENSE.md notes the ODbL. Tested: + server unit tests; one real fetch of 3×3 tiles in central Berlin through the server code + (20–30 KB each; Overpass answered 502 a few times and the retry got them); on the emulator those + tiles from the app's cache with emulator GPS, north up and turning with a simulated drive, both + layouts, no GPS, no server; no crash. Not tried: the app fetching tiles from the deployed + server (not deployed yet) and anything on the car. - 2026-10-26 — App 2.38.0 published (`car-2.38.0`) and installed on the car through the in-app updater, from 2.35.3 (so Grid, Time circuits, Quest, Visor and Empire are on the car). - 2026-10-26 — App 2.38.0: Overview styles **Visor** (after Halo's helmet display, `VisorView`) diff --git a/THEMES.md b/THEMES.md index ec5ee50..bab6f29 100644 --- a/THEMES.md +++ b/THEMES.md @@ -1,6 +1,6 @@ # Overview styles -The Overview can look seventeen ways: pick one under **Settings → Overview style**. Every style shows +The Overview can look eighteen ways: pick one under **Settings → Overview style**. Every style shows the same readings (battery, range, power, speed, gear, usage, trip, climate, odometer, time) and shows "—" for anything the car did not answer, never a made-up zero. @@ -10,7 +10,7 @@ emulator at the car's resolution with **sample values** (87 km/h in D, 76 %, 18. emulator has no car; on the car the values are live. **Colours.** Most styles are drawn in the accent colour you pick from the MG4 paints (Settings → -Accent colour; shown here in Dynamic Red). Retro, Terminal, Grid and Visor can also show their original +Accent colour; shown here in Dynamic Red). Retro, Terminal, Grid (with or without the map) and Visor can also show their original colours, and Sci-fi, Cartoon, Nixie tubes, Time circuits, Quest and Empire always do; while one of those is shown, the whole app (navigation, buttons, switches) takes on its colours too. @@ -29,6 +29,7 @@ colours, and Sci-fi, Cartoon, Nixie tubes, Time circuits, Quest and Empire alway | [Nixie tubes](#nixie-tubes) | glowing glass tubes | own | | [Chronograph](#chronograph) | a watch face | accent | | [Grid](#grid) | neon lines on a glowing grid floor | original or accent | +| [Grid with map](#grid-with-map) | the Grid, with the real roads round the car on its floor | original or accent | | [Time circuits](#time-circuits) | a time machine's LED displays | own | | [Quest](#quest) | a fantasy game's interface | own | | [Visor](#visor) | a soldier's helmet display | original or accent | @@ -162,6 +163,20 @@ films' other side. ![Grid with the navigation](screenshots/themes/grid-panel.png) ![Grid, full screen](screenshots/themes/grid.png) +## Grid with map + +The Grid, with the roads round the car from OpenStreetMap laid on its floor as light lanes, in +the floor's perspective and turned with the direction you drive: motorways glow brightest, then +main roads, then the smaller streets, fading towards the horizon. 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 them from OpenStreetMap 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 +stays the plain grid. The light-cycle trail makes way for the map here; power stays on the disc. +Shown with sample values over real roads in central Berlin. + +![Grid with map, with the navigation](screenshots/themes/grid-map-panel.png) +![Grid with map, full screen](screenshots/themes/grid-map.png) + ## Time circuits After the time machine in Back to the Future: three rows of LED read-outs in a metal panel, diff --git a/app/build.gradle b/app/build.gradle index 6196223..cec6e50 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -34,8 +34,8 @@ android { applicationId "com.stinnesbeck.nilsmg4" minSdk 27 targetSdk 36 - versionCode 141 - versionName "2.38.0" + versionCode 142 + versionName "2.39.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 68af5dc..0873c88 100644 --- a/app/src/main/java/com/evsuite/abrp/GridView.java +++ b/app/src/main/java/com/evsuite/abrp/GridView.java @@ -22,6 +22,13 @@ import android.os.SystemClock; * row of read-outs along the bottom. Unread values show "—", light nothing and leave gaps in * 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 its 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 floor's tiles are then 40 m, so they move 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 + * saying why. + * * Two palettes: the original cyan, or the chosen accent colour ({@link #setAccentPalette}). A * low battery is orange in the original (the films' other side) and white in the accent one, * so it never looks like the accent itself. The floor and the disc animate (about 30 frames a @@ -32,6 +39,11 @@ final class GridView extends ClusterCanvasView { /** Battery segments in the light wall, disc segments, metres per grid tile. */ private static final int WALL = 20, DISC = 60; private static final float TILE_M = 4f; + /** + * The map: metres per depth step of the floor, the car's depth (just above the read-outs), + * the nearest depth drawn, how far ahead, and the depth from which roads are drawn fainter. + */ + private static final float MAP_UNIT_M = 40f, CAR_DEPTH = 1.6f, NEAR = 0.6f, FAR_M = 900f, FADE_DEPTH = 5f; private final Paint line = new Paint(Paint.ANTI_ALIAS_FLAG); private final Paint fill = new Paint(Paint.ANTI_ALIAS_FLAG); @@ -44,6 +56,15 @@ final class GridView extends ClusterCanvasView { private final Typeface thin = Typeface.create("sans-serif-thin", Typeface.NORMAL); private final Typeface label = Typeface.create("sans-serif-condensed", Typeface.BOLD); private final int accent, accentLight; + /** Null for the plain Grid. */ + private final MapRoads map; + /** Per road class, near and far (from {@link #FADE_DEPTH}). */ + 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 int main, light, dim, alert; /** The floor's position within one tile (0..1) and the disc's inner ring angle. */ @@ -52,7 +73,13 @@ final class GridView extends ClusterCanvasView { private int skyFor = -1; GridView(Context context) { + this(context, null); + } + + GridView(Context context, MapRoads map) { super(context); + this.map = map; + for (int i = 0; i < roads.length; i++) roads[i] = new Path(); accent = AccentColor.resolve(context, R.attr.evAccent); accentLight = AccentColor.resolve(context, R.attr.evAccentLight); line.setStyle(Paint.Style.STROKE); @@ -83,16 +110,20 @@ final class GridView extends ClusterCanvasView { float horizon = h * 0.55f; drawSky(c, w, horizon); drawFloor(c, w, h, horizon, u); + boolean mapShown = map != null && drawMap(c, w, h, horizon, u); + if (map != null && !mapShown) drawMapState(c, w, h, horizon, u); drawGears(c, w, u); drawClock(c, w, u); drawSpeed(c, w, horizon, u); drawWall(c, horizon, u); drawDisc(c, w, u); - drawTrail(c, w, h, u); + if (!mapShown) drawTrail(c, w, h, u); drawReadouts(c, w, h, u); + if (mapShown) drawAttribution(c, w, h, u); boolean moving = v.speedKmh != null && v.speedKmh > 0.5f; - keepAnimating(moving, 33); + // The map costs more per frame: 20 a second is smooth enough for it. + keepAnimating(moving, mapShown ? 50 : 33); } /** Moves the floor by the distance driven since the last frame, and turns the disc. */ @@ -101,7 +132,8 @@ final class GridView extends ClusterCanvasView { float dt = lastFrame == 0 ? 0f : Math.min(0.2f, (now - lastFrame) / 1000f); lastFrame = now; if (v.speedKmh != null && v.speedKmh > 0.5f) { - floorPhase = (floorPhase + v.speedKmh / 3.6f * dt / TILE_M) % 1f; + float tile = map != null ? MAP_UNIT_M : TILE_M; + floorPhase = (floorPhase + v.speedKmh / 3.6f * dt / tile) % 1f; } if (v.powerKw != null) discTurn = (discTurn + v.powerKw * 3f * dt) % 360f; } @@ -138,6 +170,142 @@ final class GridView extends ClusterCanvasView { glowLine(c, 0, horizon, w, horizon, main, 1.5f * u); } + // ---------- Map ---------- + + /** The roads round the car on the floor; false when there is nothing to draw. */ + private boolean drawMap(Canvas c, float w, float h, float horizon, float u) { + MapRoads.Fix fix = map.fix(); + java.util.Collection tiles = map.tiles(); + if (fix == null || tiles.isEmpty()) return false; + // Heading up: ease round the shorter way; north up until the car has moved. + 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.2f + 360f) % 360f; + + // Between fixes the car runs on with the speed, for at most 2.5 s. + double mpu = MapGeometry.metresPerUnit(fix.lat); + float ran = 0f; + if (v.speedKmh != null && !Float.isNaN(fix.bearing)) { + ran = v.speedKmh / 3.6f * 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; + + float sin = (float) Math.sin(Math.toRadians(heading)), cos = (float) Math.cos(Math.toRadians(heading)); + float lane = w * 0.09f, depth = h - horizon, vx = w / 2f, m = (float) mpu; + double range = FAR_M / mpu; + for (Path p : roads) p.reset(); + for (RoadTile tile : tiles) { + double ox = (double) tile.x * MapGeometry.EXTENT - carX, oy = (double) tile.y * MapGeometry.EXTENT - carY; + if (ox > range || oy > range || ox + 2 * MapGeometry.EXTENT < -range || oy + 2 * 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; + int cls = tile.classes[i]; + int[] pts = tile.points[i]; + penDown = false; + float pa = 0f, pf = 0f; + 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(cls, pa, pf, side, fwd, vx, horizon, lane, depth); + pa = side; + pf = fwd; + } + } + } + c.save(); + c.clipRect(0, horizon, w, h); + line.setStrokeJoin(Paint.Join.ROUND); + line.setStrokeCap(Paint.Cap.ROUND); + // Far roads thin and faint, so the horizon stays a line rather than a tangle. + for (int cls = RoadTile.CLASSES - 1; cls >= 0; cls--) { + line.setColor(AccentColor.withAlpha(cls == 0 ? light : main, cls >= 3 ? 0x30 : 0x50)); + line.setStrokeWidth((cls == 0 ? 1.6f : 1f) * u); + c.drawPath(roads[cls * 2 + 1], line); + } + line.setColor(AccentColor.withAlpha(main, 0x80)); + line.setStrokeWidth(1.4f * u); + c.drawPath(roads[3 * 2], line); + line.setColor(AccentColor.withAlpha(main, 0xC0)); + line.setStrokeWidth(2f * u); + c.drawPath(roads[2 * 2], line); + glow(c, roads[2], main, 2.4f * u); + glow(c, roads[0], light, 3.2f * u); + line.setStrokeJoin(Paint.Join.MITER); + line.setStrokeCap(Paint.Cap.SQUARE); + c.restore(); + + // The car: a light-cycle chevron pointing ahead. + float cy = horizon + depth / CAR_DEPTH; + path.reset(); + path.moveTo(vx, cy - 16f * u); + path.lineTo(vx + 11f * u, cy + 10f * u); + path.lineTo(vx, cy + 4f * u); + path.lineTo(vx - 11f * u, cy + 10f * u); + path.close(); + fill.setColor(light); + c.drawPath(path, fill); + glow(c, path, light, 1.5f * u); + return true; + } + + /** One stretch of road from (a0, f0) to (a1, f1) in metres (side, ahead), cut at the near edge. */ + private void segment(int cls, float a0, float f0, float a1, float f1, float vx, float horizon, float lane, + float depth) { + float d0 = CAR_DEPTH + f0 / MAP_UNIT_M, d1 = CAR_DEPTH + f1 / MAP_UNIT_M; + Path p = roads[cls * 2 + (Math.min(d0, d1) > FADE_DEPTH ? 1 : 0)]; + if ((d0 < NEAR && d1 < NEAR) || (f0 > FAR_M && f1 > FAR_M)) { + penDown = false; + return; + } + boolean cut = false; + if (d0 < NEAR || d1 < NEAR) { + float t = (NEAR - d0) / (d1 - d0); + float a = a0 + (a1 - a0) * t, d = NEAR; + if (d0 < NEAR) { + a0 = a; + d0 = d; + cut = true; + } else { + a1 = a; + d1 = d; + } + } + float x0 = vx + a0 / MAP_UNIT_M * lane / d0, y0 = horizon + depth / d0; + float x1 = vx + a1 / MAP_UNIT_M * lane / d1, y1 = horizon + depth / d1; + if (!penDown || cut || p != penPath || x0 != penX || y0 != penY) p.moveTo(x0, y0); + p.lineTo(x1, y1); + penPath = p; + penX = x1; + penY = y1; + penDown = true; + } + + /** Why the map is not there, on the floor. */ + private void drawMapState(Canvas c, float w, float h, float horizon, float u) { + MapRoads.State state = map.state(); + String why = state == MapRoads.State.NO_SERVER ? "MAP · SET UP THE SERVER FIRST" + : state == MapRoads.State.WAITING_FOR_GPS ? "MAP · WAITING FOR GPS" + : state == MapRoads.State.UNAVAILABLE ? "MAP · NOT AVAILABLE, TRYING AGAIN" + : "MAP · LOADING"; + caption(c, why, w / 2f, horizon + (h - horizon) * 0.2f, u, Paint.Align.CENTER); + } + + /** OpenStreetMap's licence asks for its name wherever its data is shown. */ + private void drawAttribution(Canvas c, float w, float h, float u) { + text.setTypeface(label); + text.setTextSize(12f * u); + text.setTextAlign(Paint.Align.RIGHT); + text.setStyle(Paint.Style.FILL); + text.setColor(dim); + c.drawText("© OpenStreetMap contributors", w - 26f * u, h - 80f * u, text); + } + // ---------- Top ---------- private void drawGears(Canvas c, float w, float u) { diff --git a/app/src/main/java/com/evsuite/abrp/MainActivity.java b/app/src/main/java/com/evsuite/abrp/MainActivity.java index 21030eb..4274600 100644 --- a/app/src/main/java/com/evsuite/abrp/MainActivity.java +++ b/app/src/main/java/com/evsuite/abrp/MainActivity.java @@ -70,6 +70,11 @@ public class MainActivity extends AppCompatActivity { private TerminalView terminalView; private GridView gridView; private VisorView visorView; + /** "Grid with map": the same style over the roads round the car (MapRoads, fed by GPS here). */ + private GridView gridMapView; + private MapRoads mapRoads; + private android.location.LocationListener mapListener; + private boolean inFront; /** The pane of each Overview style, by its {@link OverviewStyle} ordinal. */ private View[] stylePanes; private View consumptionPane; @@ -345,9 +350,11 @@ public class MainActivity extends AppCompatActivity { mgPane = drawnPane(octagonView); terminalView = new TerminalView(this); gridView = new GridView(this); + mapRoads = new MapRoads(this); + gridMapView = new GridView(this, mapRoads); visorView = new VisorView(this); java.util.Collections.addAll(canvasStyles, new MinimalView(this), terminalView, - new NixieView(this), new ChronographView(this), gridView, new TimeCircuitsView(this), + new NixieView(this), new ChronographView(this), gridView, gridMapView, new TimeCircuitsView(this), new QuestView(this), visorView, new EmpireView(this)); View[] canvasPanes = new View[canvasStyles.size()]; for (int i = 0; i < canvasPanes.length; i++) canvasPanes[i] = drawnPane(canvasStyles.get(i)); @@ -487,6 +494,8 @@ public class MainActivity extends AppCompatActivity { // Poll state and log only while the screen is up; onPause cancels it. uiHandler.post(uiRefresh); uiHandler.post(overviewRefresh); + inFront = true; + updateMapLocation(); } @Override @@ -534,6 +543,8 @@ public class MainActivity extends AppCompatActivity { // Stop polling off-screen: this activity has no reason to spend cycles then. uiHandler.removeCallbacks(uiRefresh); uiHandler.removeCallbacks(overviewRefresh); + inFront = false; + updateMapLocation(); } @Override @@ -1725,9 +1736,56 @@ public class MainActivity extends AppCompatActivity { retroView.setAccentPalette(!originalColours(OverviewStyle.RETRO)); terminalView.setAccentPalette(!originalColours(OverviewStyle.TERMINAL)); gridView.setAccentPalette(!originalColours(OverviewStyle.GRID)); + gridMapView.setAccentPalette(!originalColours(OverviewStyle.GRID_MAP)); visorView.setAccentPalette(!originalColours(OverviewStyle.VISOR)); // The accent choices depend on the style ("Original colours" for Retro and Terminal). if (settingsPane != null) bindAccentColor(); + updateMapLocation(); + } + + /** + * GPS for the Grid's map, only while "Grid with map" is the style and the app is in front; + * every fix goes to {@link MapRoads}. Without the location permission there is no map (the + * floor says it waits for GPS), never a crash. + */ + private void updateMapLocation() { + android.location.LocationManager lm = + (android.location.LocationManager) getSystemService(LOCATION_SERVICE); + if (lm == null || mapRoads == null) return; + boolean wanted = inFront && overviewStyle() == OverviewStyle.GRID_MAP; + if (!wanted) { + if (mapListener != null) { + try { + lm.removeUpdates(mapListener); + } catch (RuntimeException ignored) { + // already gone + } + mapListener = null; + } + return; + } + if (mapListener != null) return; + if (checkSelfPermission(android.Manifest.permission.ACCESS_FINE_LOCATION) + != android.content.pm.PackageManager.PERMISSION_GRANTED) { + return; + } + android.location.LocationListener listener = new android.location.LocationListener() { + @Override public void onLocationChanged(android.location.Location location) { mapRoads.onFix(location); } + @Override public void onStatusChanged(String provider, int status, android.os.Bundle extras) { } + @Override public void onProviderEnabled(String provider) { } + @Override public void onProviderDisabled(String provider) { } + }; + try { + lm.requestLocationUpdates(android.location.LocationManager.GPS_PROVIDER, 1000L, 0f, listener); + mapListener = listener; + android.location.Location last = lm.getLastKnownLocation(android.location.LocationManager.GPS_PROVIDER); + if (last != null && android.os.SystemClock.elapsedRealtimeNanos() - last.getElapsedRealtimeNanos() + < MapRoads.FIX_FRESH_MS * 1_000_000L) { + mapRoads.onFix(last); + } + } catch (RuntimeException e) { + android.util.Log.w("MainActivity", "No GPS for the map: " + e.getClass().getSimpleName()); + } } /** @@ -1757,6 +1815,7 @@ public class MainActivity extends AppCompatActivity { getString(R.string.overview_style_minimal), getString(R.string.overview_style_terminal), getString(R.string.overview_style_nixie), getString(R.string.overview_style_chronograph), getString(R.string.overview_style_grid), + getString(R.string.overview_style_grid_map), getString(R.string.overview_style_time_circuits), getString(R.string.overview_style_quest), getString(R.string.overview_style_visor), getString(R.string.overview_style_empire) }; } diff --git a/app/src/main/java/com/evsuite/abrp/MapGeometry.java b/app/src/main/java/com/evsuite/abrp/MapGeometry.java new file mode 100644 index 0000000..0bda5c1 --- /dev/null +++ b/app/src/main/java/com/evsuite/abrp/MapGeometry.java @@ -0,0 +1,63 @@ +package com.evsuite.abrp; + +/** + * Web-mercator arithmetic for the Grid's map: which tile the car is in, positions in the + * server's tile units (4096 across a zoom-14 tile) and metres per unit. Pure Java, unit-tested. + */ +final class MapGeometry { + + static final int ZOOM = 14; + static final int EXTENT = 4096; + /** Tile units across the world at {@link #ZOOM}. */ + static final double WORLD = (double) (1 << ZOOM) * EXTENT; + private static final double EQUATOR_M = 40_075_016.686; + + private MapGeometry() {} + + /** The position east, in tile units from the date line. */ + static double worldX(double lon) { + return (lon + 180d) / 360d * WORLD; + } + + /** The position south, in tile units from the top of the map. */ + static double worldY(double lat) { + double r = Math.toRadians(lat); + return (1d - Math.log(Math.tan(r) + 1d / Math.cos(r)) / Math.PI) / 2d * WORLD; + } + + static int tileX(double lon) { + return clampTile((int) Math.floor(worldX(lon) / EXTENT)); + } + + static int tileY(double lat) { + return clampTile((int) Math.floor(worldY(lat) / EXTENT)); + } + + private static int clampTile(int t) { + return Math.max(0, Math.min((1 << ZOOM) - 1, t)); + } + + /** Metres per tile unit on the ground at this latitude. */ + static double metresPerUnit(double lat) { + return EQUATOR_M * Math.cos(Math.toRadians(lat)) / WORLD; + } + + /** One number for a tile, for maps and file names. */ + static long key(int x, int y) { + return ((long) x << 20) | y; + } + + static int keyX(long key) { + return (int) (key >> 20); + } + + static int keyY(long key) { + return (int) (key & 0xFFFFF); + } + + /** Whether the position is one a map can show (web mercator stops short of the poles). */ + static boolean usable(double lat, double lon) { + return Double.isFinite(lat) && Double.isFinite(lon) && Math.abs(lat) < 85d && Math.abs(lon) <= 180d + && !(lat == 0d && lon == 0d); + } +} diff --git a/app/src/main/java/com/evsuite/abrp/MapRoads.java b/app/src/main/java/com/evsuite/abrp/MapRoads.java new file mode 100644 index 0000000..1973357 --- /dev/null +++ b/app/src/main/java/com/evsuite/abrp/MapRoads.java @@ -0,0 +1,198 @@ +package com.evsuite.abrp; + +import android.content.Context; +import android.location.Location; +import android.os.SystemClock; +import android.util.Log; + +import org.json.JSONObject; + +import java.io.File; +import java.io.FileOutputStream; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.util.Arrays; +import java.util.Collection; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +/** + * The roads around the car for the Grid's map, and where the car is: the activity hands in + * GPS fixes ({@link #onFix}) while "Grid with map" is shown; this keeps the 3×3 tiles round + * the car loaded from the app's own cache (files/map, 30 days, at most {@value #DISK_TILES} + * tiles) or from the car's server ({@code POST /v1/map/roads}, server/maps.go), one at a time + * on a background thread, and drops tiles from memory once the car is two tiles away. The car + * asks only its own server, naming a tile rather than its position. Without a configured + * server, a fix or the roads, the Grid shows its plain floor ({@link #state}). + */ +final class MapRoads { + + /** Why the map shows nothing (or {@link #SHOWING}). */ + enum State { NO_SERVER, WAITING_FOR_GPS, LOADING, UNAVAILABLE, SHOWING } + + /** One GPS fix: where, which way (degrees from north, NaN before the car has moved), when. */ + static final class Fix { + final double lat, lon; + final float bearing; + final long elapsedMs; + + Fix(double lat, double lon, float bearing, long elapsedMs) { + this.lat = lat; + this.lon = lon; + this.bearing = bearing; + this.elapsedMs = elapsedMs; + } + } + + private static final String TAG = "MapRoads"; + private static final int DISK_TILES = 400; + private static final long DISK_AGE_MS = 30L * 24 * 60 * 60 * 1000; + private static final long RETRY_MS = 60_000L; + /** A fix older than this is not drawn from. */ + static final long FIX_FRESH_MS = 15_000L; + + private final Context context; + private final File dir; + private final Map tiles = new ConcurrentHashMap<>(); + private final Map failedAt = new ConcurrentHashMap<>(); + private final Map queued = new ConcurrentHashMap<>(); + private final ExecutorService worker = Executors.newSingleThreadExecutor(r -> { + Thread t = new Thread(r, "map-roads"); + t.setPriority(Thread.MIN_PRIORITY); + return t; + }); + private volatile Fix fix; + /** The last bearing while moving; GPS has none at a standstill. */ + private volatile float bearing = Float.NaN; + private volatile boolean serverMissing; + /** Where the bearing was last worked out from, for a GPS that gives none. */ + private Location anchor; + + MapRoads(Context context) { + this.context = context.getApplicationContext(); + dir = new File(this.context.getFilesDir(), "map"); + } + + /** A GPS fix. Main thread; the loading happens on the worker. */ + void onFix(Location location) { + if (location == null || !MapGeometry.usable(location.getLatitude(), location.getLongitude())) return; + if (location.hasBearing() && location.hasSpeed() && location.getSpeed() > 1.5f) { + bearing = location.getBearing(); + anchor = location; + } else if (anchor == null || location.getElapsedRealtimeNanos() - anchor.getElapsedRealtimeNanos() > 10_000_000_000L) { + anchor = location; + } else if (anchor.distanceTo(location) >= 8f) { + // No bearing from the GPS: the way from the last place 8 m or more behind. + bearing = (anchor.bearingTo(location) + 360f) % 360f; + anchor = location; + } + Fix f = new Fix(location.getLatitude(), location.getLongitude(), bearing, SystemClock.elapsedRealtime()); + fix = f; + want(MapGeometry.tileX(f.lon), MapGeometry.tileY(f.lat)); + } + + /** The latest fix, or null when there is none or it is stale. */ + Fix fix() { + Fix f = fix; + return f == null || SystemClock.elapsedRealtime() - f.elapsedMs > FIX_FRESH_MS ? null : f; + } + + Collection tiles() { + return tiles.values(); + } + + State state() { + if (serverMissing) return State.NO_SERVER; + Fix f = fix(); + if (f == null) return State.WAITING_FOR_GPS; + long here = MapGeometry.key(MapGeometry.tileX(f.lon), MapGeometry.tileY(f.lat)); + if (tiles.containsKey(here)) return State.SHOWING; + return failedAt.containsKey(here) ? State.UNAVAILABLE : State.LOADING; + } + + /** Queues the 3×3 tiles round (cx, cy), the car's own first, and forgets far ones. */ + private void want(int cx, int cy) { + for (Long key : tiles.keySet()) { + if (Math.abs(MapGeometry.keyX(key) - cx) > 2 || Math.abs(MapGeometry.keyY(key) - cy) > 2) tiles.remove(key); + } + int[][] order = { {0, 0}, {0, -1}, {1, 0}, {0, 1}, {-1, 0}, {1, -1}, {1, 1}, {-1, 1}, {-1, -1} }; + long now = SystemClock.elapsedRealtime(); + for (int[] d : order) { + int x = cx + d[0], y = cy + d[1]; + if (x < 0 || y < 0 || x >= (1 << MapGeometry.ZOOM) || y >= (1 << MapGeometry.ZOOM)) continue; + long key = MapGeometry.key(x, y); + if (tiles.containsKey(key) || queued.containsKey(key)) continue; + Long failed = failedAt.get(key); + if (failed != null && now - failed < RETRY_MS) continue; + queued.put(key, Boolean.TRUE); + worker.execute(() -> { + try { + load(key, x, y); + } finally { + queued.remove(key); + } + }); + } + } + + /** From the cache when fresh enough, else from the server; a stale copy beats nothing. */ + private void load(long key, int x, int y) { + File file = new File(dir, MapGeometry.ZOOM + "_" + x + "_" + y + ".json"); + RoadTile cached = null; + try { + if (file.isFile()) { + cached = RoadTile.parse(new String(Files.readAllBytes(file.toPath()), StandardCharsets.UTF_8), x, y); + if (cached == null) file.delete(); + } + } catch (Exception e) { + Log.w(TAG, "Cached map tile unreadable: " + e.getClass().getSimpleName()); + } + if (cached != null) { + tiles.put(key, cached); + if (System.currentTimeMillis() - file.lastModified() < DISK_AGE_MS) return; + } + ServerConfig config = ServerConfig.get(context); + if (!config.isConfigured()) { + serverMissing = true; + return; + } + serverMissing = false; + try { + JSONObject body = new JSONObject().put("z", MapGeometry.ZOOM).put("x", x).put("y", y); + ServerHttp.Response r = ServerHttp.request("POST", config.url + "/v1/map/roads", config.token, body); + RoadTile tile = r.status == 200 ? RoadTile.parse(r.body, x, y) : null; + if (tile == null) { + // The tile stays out of the log: it says where the car is. + Log.w(TAG, "Map tile not loaded: HTTP " + r.status); + if (cached == null) failedAt.put(key, SystemClock.elapsedRealtime()); + return; + } + failedAt.remove(key); + tiles.put(key, tile); + save(file, r.body); + } catch (Exception e) { + Log.w(TAG, "Map tile not loaded: " + e.getClass().getSimpleName()); + if (cached == null) failedAt.put(key, SystemClock.elapsedRealtime()); + } + } + + private void save(File file, String body) { + try { + if (!dir.isDirectory() && !dir.mkdirs()) return; + File tmp = new File(dir, file.getName() + ".tmp"); + try (FileOutputStream out = new FileOutputStream(tmp)) { + out.write(body.getBytes(StandardCharsets.UTF_8)); + } + if (!tmp.renameTo(file)) tmp.delete(); + File[] all = dir.listFiles((d, name) -> name.endsWith(".json")); + if (all != null && all.length > DISK_TILES) { + Arrays.sort(all, (a, b) -> Long.compare(a.lastModified(), b.lastModified())); + for (int i = 0; i < all.length - DISK_TILES; i++) all[i].delete(); + } + } catch (Exception e) { + Log.w(TAG, "Map tile not cached: " + e.getClass().getSimpleName()); + } + } +} diff --git a/app/src/main/java/com/evsuite/abrp/OverviewStyle.java b/app/src/main/java/com/evsuite/abrp/OverviewStyle.java index 859696e..a4886a3 100644 --- a/app/src/main/java/com/evsuite/abrp/OverviewStyle.java +++ b/app/src/main/java/com/evsuite/abrp/OverviewStyle.java @@ -27,6 +27,7 @@ enum OverviewStyle { NIXIE(false, false), CHRONOGRAPH(true, false), GRID(true, true), + GRID_MAP(true, true), TIME_CIRCUITS(false, false), QUEST(false, false), VISOR(true, true), @@ -53,7 +54,8 @@ enum OverviewStyle { /** Where the choice between the original palette and the accent colour is kept. */ String originalKey() { - return "original_colours_" + name(); + // Grid with map is the Grid over a map: one choice for both. + return "original_colours_" + (this == GRID_MAP ? GRID : this).name(); } /** diff --git a/app/src/main/java/com/evsuite/abrp/RoadTile.java b/app/src/main/java/com/evsuite/abrp/RoadTile.java new file mode 100644 index 0000000..4daa634 --- /dev/null +++ b/app/src/main/java/com/evsuite/abrp/RoadTile.java @@ -0,0 +1,76 @@ +package com.evsuite.abrp; + +import org.json.JSONArray; +import org.json.JSONObject; + +/** + * The roads of one map tile as the server sends them (server/maps.go): per road its class + * (0 motorway and trunk, 1 primary and secondary, 2 tertiary and unclassified, 3 residential) + * and its points in tile units, plus each road's bounding box for skipping far ones quickly. + * {@link #parse} fails closed: anything unexpected is no tile at all. Pure Java, unit-tested. + */ +final class RoadTile { + + static final int CLASSES = 4; + /** Points may lie a little beyond the tile (a road is kept to its first point outside). */ + private static final int SLACK = MapGeometry.EXTENT; + private static final int MAX_ROADS = 20_000; + + final int x, y; + final int[] classes; + /** Per road: x0, y0, x1, y1, … in tile units. */ + final int[][] points; + /** Per road: minX, minY, maxX, maxY. */ + final int[][] bounds; + + private RoadTile(int x, int y, int[] classes, int[][] points, int[][] bounds) { + this.x = x; + this.y = y; + this.classes = classes; + this.points = points; + this.bounds = bounds; + } + + /** The tile in {@code json}, which must be the one asked for; null when it is not usable. */ + static RoadTile parse(String json, int expectX, int expectY) { + try { + JSONObject o = new JSONObject(json); + if (o.getInt("z") != MapGeometry.ZOOM || o.getInt("x") != expectX || o.getInt("y") != expectY + || o.getInt("extent") != MapGeometry.EXTENT) { + return null; + } + JSONArray ways = o.getJSONArray("ways"); + if (ways.length() > MAX_ROADS) return null; + int n = ways.length(); + int[] classes = new int[n]; + int[][] points = new int[n][]; + int[][] bounds = new int[n][]; + for (int i = 0; i < n; i++) { + JSONObject way = ways.getJSONObject(i); + int c = way.getInt("c"); + JSONArray p = way.getJSONArray("p"); + if (c < 0 || c >= CLASSES || p.length() < 4 || p.length() % 2 != 0) return null; + int[] pts = new int[p.length()]; + int minX = Integer.MAX_VALUE, minY = Integer.MAX_VALUE, maxX = Integer.MIN_VALUE, maxY = Integer.MIN_VALUE; + for (int k = 0; k < pts.length; k++) { + int value = p.getInt(k); + if (value < -SLACK || value > MapGeometry.EXTENT + SLACK) return null; + pts[k] = value; + if (k % 2 == 0) { + minX = Math.min(minX, value); + maxX = Math.max(maxX, value); + } else { + minY = Math.min(minY, value); + maxY = Math.max(maxY, value); + } + } + classes[i] = c; + points[i] = pts; + bounds[i] = new int[] { minX, minY, maxX, maxY }; + } + return new RoadTile(expectX, expectY, classes, points, bounds); + } catch (Exception e) { + return null; + } + } +} diff --git a/app/src/main/java/com/evsuite/abrp/StylePalette.java b/app/src/main/java/com/evsuite/abrp/StylePalette.java index d7d14bb..3dac82e 100644 --- a/app/src/main/java/com/evsuite/abrp/StylePalette.java +++ b/app/src/main/java/com/evsuite/abrp/StylePalette.java @@ -18,7 +18,8 @@ final class StylePalette { case SCIFI: return R.style.ThemeOverlay_EV_Style_SciFi; case CARTOON: return R.style.ThemeOverlay_EV_Style_Cartoon; case NIXIE: return R.style.ThemeOverlay_EV_Style_Nixie; - case GRID: return R.style.ThemeOverlay_EV_Style_Grid; + case GRID: + case GRID_MAP: return R.style.ThemeOverlay_EV_Style_Grid; case TIME_CIRCUITS: return R.style.ThemeOverlay_EV_Style_TimeCircuits; case QUEST: return R.style.ThemeOverlay_EV_Style_Quest; case VISOR: return R.style.ThemeOverlay_EV_Style_Visor; diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 1c5f899..32d9b8c 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -136,6 +136,7 @@ Terminal Original Grid + Grid with map Time circuits Quest Visor diff --git a/app/src/test/java/com/evsuite/abrp/MapGeometryTest.java b/app/src/test/java/com/evsuite/abrp/MapGeometryTest.java new file mode 100644 index 0000000..0d247cf --- /dev/null +++ b/app/src/test/java/com/evsuite/abrp/MapGeometryTest.java @@ -0,0 +1,65 @@ +package com.evsuite.abrp; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +import org.junit.Test; + +/** The Grid map's tile arithmetic and the server's road tiles — pure JVM. */ +public class MapGeometryTest { + + @Test + public void tilesMatchTheServer() { + // Alexanderplatz, Berlin: the tile the server tests use too. + assertEquals(8802, MapGeometry.tileX(13.4125)); + assertEquals(5373, MapGeometry.tileY(52.5219)); + long key = MapGeometry.key(8802, 5373); + assertEquals(8802, MapGeometry.keyX(key)); + assertEquals(5373, MapGeometry.keyY(key)); + } + + @Test + public void metresPerUnitShrinkTowardsThePoles() { + // A zoom-14 tile is about 2.4 km across at the equator, in 4096 units. + assertEquals(0.597, MapGeometry.metresPerUnit(0), 0.001); + assertEquals(0.363, MapGeometry.metresPerUnit(52.52), 0.001); + } + + @Test + public void unusablePositions() { + assertFalse(MapGeometry.usable(Double.NaN, 13)); + assertFalse(MapGeometry.usable(0, 0)); // a GPS that has nothing yet + assertFalse(MapGeometry.usable(89, 13)); + assertTrue(MapGeometry.usable(52.52, 13.41)); + } + + @Test + public void roadTilesParse() { + RoadTile t = RoadTile.parse("{\"z\":14,\"x\":8802,\"y\":5373,\"extent\":4096,\"ways\":[" + + "{\"c\":1,\"p\":[-115,1236,82,1080,100,1000]},{\"c\":3,\"p\":[0,0,10,10]}]}", 8802, 5373); + assertNotNull(t); + assertEquals(2, t.classes.length); + assertEquals(1, t.classes[0]); + assertEquals(6, t.points[0].length); + assertEquals(-115, t.bounds[0][0]); + assertEquals(1000, t.bounds[0][1]); + assertEquals(100, t.bounds[0][2]); + assertEquals(1236, t.bounds[0][3]); + } + + @Test + public void badRoadTilesAreNoTile() { + String head = "{\"z\":14,\"x\":1,\"y\":2,\"extent\":4096,\"ways\":"; + assertNotNull(RoadTile.parse(head + "[]}", 1, 2)); + assertNull("another tile", RoadTile.parse(head + "[]}", 1, 3)); + assertNull("another zoom", RoadTile.parse("{\"z\":13,\"x\":1,\"y\":2,\"extent\":4096,\"ways\":[]}", 1, 2)); + assertNull("unknown class", RoadTile.parse(head + "[{\"c\":7,\"p\":[0,0,1,1]}]}", 1, 2)); + assertNull("one point", RoadTile.parse(head + "[{\"c\":1,\"p\":[0,0]}]}", 1, 2)); + assertNull("odd count", RoadTile.parse(head + "[{\"c\":1,\"p\":[0,0,1]}]}", 1, 2)); + assertNull("far outside", RoadTile.parse(head + "[{\"c\":1,\"p\":[0,0,99999,1]}]}", 1, 2)); + assertNull("not JSON", RoadTile.parse("", 1, 2)); + } +} diff --git a/app/src/test/java/com/evsuite/abrp/OverviewStyleTest.java b/app/src/test/java/com/evsuite/abrp/OverviewStyleTest.java index 69e74ae..73fed72 100644 --- a/app/src/test/java/com/evsuite/abrp/OverviewStyleTest.java +++ b/app/src/test/java/com/evsuite/abrp/OverviewStyleTest.java @@ -63,5 +63,8 @@ public class OverviewStyleTest { assertEquals(R.style.ThemeOverlay_EV_Style_Retro, StylePalette.appOverlay(OverviewStyle.RETRO, true, AccentColor.HOLBORN_BLUE)); assertEquals("original_colours_RETRO", OverviewStyle.RETRO.originalKey()); + // Grid with map shares the Grid's choice. + assertEquals(OverviewStyle.GRID.originalKey(), OverviewStyle.GRID_MAP.originalKey()); + assertEquals(StylePalette.overlay(OverviewStyle.GRID), StylePalette.overlay(OverviewStyle.GRID_MAP)); } } diff --git a/k8s/server.yaml b/k8s/server.yaml index 72b2d94..3864ffd 100644 --- a/k8s/server.yaml +++ b/k8s/server.yaml @@ -50,6 +50,9 @@ spec: value: https://git.stinnesbeck.com - name: FORGE_REPO value: nils/mg4 + # The roads for the app's Grid map, from OpenStreetMap; "off" switches maps off. + - name: OVERPASS_URL + value: https://overpass-api.de/api/interpreter - name: INFLUX_TOKEN valueFrom: secretKeyRef: diff --git a/screenshots/themes/grid-map-panel.png b/screenshots/themes/grid-map-panel.png new file mode 100644 index 0000000000000000000000000000000000000000..e85c8676d1ef1acebfe18ef07365148dfd0163df GIT binary patch literal 71214 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 literal 0 HcmV?d00001 diff --git a/screenshots/themes/grid-map.png b/screenshots/themes/grid-map.png new file mode 100644 index 0000000000000000000000000000000000000000..b3bb0954672b99299635ca67ab438feaa31ff5fc GIT binary patch literal 67579 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 2 { + flat := make([]int, 0, len(pts)*2) + for _, p := range pts { + flat = append(flat, p[0], p[1]) + } + out.Ways = append(out.Ways, road{C: class, P: flat}) + } + run = nil + } + for _, g := range el.Geometry { + if g == nil { + flush() + continue + } + x, y := tilePoint(t, g.Lat, g.Lon) + if n := len(run); n > 0 && run[n-1] == [2]int{x, y} { + continue + } + run = append(run, [2]int{x, y}) + } + flush() + } + return json.Marshal(out) +} + +// simplify is Douglas–Peucker: the points that keep the line within tol of the original. +func simplify(pts [][2]int, tol float64) [][2]int { + if len(pts) < 3 { + return pts + } + keep := make([]bool, len(pts)) + keep[0], keep[len(pts)-1] = true, true + type span struct{ a, b int } + stack := []span{{0, len(pts) - 1}} + for len(stack) > 0 { + s := stack[len(stack)-1] + stack = stack[:len(stack)-1] + best, at := 0.0, -1 + for i := s.a + 1; i < s.b; i++ { + if d := segmentDistance(pts[i], pts[s.a], pts[s.b]); d > best { + best, at = d, i + } + } + if at >= 0 && best > tol { + keep[at] = true + stack = append(stack, span{s.a, at}, span{at, s.b}) + } + } + var out [][2]int + for i, k := range keep { + if k { + out = append(out, pts[i]) + } + } + return out +} + +func segmentDistance(p, a, b [2]int) float64 { + px, py := float64(p[0]), float64(p[1]) + ax, ay, bx, by := float64(a[0]), float64(a[1]), float64(b[0]), float64(b[1]) + dx, dy := bx-ax, by-ay + if dx == 0 && dy == 0 { + return math.Hypot(px-ax, py-ay) + } + u := math.Max(0, math.Min(1, ((px-ax)*dx+(py-ay)*dy)/(dx*dx+dy*dy))) + return math.Hypot(px-ax-u*dx, py-ay-u*dy) +} + +// Tile returns the tile's roads as JSON, from the cache or from Overpass, and whether it +// came from the cache. +func (m *Maps) Tile(ctx context.Context, t mapTile) ([]byte, bool, error) { + if body, ok := m.cached(t); ok { + return body, true, nil + } + m.fetchMu.Lock() + defer m.fetchMu.Unlock() + // Another request may have fetched it while this one waited. + if body, ok := m.cached(t); ok { + return body, true, nil + } + if wait := m.gap - time.Since(m.lastFetch); wait > 0 { + select { + case <-time.After(wait): + case <-ctx.Done(): + return nil, false, ctx.Err() + } + } + m.lastFetch = time.Now() + raw, err := m.fetch(ctx, t) + if err != nil { + return nil, false, err + } + body, err := buildTile(t, raw) + if err != nil { + return nil, false, err + } + m.store(t, body) + return body, false, nil +} + +func (m *Maps) cached(t mapTile) ([]byte, bool) { + m.mu.Lock() + defer m.mu.Unlock() + e, ok := m.tiles[t] + if !ok || time.Since(e.at) > mapCacheAge { + return nil, false + } + return e.body, true +} + +// store keeps the tile, dropping the oldest when the cache is full. +func (m *Maps) store(t mapTile, body []byte) { + m.mu.Lock() + defer m.mu.Unlock() + if m.tiles == nil { + m.tiles = map[mapTile]mapEntry{} + } + if len(m.tiles) >= mapCacheTiles { + var oldest mapTile + var at time.Time + for k, e := range m.tiles { + if at.IsZero() || e.at.Before(at) { + oldest, at = k, e.at + } + } + delete(m.tiles, oldest) + } + m.tiles[t] = mapEntry{body: body, at: time.Now()} +} + +// fetch asks Overpass. The query goes in the body, so its errors never carry the tile. +func (m *Maps) fetch(ctx context.Context, t mapTile) ([]byte, error) { + form := url.Values{"data": {overpassQuery(t)}}.Encode() + req, err := http.NewRequestWithContext(ctx, http.MethodPost, m.OverpassURL, strings.NewReader(form)) + if err != nil { + return nil, err + } + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + req.Header.Set("User-Agent", m.UserAgent) + resp, err := m.client().Do(req) + if err != nil { + return nil, errors.New("overpass could not be reached") + } + defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { + _, _ = io.Copy(io.Discard, io.LimitReader(resp.Body, 1<<16)) + return nil, fmt.Errorf("overpass answered %d", resp.StatusCode) + } + raw, err := io.ReadAll(io.LimitReader(resp.Body, maxOverpassBytes+1)) + if err != nil { + return nil, errors.New("overpass answer could not be read") + } + if len(raw) > maxOverpassBytes { + return nil, errors.New("overpass answer is too large") + } + return raw, nil +} + +func (a *API) mapRoutes(mux *http.ServeMux) { + mux.HandleFunc("POST /v1/map/roads", a.ingest(a.mapRoads)) +} + +func (a *API) mapRoads(w http.ResponseWriter, r *http.Request, c caller, body []byte) error { + if a.Maps == nil { + writeJSON(w, http.StatusNotFound, map[string]string{"error": "maps are not enabled"}) + return nil + } + var req struct { + Z, X, Y *int + } + if err := json.Unmarshal(body, &req); err != nil || req.Z == nil || req.X == nil || req.Y == nil { + return invalid("z, x and y are required") + } + if *req.Z != mapZoom { + return invalid("z must be %d", mapZoom) + } + limit := 1 << mapZoom + if *req.X < 0 || *req.X >= limit || *req.Y < 0 || *req.Y >= limit { + return invalid("x and y must be within the zoom level") + } + t := mapTile{Z: *req.Z, X: *req.X, Y: *req.Y} + data, cached, err := a.Maps.Tile(r.Context(), t) + if err != nil { + slog.Warn("map tile not available", "installation", who(c), "error", err) + writeJSON(w, http.StatusServiceUnavailable, map[string]string{"error": "map data not available, try again later"}) + return nil + } + if !cached { + slog.Info("map tile fetched", "installation", who(c), "ways", strings.Count(string(data), `"c":`), + "bytes", len(data)) + } + w.Header().Set("Content-Type", "application/json") + if strings.Contains(r.Header.Get("Accept-Encoding"), "gzip") { + var buf bytes.Buffer + zw := gzip.NewWriter(&buf) + _, _ = zw.Write(data) + _ = zw.Close() + w.Header().Set("Content-Encoding", "gzip") + data = buf.Bytes() + } + w.WriteHeader(http.StatusOK) + _, _ = w.Write(data) + return nil +} diff --git a/server/maps_test.go b/server/maps_test.go new file mode 100644 index 0000000..fd44c5f --- /dev/null +++ b/server/maps_test.go @@ -0,0 +1,212 @@ +package main + +import ( + "bytes" + "compress/gzip" + "encoding/json" + "fmt" + "io" + "log/slog" + "net/http" + "net/http/httptest" + "strings" + "sync/atomic" + "testing" +) + +// A tile in central Berlin. +var testTile = mapTile{Z: 14, X: 8802, Y: 5373} + +// fakeOverpass answers every query with a primary road (with a gap in its geometry), a +// residential street with a needless middle point, and a footway the car does not draw. +func fakeOverpass(t *testing.T, calls *atomic.Int32, status int) *httptest.Server { + t.Helper() + s, w, n, e := tileBounds(testTile) + lat := func(f float64) float64 { return s + (n-s)*f } + lon := func(f float64) float64 { return w + (e-w)*f } + answer := fmt.Sprintf(`{"elements":[ + {"type":"way","tags":{"highway":"primary"},"geometry":[ + {"lat":%f,"lon":%f},{"lat":%f,"lon":%f},null,{"lat":%f,"lon":%f},{"lat":%f,"lon":%f}]}, + {"type":"way","tags":{"highway":"residential"},"geometry":[ + {"lat":%f,"lon":%f},{"lat":%f,"lon":%f},{"lat":%f,"lon":%f}]}, + {"type":"way","tags":{"highway":"footway"},"geometry":[ + {"lat":%f,"lon":%f},{"lat":%f,"lon":%f}]}]}`, + lat(0.1), lon(0.1), lat(0.2), lon(0.2), lat(0.6), lon(0.6), lat(0.7), lon(0.7), + lat(0.5), lon(0.1), lat(0.5), lon(0.5), lat(0.5), lon(0.9), + lat(0.3), lon(0.3), lat(0.4), lon(0.4)) + return httptest.NewTLSServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { + calls.Add(1) + if r.Method != http.MethodPost || !strings.Contains(r.UserAgent(), "nils-mg4-server") { + t.Errorf("request %s %q", r.Method, r.UserAgent()) + } + _ = r.ParseForm() + if q := r.PostForm.Get("data"); !strings.Contains(q, "out geom(") || !strings.Contains(q, "residential") { + t.Errorf("query %q", q) + } + if status != http.StatusOK { + http.Error(rw, "busy", status) + return + } + _, _ = rw.Write([]byte(answer)) + })) +} + +func mapsAPI(overpass *httptest.Server) http.Handler { + api, _ := newTestAPI() + api.Maps = NewMaps(overpass.URL, mapUserAgent) + api.Maps.Client = overpass.Client() + api.Maps.gap = 0 + return api.Routes() +} + +func tileBody(t mapTile) string { + return fmt.Sprintf(`{"z":%d,"x":%d,"y":%d}`, t.Z, t.X, t.Y) +} + +func TestMapTileRoadsAreClassifiedSplitAndSimplified(t *testing.T) { + var calls atomic.Int32 + overpass := fakeOverpass(t, &calls, http.StatusOK) + defer overpass.Close() + h := mapsAPI(overpass) + if rec := do(t, h, "POST", "/v1/map/roads", "", tileBody(testTile)); rec.Code != 401 { + t.Fatalf("without the car's token: %d", rec.Code) + } + rec := do(t, h, "POST", "/v1/map/roads", testToken, tileBody(testTile)) + if rec.Code != 200 { + t.Fatalf("%d %s", rec.Code, rec.Body) + } + var got roadTile + if err := json.Unmarshal(rec.Body.Bytes(), &got); err != nil { + t.Fatal(err) + } + if got.Extent != 4096 || got.X != testTile.X || got.Y != testTile.Y { + t.Fatalf("tile %+v", got) + } + // The primary road in two pieces, the street straightened to its ends, no footway. + if len(got.Ways) != 3 || got.Ways[0].C != 1 || got.Ways[1].C != 1 || got.Ways[2].C != 3 { + t.Fatalf("ways %+v", got.Ways) + } + if len(got.Ways[2].P) != 4 { + t.Fatalf("the straight street keeps only its ends: %v", got.Ways[2].P) + } + // Tile-local: 0.1 of the way in is about 410 units from the west edge. + if x := got.Ways[0].P[0]; x < 400 || x > 420 { + t.Fatalf("x %d", x) + } + + // Cached: the second car asking costs Overpass nothing. + if rec := do(t, h, "POST", "/v1/map/roads", testToken, tileBody(testTile)); rec.Code != 200 { + t.Fatalf("%d", rec.Code) + } + if calls.Load() != 1 { + t.Fatalf("overpass asked %d times", calls.Load()) + } +} + +func TestMapTilesAreGzippedWhenTheCarAcceptsIt(t *testing.T) { + var calls atomic.Int32 + overpass := fakeOverpass(t, &calls, http.StatusOK) + defer overpass.Close() + req := httptest.NewRequest("POST", "/v1/map/roads", strings.NewReader(tileBody(testTile))) + req.Header.Set("Authorization", "Bearer "+testToken) + req.Header.Set("Accept-Encoding", "gzip") + rec := httptest.NewRecorder() + mapsAPI(overpass).ServeHTTP(rec, req) + if rec.Code != 200 || rec.Header().Get("Content-Encoding") != "gzip" { + t.Fatalf("%d %v", rec.Code, rec.Header()) + } + zr, err := gzip.NewReader(rec.Body) + if err != nil { + t.Fatal(err) + } + plain, _ := io.ReadAll(zr) + if !bytes.Contains(plain, []byte(`"extent":4096`)) { + t.Fatalf("%s", plain) + } +} + +func TestMapRequestsAreChecked(t *testing.T) { + var calls atomic.Int32 + overpass := fakeOverpass(t, &calls, http.StatusOK) + defer overpass.Close() + h := mapsAPI(overpass) + for _, body := range []string{`{}`, `{"z":15,"x":1,"y":1}`, `{"z":14,"x":-1,"y":1}`, + `{"z":14,"x":1,"y":16384}`, `not json`} { + if rec := do(t, h, "POST", "/v1/map/roads", testToken, body); rec.Code != 400 { + t.Errorf("%s: %d", body, rec.Code) + } + } + if calls.Load() != 0 { + t.Fatal("a bad request reached Overpass") + } +} + +func TestABusyOverpassIsATemporaryFailureAndNotCached(t *testing.T) { + var logged bytes.Buffer + previous := slog.Default() + slog.SetDefault(slog.New(slog.NewTextHandler(&logged, nil))) + defer slog.SetDefault(previous) + + var calls atomic.Int32 + overpass := fakeOverpass(t, &calls, http.StatusTooManyRequests) + defer overpass.Close() + h := mapsAPI(overpass) + for i := 0; i < 2; i++ { + if rec := do(t, h, "POST", "/v1/map/roads", testToken, tileBody(testTile)); rec.Code != 503 { + t.Fatalf("%d", rec.Code) + } + } + if calls.Load() != 2 { + t.Fatalf("a failure was cached: %d calls", calls.Load()) + } + // Where the car is stays out of the log. + out := logged.String() + if !strings.Contains(out, "overpass answered 429") || strings.Contains(out, "8802") || + strings.Contains(out, "5373") { + t.Fatalf("log:\n%s", out) + } +} + +func TestMapsAreOffWithoutOverpass(t *testing.T) { + api, _ := newTestAPI() + if rec := do(t, api.Routes(), "POST", "/v1/map/roads", testToken, tileBody(testTile)); rec.Code != 404 { + t.Fatalf("%d", rec.Code) + } +} + +func TestOverpassURLConfiguration(t *testing.T) { + base := map[string]string{"INFLUX_URL": "http://influx", "INFLUX_TOKEN": "t", "INFLUX_ORG": "o", + "INGEST_TOKEN": "0123456789abcdef01234567"} + load := func(overpass string) (config, error) { + return loadConfig(func(k string) string { + if k == "OVERPASS_URL" { + return overpass + } + return base[k] + }) + } + if c, err := load(""); err != nil || c.overpassURL != DefaultOverpassURL { + t.Fatalf("default: %q %v", c.overpassURL, err) + } + if c, err := load("off"); err != nil || c.overpassURL != "" { + t.Fatalf("off: %q %v", c.overpassURL, err) + } + if _, err := load("http://overpass.example.com/api/interpreter"); err == nil { + t.Fatal("plain http accepted") + } +} + +func TestTilePointsAndSimplification(t *testing.T) { + s, w, n, e := tileBounds(testTile) + if x, y := tilePoint(testTile, n, w); x != 0 || y != 0 { + t.Fatalf("north-west corner %d,%d", x, y) + } + if x, y := tilePoint(testTile, s, e); x != 4096 || y != 4096 { + t.Fatalf("south-east corner %d,%d", x, y) + } + line := [][2]int{{0, 0}, {10, 1}, {20, 0}, {30, 40}} + got := simplify(line, 4) + if len(got) != 3 || got[1] != [2]int{20, 0} { + t.Fatalf("%v", got) + } +}