Skip to content

Commit 2c6428a

Browse files
committed
mobile: wind arrows anchor to ground + consistent density
Two fixes to the wind overlay: - Snap arrow positions to a fixed lat/lon lattice (multiples of step) so they stay ground-anchored and scroll with the map on pan, instead of being re-placed across the viewport each frame (which looked screen-locked). - Drop the 0.25° data-resolution floor: ~12 arrows across the view at any zoom, with bilinear interpolation (WindField.sampleInterp) so sub-grid sampling stays smooth rather than sparse when zoomed in. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016m9NjsSfBSx1RgN3fpnx7e
1 parent 32b1ecf commit 2c6428a

2 files changed

Lines changed: 49 additions & 4 deletions

File tree

mobile/lib/map_screen.dart

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -110,12 +110,17 @@ class _MapScreenState extends State<MapScreen> {
110110
}
111111
final markers = <Marker>[];
112112
final span = math.max(b.east - b.west, b.north - b.south);
113-
final step = math.max(f.dx, span / 14);
113+
// ~12 arrows across the view at any zoom (not tied to the 0.25° grid).
114+
final step = (span / 12).clamp(0.02, 5.0).toDouble();
114115
if (step > 0) {
115-
for (double lat = b.north; lat >= b.south; lat -= step) {
116-
for (double lon = b.west; lon <= b.east; lon += step) {
116+
// Snap the lattice to multiples of `step` so arrows stay anchored to the
117+
// ground and scroll with the map when panning (instead of the viewport).
118+
final lat0 = (b.south / step).floorToDouble() * step;
119+
final lon0 = (b.west / step).floorToDouble() * step;
120+
for (double lat = lat0; lat <= b.north; lat += step) {
121+
for (double lon = lon0; lon <= b.east; lon += step) {
117122
final nlon = ((lon + 540) % 360) - 180;
118-
final s = f.sample(nlon, lat);
123+
final s = f.sampleInterp(nlon, lat);
119124
if (s == null) continue;
120125
final knots = math.sqrt(s.u * s.u + s.v * s.v) * 1.94384;
121126
final ang = math.atan2(s.u, s.v); // bearing wind blows toward

mobile/lib/weather.dart

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,46 @@ class WindField {
3838
return (u: u[idx], v: v[idx]);
3939
}
4040

41+
/// Bilinearly-interpolated (u, v) for a lon/lat — smooth between grid cells
42+
/// so arrows can be drawn finer than the 0.25° data. Falls back to nearest at
43+
/// the grid edges. Null if fully out of range.
44+
({double u, double v})? sampleInterp(double lon, double lat) {
45+
if (dx <= 0 || dy <= 0) return null;
46+
final l = ((lon - lo1) % 360 + 360) % 360;
47+
final fx = l / dx;
48+
final fy = (la1 - lat) / dy;
49+
final ix = fx.floor(), iy = fy.floor();
50+
final tx = fx - ix, ty = fy - iy;
51+
double? g(int gx, int gy, List<double> a) {
52+
if (gy < 0 || gy >= ny) return null;
53+
final wx = ((gx % nx) + nx) % nx;
54+
final idx = gy * nx + wx;
55+
if (idx < 0 || idx >= a.length) return null;
56+
return a[idx];
57+
}
58+
59+
final u00 = g(ix, iy, u), u10 = g(ix + 1, iy, u);
60+
final u01 = g(ix, iy + 1, u), u11 = g(ix + 1, iy + 1, u);
61+
final v00 = g(ix, iy, v), v10 = g(ix + 1, iy, v);
62+
final v01 = g(ix, iy + 1, v), v11 = g(ix + 1, iy + 1, v);
63+
if (u00 == null ||
64+
u10 == null ||
65+
u01 == null ||
66+
u11 == null ||
67+
v00 == null ||
68+
v10 == null ||
69+
v01 == null ||
70+
v11 == null) {
71+
return sample(lon, lat); // fall back to nearest at edges
72+
}
73+
double bil(double a, double b, double c, double d) =>
74+
a * (1 - tx) * (1 - ty) +
75+
b * tx * (1 - ty) +
76+
c * (1 - tx) * ty +
77+
d * tx * ty;
78+
return (u: bil(u00, u10, u01, u11), v: bil(v00, v10, v01, v11));
79+
}
80+
4181
/// Wind speed in knots for a lon/lat, or null.
4282
double? speedKnots(double lon, double lat) {
4383
final s = sample(lon, lat);

0 commit comments

Comments
 (0)