Skip to content

Commit 2b7a6d8

Browse files
Merge branch 'feature/desired-heading-pointer' into develop
2 parents ba9afcc + b6da8e0 commit 2b7a6d8

File tree

5 files changed

+269
-1
lines changed

5 files changed

+269
-1
lines changed

src/java/pt/lsts/neptus/comm/SystemUtils.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ public class SystemUtils {
5050
public final static String RPM_MAP_ENTITY_KEY = "RPM";
5151
public final static String COURSE_DEGS_KEY = "Course";
5252
public final static String HEADING_DEGS_KEY = "Heading";
53+
public final static String DESIRED_HEADING_DEGS_KEY = "Desired Heading";
5354
public final static String FUEL_LEVEL_KEY = "Fuel Level";
5455
public final static String WEB_UPDATED_KEY = "Web Updated";
5556
public final static String LBL_CONFIG_KEY = "LblConfig";

src/java/pt/lsts/neptus/comm/manager/imc/SystemImcMsgCommInfo.java

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
import com.google.common.eventbus.AsyncEventBus;
4040

4141
import pt.lsts.imc.AcousticSystems;
42+
import pt.lsts.imc.DesiredHeading;
4243
import pt.lsts.imc.EmergencyControlState;
4344
import pt.lsts.imc.EntityParameters;
4445
import pt.lsts.imc.EstimatedState;
@@ -610,6 +611,19 @@ protected boolean processMsgLocally(MessageInfo info, IMCMessage msg) {
610611
e.printStackTrace();
611612
}
612613
break;
614+
case DesiredHeading.ID_STATIC:
615+
try {
616+
long timeMillis = msg.getTimestampMillis();
617+
double desiredHeadingRad = msg.getDouble("value");
618+
resSys.storeData(
619+
SystemUtils.DESIRED_HEADING_DEGS_KEY,
620+
(int) AngleUtils.nomalizeAngleDegrees360(MathMiscUtils.round(Math.toDegrees(desiredHeadingRad), 0)),
621+
timeMillis, true);
622+
}
623+
catch (Exception e) {
624+
e.printStackTrace();
625+
}
626+
break;
613627
default:
614628
break;
615629
}

src/java/pt/lsts/neptus/console/plugins/SystemsList.java

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -277,6 +277,10 @@ public enum MilStd2525SymbolsFilledEnum {
277277
category = "Renderer", userLevel = LEVEL.REGULAR)
278278
public boolean drawSystemLocAge = true;
279279

280+
@NeptusProperty(name = "Draw System Desired Heading", description = "Configures if this component will draw the system desired heading on the renderer",
281+
category = "Renderer", userLevel = LEVEL.REGULAR)
282+
public boolean drawSystemDesiredHeading = false;
283+
280284
@NeptusProperty(name = "Use Mil Std 2525 Like Symbols", description = "This configures if the location symbols to draw on the renderer will use the MIL-STD-2525 standard",
281285
category = "MilStd-2525", userLevel = LEVEL.REGULAR)
282286
public boolean useMilStd2525LikeSymbols = false;
@@ -1934,6 +1938,12 @@ private void drawImcSystem(StateRenderer2D renderer, Graphics2D g, ImcSystem sys
19341938
minimumSpeedToBeStopped);
19351939
}
19361940

1941+
// To draw the desired heading pointer
1942+
if (lod >= LOD_MIN_TO_SHOW_SPEED_VECTOR && drawSystemDesiredHeading) {
1943+
SystemPainterHelper.drawSystemDesiredHeading(renderer, g2, sys, iconWidth, isLocationKnownUpToDate,
1944+
minimumSpeedToBeStopped);
1945+
}
1946+
19371947
g2.dispose();
19381948
}
19391949

@@ -2134,6 +2144,18 @@ private void drawExternalSystem(StateRenderer2D renderer, Graphics2D g, External
21342144
}
21352145
}
21362146

2147+
// To draw the desired heading pointer
2148+
obj = sys.retrieveData(SystemUtils.DESIRED_HEADING_DEGS_KEY);
2149+
if (lod >= (LOD_MIN_TO_SHOW_SPEED_VECTOR + LOD_MIN_OFFSET_FOR_EXTERNAL) && obj != null) {
2150+
double desiredHeadingDegrees = ((Number) obj).doubleValue();
2151+
obj = sys.retrieveData(SystemUtils.GROUND_SPEED_KEY);
2152+
if (obj != null) {
2153+
double gSpeed = ((Number) obj).doubleValue();
2154+
SystemPainterHelper.drawSystemDesiredHeading(renderer, g2, desiredHeadingDegrees, iconWidth,
2155+
isLocationKnownUpToDate, minimumSpeedToBeStopped, gSpeed);
2156+
}
2157+
}
2158+
21372159
obj = sys.retrieveData(SystemUtils.DISTRESS_MSG_KEY, minutesToShowDistress * DateTimeUtil.MINUTE);
21382160
if (obj != null) {
21392161
Graphics2D g3 = (Graphics2D) g.create();

src/java/pt/lsts/neptus/gui/system/SystemPainterHelper.java

Lines changed: 53 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
import java.awt.geom.Arc2D;
4242
import java.awt.geom.Ellipse2D;
4343
import java.awt.geom.GeneralPath;
44+
import java.awt.geom.Path2D;
4445
import java.awt.geom.Rectangle2D;
4546
import java.awt.geom.RoundRectangle2D;
4647

@@ -456,7 +457,58 @@ public static final void drawCourseSpeedVectorForSystem(StateRenderer2D renderer
456457
g2.dispose();
457458
return;
458459
}
459-
460+
461+
public static final void drawSystemDesiredHeading(StateRenderer2D renderer, Graphics2D g,
462+
ImcSystem sys, double iconWidth, boolean isLocationKnownUpToDate,
463+
double minimumSpeedToBeStopped) {
464+
Object obj = sys.retrieveData(SystemUtils.DESIRED_HEADING_DEGS_KEY);
465+
if (obj != null) {
466+
double headingDegrees = (Integer) obj;
467+
obj = sys.retrieveData(SystemUtils.GROUND_SPEED_KEY);
468+
double speed = (Double) obj;
469+
if(obj != null) {
470+
drawSystemDesiredHeading(renderer, g, headingDegrees, iconWidth,
471+
isLocationKnownUpToDate, minimumSpeedToBeStopped, speed);
472+
}
473+
}
474+
}
475+
476+
public static final void drawSystemDesiredHeading(StateRenderer2D renderer, Graphics2D g,
477+
double headingDegrees, double iconWidth,
478+
boolean isLocationKnownUpToDate, double minimumSpeedToBeStopped, double speed) {
479+
Graphics2D g2 = (Graphics2D) g.create();
480+
481+
int useTransparency = (isLocationKnownUpToDate ? 255 : AGE_TRANSPARENCY);
482+
if (useTransparency != 255)
483+
g2.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, useTransparency / 255f));
484+
485+
if (Double.isFinite(headingDegrees) && speed > minimumSpeedToBeStopped) {
486+
int distanceFromCenter = (int) (iconWidth);
487+
double arrowLength = iconWidth * 0.6;
488+
double arrowAngle = Math.PI * 3 / 4;
489+
490+
g2.rotate(Math.toRadians(headingDegrees) - renderer.getRotation());
491+
g2.translate(0, -distanceFromCenter);
492+
493+
g2.setColor(Color.YELLOW);
494+
g2.setStroke(new BasicStroke(2, BasicStroke.CAP_BUTT, BasicStroke.JOIN_ROUND));
495+
496+
double x1 = -Math.sin(arrowAngle) * arrowLength;
497+
double y1 = -Math.cos(arrowAngle) * arrowLength;
498+
double x2 = 0;
499+
double y2 = 0;
500+
double x3 = Math.sin(arrowAngle) * arrowLength;
501+
double y3 = -Math.cos(arrowAngle) * arrowLength;
502+
503+
Path2D path = new Path2D.Double();
504+
path.moveTo(x1, y1);
505+
path.lineTo(x2, y2);
506+
path.lineTo(x3, y3);
507+
508+
g2.draw(path);
509+
}
510+
g2.dispose();
511+
}
460512

461513
public static final void drawVesselDimentionsIconForSystem(StateRenderer2D renderer, Graphics2D g, double width,
462514
double length, double widthOffsetFromCenter, double lenghtOffsetFromCenter, double headingDegrees,
Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
/*
2+
* Copyright (c) 2004-2026 Universidade do Porto - Faculdade de Engenharia
3+
* Laboratório de Sistemas e Tecnologia Subaquática (LSTS)
4+
* All rights reserved.
5+
* Rua Dr. Roberto Frias s/n, sala I203, 4200-465 Porto, Portugal
6+
*
7+
* This file is part of Neptus, Command and Control Framework.
8+
*
9+
* Commercial Licence Usage
10+
* Licencees holding valid commercial Neptus licences may use this file
11+
* in accordance with the commercial licence agreement provided with the
12+
* Software or, alternatively, in accordance with the terms contained in a
13+
* written agreement between you and Universidade do Porto. For licensing
14+
* terms, conditions, and further information contact lsts@fe.up.pt.
15+
*
16+
* Modified European Union Public Licence - EUPL v.1.1 Usage
17+
* Alternatively, this file may be used under the terms of the Modified EUPL,
18+
* Version 1.1 only (the "Licence"), appearing in the file LICENCE.md
19+
* included in the packaging of this file. You may not use this work
20+
* except in compliance with the Licence. Unless required by applicable
21+
* law or agreed to in writing, software distributed under the Licence is
22+
* distributed on an "AS IS" basis, WITHOUT WARRANTIES OR CONDITIONS OF
23+
* ANY KIND, either express or implied. See the Licence for the specific
24+
* language governing permissions and limitations at
25+
* https://github.com/LSTS/neptus/blob/develop/LICENSE.md
26+
* and http://ec.europa.eu/idabc/eupl.html.
27+
*
28+
* For more information please see <http://lsts.fe.up.pt/neptus>.
29+
*
30+
* Author: jcordeiro
31+
* January 06, 2026
32+
*/
33+
package pt.lsts.neptus.mra.replay;
34+
35+
import java.awt.BasicStroke;
36+
import java.awt.Color;
37+
import java.awt.Graphics2D;
38+
import java.awt.Polygon;
39+
import java.awt.RenderingHints;
40+
import java.awt.geom.AffineTransform;
41+
import java.awt.geom.Point2D;
42+
43+
import pt.lsts.imc.EstimatedState;
44+
import pt.lsts.imc.IMCMessage;
45+
import pt.lsts.imc.lsf.LsfIndex;
46+
import pt.lsts.neptus.mra.importers.IMraLogGroup;
47+
import pt.lsts.neptus.plugins.PluginDescription;
48+
import pt.lsts.neptus.renderer2d.StateRenderer2D;
49+
import pt.lsts.neptus.types.coord.LocationType;
50+
import pt.lsts.neptus.NeptusLog;
51+
52+
@PluginDescription(icon = "images/menus/compass.png", name = "Desired Heading")
53+
public class DesiredHeadingLayer implements LogReplayLayer {
54+
private static final Color ARROW_COLOR = new Color(255, 255, 0, 200);
55+
private static final int ARROW_LENGTH_PX = 40;
56+
private static final int ARROW_OFFSET_PX = 20;
57+
private static final int ARROW_GAP_PX = 4;
58+
private static final int ARROW_HEAD_LEN = 10;
59+
private static final int ARROW_HEAD_WIDTH = 6;
60+
61+
private LocationType currentPosition;
62+
private double currentHeading = 0;
63+
private boolean hasPosition = false;
64+
private LsfIndex index;
65+
66+
@Override
67+
public void paint(Graphics2D g, StateRenderer2D renderer) {
68+
if (!hasPosition) {
69+
return;
70+
}
71+
72+
Graphics2D g2 = (Graphics2D) g.create();
73+
try {
74+
g2.setColor(ARROW_COLOR);
75+
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
76+
g2.setStroke(new BasicStroke(2.5f));
77+
78+
Point2D vehiclePoint = renderer.getScreenPosition(currentPosition);
79+
80+
double headingRad = Math.toRadians(currentHeading);
81+
82+
double ux = Math.sin(headingRad);
83+
double uy = -Math.cos(headingRad);
84+
85+
Point2D startPoint = new Point2D.Double(
86+
vehiclePoint.getX() + ux * ARROW_OFFSET_PX,
87+
vehiclePoint.getY() + uy * ARROW_OFFSET_PX
88+
);
89+
90+
Point2D endPoint = new Point2D.Double(
91+
startPoint.getX() + ux * ARROW_LENGTH_PX,
92+
startPoint.getY() + uy * ARROW_LENGTH_PX
93+
);
94+
95+
g2.drawLine(
96+
(int) startPoint.getX(),
97+
(int) startPoint.getY(),
98+
(int) endPoint.getX(),
99+
(int) endPoint.getY()
100+
);
101+
102+
Point2D arrowTip = new Point2D.Double(
103+
endPoint.getX() + ux * ARROW_GAP_PX,
104+
endPoint.getY() + uy * ARROW_GAP_PX
105+
);
106+
double angle = Math.atan2(uy, ux) - Math.PI/2;
107+
drawArrowHead(g2, arrowTip, angle);
108+
} finally {
109+
g2.dispose();
110+
}
111+
}
112+
113+
private void drawArrowHead(Graphics2D g2, Point2D point, double angle) {
114+
AffineTransform old = g2.getTransform();
115+
116+
g2.translate(point.getX(), point.getY());
117+
g2.rotate(angle);
118+
119+
Polygon arrowHead = new Polygon();
120+
arrowHead.addPoint(0, 0);
121+
arrowHead.addPoint(-(int)(ARROW_HEAD_WIDTH), (int) -ARROW_HEAD_LEN);
122+
arrowHead.addPoint((int)(ARROW_HEAD_WIDTH), (int) -ARROW_HEAD_LEN);
123+
124+
g2.fill(arrowHead);
125+
g2.setTransform(old);
126+
}
127+
128+
@Override
129+
public void onMessage(IMCMessage message) {
130+
if ("DesiredHeading".equals(message.getAbbrev())) {
131+
currentHeading = Math.toDegrees(message.getDouble("value"));
132+
}
133+
else if ("EstimatedState".equals(message.getAbbrev())) {
134+
try {
135+
EstimatedState state = (EstimatedState) message;
136+
currentPosition = new LocationType(
137+
Math.toDegrees(state.getLat()),
138+
Math.toDegrees(state.getLon())
139+
);
140+
currentPosition.setDepth(state.getDepth());
141+
currentPosition.translatePosition(state.getX(), state.getY(), state.getZ());
142+
hasPosition = true;
143+
} catch (Exception e) {
144+
NeptusLog.pub().error("Error processing EstimatedState", e);
145+
}
146+
}
147+
}
148+
149+
@Override
150+
public void parse(IMraLogGroup source) {
151+
this.index = source.getLsfIndex();
152+
}
153+
154+
@Override
155+
public String[] getObservedMessages() {
156+
return new String[]{"DesiredHeading", "EstimatedState"};
157+
}
158+
159+
@Override
160+
public boolean canBeApplied(IMraLogGroup source, Context context) {
161+
return true;
162+
}
163+
164+
@Override
165+
public String getName() {
166+
return "Desired Heading";
167+
}
168+
169+
@Override
170+
public boolean getVisibleByDefault() {
171+
return false;
172+
}
173+
174+
@Override
175+
public void cleanup() {
176+
currentPosition = null;
177+
hasPosition = false;
178+
}
179+
}

0 commit comments

Comments
 (0)