Skip to content

Commit 860302c

Browse files
bgrozevclaude
andcommitted
Improve crab angle arrow and add setting to toggle it
- Double arrow length to 40m with 10m arrowhead barbs at 150deg - Add showCrabArrow setting (default on) under Map in Settings - Arrow only renders on wind-adjusted path, not pre-wind Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent b05d1d6 commit 860302c

4 files changed

Lines changed: 42 additions & 2 deletions

File tree

src/components/MapComponent.tsx

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,10 @@ function LegStatsDisplay({ stats, formatAltitude, altitudeLabel, showDrift = tru
183183
<div>Time: {stats.timeSec.toFixed(1)}s</div>
184184
<div>Heading: {formatDegrees(stats.heading)}</div>
185185
{showBearing && <div>Bearing: {formatDegrees(stats.bearing)}</div>}
186+
{showBearing && (() => {
187+
const crab = Math.abs(((stats.heading - stats.bearing + 180 + 360) % 360) - 180);
188+
return crab >= 1 ? <div>Crab: {Math.round(crab)}°</div> : null;
189+
})()}
186190
<div>Distance: {formatDistance(stats.distance, altitudeLabel)}</div>
187191
<div>Glide: {stats.glideRatio.toFixed(1)}</div>
188192
{showDrift && stats.windDriftDist > 1 && (
@@ -286,6 +290,7 @@ interface InteractivePointProps {
286290
options: google.maps.CircleOptions;
287291
showTooltip: boolean;
288292
showDrift: boolean;
293+
showCrabArrow: boolean;
289294
isHovered: boolean;
290295
onHover: () => void;
291296
onHoverEnd: () => void;
@@ -308,11 +313,24 @@ const HIGHLIGHT_OPTIONS: google.maps.CircleOptions = {
308313
const HOVER_RADIUS = 15;
309314
const HOVER_RADIUS_POM_ONLY = 30;
310315

311-
function InteractivePoint({ point, pointIndex, manoeuvreInitTime, pathStats, options, showTooltip, showDrift, isHovered, onHover, onHoverEnd, formatAltitude, altitudeLabel }: InteractivePointProps) {
316+
function destPoint(lat: number, lng: number, heading: number, distM: number): google.maps.LatLngLiteral {
317+
const pt = turf.destination([lng, lat], distM, heading, { units: 'meters' });
318+
return { lat: pt.geometry.coordinates[1], lng: pt.geometry.coordinates[0] };
319+
}
320+
321+
function InteractivePoint({ point, pointIndex, manoeuvreInitTime, pathStats, options, showTooltip, showDrift, showCrabArrow, isHovered, onHover, onHoverEnd, formatAltitude, altitudeLabel }: InteractivePointProps) {
312322
// POMs always have hover/tooltip, non-POMs respect the showTooltip setting
313323
const isPom = Boolean(point.pom);
314324
const enableHover = isPom || showTooltip;
315325

326+
// Crab angle arrow: shown on leg POMs when crab > 10°
327+
const segStats = isPom ? getPointSegmentStats(pointIndex, pathStats) : null;
328+
const legStats = segStats?.type === 'leg' ? segStats.stats : null;
329+
const crabAngle = legStats
330+
? Math.abs(((legStats.heading - legStats.bearing + 180 + 360) % 360) - 180)
331+
: 0;
332+
const renderCrabArrow = showCrabArrow && crabAngle > 10 && legStats != null;
333+
316334
// Use larger hover radius when only POMs are hoverable (showTooltip off)
317335
const hoverAreaOptions: google.maps.CircleOptions = {
318336
fillOpacity: 0,
@@ -329,6 +347,19 @@ function InteractivePoint({ point, pointIndex, manoeuvreInitTime, pathStats, opt
329347
center={point}
330348
options={options}
331349
/>
350+
{/* Crab angle heading arrow (40m shaft + arrowhead barbs) */}
351+
{renderCrabArrow && (() => {
352+
const h = legStats!.heading;
353+
const tip = destPoint(point.lat, point.lng, h, 40);
354+
const lineOpts: google.maps.PolylineOptions = { strokeColor: '#ffffff', strokeOpacity: 0.9, strokeWeight: 2, zIndex: 40, clickable: false };
355+
return (
356+
<>
357+
<PolylineF path={[{ lat: point.lat, lng: point.lng }, tip]} options={lineOpts} />
358+
<PolylineF path={[tip, destPoint(tip.lat, tip.lng, (h + 150 + 360) % 360, 10)]} options={lineOpts} />
359+
<PolylineF path={[tip, destPoint(tip.lat, tip.lng, (h - 150 + 360) % 360, 10)]} options={lineOpts} />
360+
</>
361+
);
362+
})()}
332363
{/* Invisible hover area */}
333364
{enableHover && (
334365
<CircleF
@@ -511,7 +542,7 @@ function MapComponent({
511542
forecastValidTime,
512543
finalHeading = 0
513544
}: MapComponentProps) {
514-
const { showPoms, showPomAltitudes, showPomTooltips, showPreWind, displayWindArrow, highlightCorrespondingPoints, showMeasureTool } = settings;
545+
const { showPoms, showPomAltitudes, showPomTooltips, showPreWind, displayWindArrow, highlightCorrespondingPoints, showMeasureTool, showCrabArrow } = settings;
515546
const { formatAltitude, altitudeLabel, formatWindSpeed, windSpeedLabel } = useUnits();
516547
const [hoveredPointIndex, setHoveredPointIndex] = useState<number | null>(null);
517548
const [hoveredPreWindIndex, setHoveredPreWindIndex] = useState<number | null>(null);
@@ -660,6 +691,7 @@ function MapComponent({
660691
}}
661692
showTooltip={showPomTooltips}
662693
showDrift={false}
694+
showCrabArrow={false}
663695
isHovered={hoveredPreWindIndex === i}
664696
onHover={() => setHoveredPreWindIndex(i)}
665697
onHoverEnd={() => setHoveredPreWindIndex(null)}
@@ -697,6 +729,7 @@ function MapComponent({
697729
}}
698730
showTooltip={showPomTooltips}
699731
showDrift={true}
732+
showCrabArrow={showCrabArrow}
700733
isHovered={hoveredPointIndex === i}
701734
onHover={() => setHoveredPointIndex(i)}
702735
onHoverEnd={() => setHoveredPointIndex(null)}

src/components/SettingsComponent.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,11 @@ const settingsGroups: SettingsGroup[] = [
8282
tooltip:
8383
'When hovering over a point, also highlight the corresponding point in the pre-wind pattern.'
8484
},
85+
{
86+
key: 'showCrabArrow',
87+
label: 'Show crab angle arrows',
88+
tooltip: 'Show a heading arrow on pattern points where crab angle exceeds 10°.'
89+
},
8590
{
8691
key: 'displayWindArrow',
8792
label: 'Show average wind on the map',

src/hooks/useAppState.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ const DEFAULT_SETTINGS: Settings = {
4646
showPresets: true,
4747
showMeasureTool: false,
4848
highlightCorrespondingPoints: true,
49+
showCrabArrow: true,
4950
units: DEFAULT_UNIT_PREFERENCES
5051
};
5152

src/types/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ export interface Settings {
7373
showPresets: boolean;
7474
showMeasureTool: boolean;
7575
highlightCorrespondingPoints: boolean;
76+
showCrabArrow: boolean;
7677
units: UnitPreferences;
7778
}
7879

0 commit comments

Comments
 (0)