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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LhzAVaUyqcw5PkGjz4mWW6
This commit is contained in:
2026-10-26 23:34:21 +01:00
co-authored by Claude Opus 5.5
parent 776a11b182
commit 65dc1ba847
24 changed files with 1312 additions and 18 deletions
@@ -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;
}
}
}