Skip to content

Commit 23e5b41

Browse files
committed
mobile: render wind as markers (was invisible) + filter -filtered cameras
Wind: the CustomPainter overlay drew nothing; switch to viewport-sampled arrow MarkerLayer (the marker path already renders reliably for AIS/boat). Sample the field across the visible bounds (tracked via onPositionChanged, seeded on toggle), one arrow per grid step pointing where the wind blows, coloured by speed; capped at 1500 markers. Remove the unused wind_layer.dart. Cameras: apply the web app's filtered-camera rule (drop <name> when <name>-filtered exists). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016m9NjsSfBSx1RgN3fpnx7e
1 parent 22018e3 commit 23e5b41

3 files changed

Lines changed: 59 additions & 99 deletions

File tree

mobile/lib/map_screen.dart

Lines changed: 51 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ import 'debug_screen.dart';
1313
import 'tile_sources.dart';
1414
import 'viam_connection.dart';
1515
import 'weather.dart';
16-
import 'wind_layer.dart';
1716

1817
/// Full-screen chart with a heading-rotated boat marker. The data readouts live
1918
/// in a dashboard drawer (DataDrawer) rather than overlaid on the chart; only
@@ -37,6 +36,7 @@ class _MapScreenState extends State<MapScreen> {
3736
WindField? _wind;
3837
bool _windOn = false;
3938
bool _windLoading = false;
39+
LatLngBounds? _bounds; // current viewport, for sampling wind arrows
4040

4141
Future<void> _toggleWind() async {
4242
if (_windOn) {
@@ -57,6 +57,7 @@ class _MapScreenState extends State<MapScreen> {
5757
_wind = f;
5858
_windOn = true;
5959
_windLoading = false;
60+
_bounds = _map.camera.visibleBounds; // seed so arrows show immediately
6061
});
6162
widget.state
6263
.setWindInfo('loaded ${f.nx}×${f.ny} grid (${f.u.length} cells)');
@@ -92,6 +93,46 @@ class _MapScreenState extends State<MapScreen> {
9293
if (mounted) setState(() {});
9394
}
9495

96+
/// Wind arrows sampled across the visible bounds. Uses MarkerLayer (which is
97+
/// proven to render here) rather than a CustomPainter. Each arrow points the
98+
/// way the wind blows and is coloured by speed.
99+
List<Marker> _windMarkers(WindField f, LatLngBounds b) {
100+
final markers = <Marker>[];
101+
final span = math.max(b.east - b.west, b.north - b.south);
102+
final step = math.max(f.dx, span / 14);
103+
if (step <= 0) return markers;
104+
for (double lat = b.north; lat >= b.south; lat -= step) {
105+
for (double lon = b.west; lon <= b.east; lon += step) {
106+
final nlon = ((lon + 540) % 360) - 180;
107+
final s = f.sample(nlon, lat);
108+
if (s == null) continue;
109+
final knots = math.sqrt(s.u * s.u + s.v * s.v) * 1.94384;
110+
// Bearing (clockwise from north) the wind blows toward.
111+
final ang = math.atan2(s.u, s.v);
112+
markers.add(Marker(
113+
point: LatLng(lat, nlon),
114+
width: 24,
115+
height: 24,
116+
child: Transform.rotate(
117+
angle: ang,
118+
child: Icon(Icons.navigation, size: 16, color: _windColor(knots)),
119+
),
120+
));
121+
if (markers.length >= 1500) return markers;
122+
}
123+
}
124+
return markers;
125+
}
126+
127+
Color _windColor(double kn) {
128+
if (kn < 5) return const Color(0xFFabd9e9);
129+
if (kn < 12) return const Color(0xFF74add1);
130+
if (kn < 18) return const Color(0xFF66bd63);
131+
if (kn < 25) return const Color(0xFFfdae61);
132+
if (kn < 34) return const Color(0xFFf46d43);
133+
return const Color(0xFFd73027);
134+
}
135+
95136
void _showAisDetails(AisBoat b) {
96137
showModalBottomSheet<void>(
97138
context: context,
@@ -160,9 +201,13 @@ class _MapScreenState extends State<MapScreen> {
160201
children: [
161202
FlutterMap(
162203
mapController: _map,
163-
options: const MapOptions(
164-
initialCenter: LatLng(41.3, -72.0), // Long Island Sound-ish
204+
options: MapOptions(
205+
initialCenter: const LatLng(41.3, -72.0), // Long Island Sound-ish
165206
initialZoom: 9,
207+
onPositionChanged: (camera, _) {
208+
_bounds = camera.visibleBounds;
209+
if (_windOn && mounted) setState(() {});
210+
},
166211
),
167212
children: [
168213
TileLayer(
@@ -176,8 +221,9 @@ class _MapScreenState extends State<MapScreen> {
176221
errorTileCallback: (tile, error, stackTrace) =>
177222
debugPrint('tile load failed (${_base.id}): $error'),
178223
),
179-
// Wind overlay (over the chart, under markers).
180-
if (_windOn && _wind != null) WindLayer(field: _wind!),
224+
// Wind overlay (arrow markers, over the chart, under boat markers).
225+
if (_windOn && _wind != null && _bounds != null)
226+
MarkerLayer(markers: _windMarkers(_wind!, _bounds!)),
181227
// Active route: line from the boat to the destination.
182228
if (s.position != null && s.destination != null)
183229
PolylineLayer(

mobile/lib/viam_connection.dart

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -106,15 +106,19 @@ class ViamConnection {
106106
return null;
107107
}
108108

109-
/// All camera components on the robot, sorted by name.
109+
/// Camera components on the robot, sorted, with the web app's filtered-camera
110+
/// rule: drop "<name>" when a "<name>-filtered" sibling exists (prefer the
111+
/// filtered feed). (chartplotter-hide needs the machine config; not yet done.)
110112
List<String> _discoverCameras(RobotClient robot) {
111-
final out = <String>[];
113+
final all = <String>[];
112114
try {
113115
for (final rn in robot.resourceNames) {
114-
if (rn.subtype == 'camera') out.add(rn.name);
116+
if (rn.subtype == 'camera') all.add(rn.name);
115117
}
116118
} catch (_) {}
117-
out.sort();
119+
final names = all.toSet();
120+
final out = all.where((n) => !names.contains('$n-filtered')).toList()
121+
..sort();
118122
return out;
119123
}
120124

mobile/lib/wind_layer.dart

Lines changed: 0 additions & 90 deletions
This file was deleted.

0 commit comments

Comments
 (0)