signalk-racer 0.9.3 → 0.9.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/ReleaseNotes.md
CHANGED
|
@@ -6,4 +6,8 @@
|
|
|
6
6
|
+ Added API to set the start line
|
|
7
7
|
+ Added webapp to call start line API
|
|
8
8
|
## 0.9.3 KIP racer-timer integration
|
|
9
|
-
+ added timeToStart API
|
|
9
|
+
+ added timeToStart API
|
|
10
|
+
## 0.9.4 Trivial rename
|
|
11
|
+
+ Renamed README.md for npmjs
|
|
12
|
+
## 0.9.5 Restructured
|
|
13
|
+
+ Restructured the plugin to avoid initialization problem in new server
|
package/index.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
+
const geolib = require("geolib");
|
|
1
2
|
module.exports = (app) => {
|
|
3
|
+
const state = {};
|
|
2
4
|
const geolib = require('geolib')
|
|
3
5
|
const racerSchema = {
|
|
4
6
|
title: 'Set System Time with sudo',
|
|
@@ -39,13 +41,14 @@ module.exports = (app) => {
|
|
|
39
41
|
}
|
|
40
42
|
}
|
|
41
43
|
|
|
44
|
+
const unsubscribes = [];
|
|
45
|
+
|
|
42
46
|
// Update meta data for non standard paths
|
|
43
|
-
const timestamp = new Date().toISOString();
|
|
44
47
|
app.handleMessage('vessels.self', {
|
|
45
48
|
context: "vessels.self",
|
|
46
49
|
updates: [
|
|
47
50
|
{
|
|
48
|
-
timestamp:
|
|
51
|
+
timestamp: new Date().toISOString(),
|
|
49
52
|
"meta": [
|
|
50
53
|
{
|
|
51
54
|
"path": "navigation.racing.startLineLength",
|
|
@@ -88,391 +91,389 @@ module.exports = (app) => {
|
|
|
88
91
|
]
|
|
89
92
|
});
|
|
90
93
|
|
|
91
|
-
|
|
94
|
+
// send multiple deltas, each in the form of { path, value, units?}
|
|
95
|
+
function sendDeltas(updatesArray) {
|
|
96
|
+
const timestamp = new Date().toISOString();
|
|
92
97
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
98
|
+
const delta = {
|
|
99
|
+
context: 'vessels.self',
|
|
100
|
+
updates: [
|
|
101
|
+
{
|
|
102
|
+
source: {label: 'signalk-racer'},
|
|
103
|
+
timestamp: timestamp,
|
|
104
|
+
values: updatesArray.map(({path, value}) => ({
|
|
105
|
+
path,
|
|
106
|
+
value
|
|
107
|
+
}))
|
|
108
|
+
}
|
|
109
|
+
]
|
|
110
|
+
};
|
|
104
111
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
wayPointsScanned: false,
|
|
109
|
-
gpsFromBow: fromBow ? fromBow.value : null,
|
|
110
|
-
gpsFromCenter: fromCenter ? fromCenter.value : null,
|
|
111
|
-
}
|
|
112
|
+
app.debug('Sending deltas:', JSON.stringify(delta));
|
|
113
|
+
app.handleMessage('vessels.self', delta);
|
|
114
|
+
}
|
|
112
115
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
+
function waypointToPosition(waypoint) {
|
|
117
|
+
if (
|
|
118
|
+
waypoint &&
|
|
119
|
+
waypoint.feature &&
|
|
120
|
+
waypoint.feature.geometry &&
|
|
121
|
+
waypoint.feature.geometry.type === 'Point' &&
|
|
122
|
+
Array.isArray(waypoint.feature.geometry.coordinates) &&
|
|
123
|
+
waypoint.feature.geometry.coordinates.length === 2
|
|
124
|
+
) {
|
|
125
|
+
const [longitude, latitude] = waypoint.feature.geometry.coordinates;
|
|
126
|
+
return {latitude, longitude};
|
|
127
|
+
} else {
|
|
128
|
+
return null;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
116
131
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
{
|
|
121
|
-
source: {label: 'signalk-racer'},
|
|
122
|
-
timestamp: timestamp,
|
|
123
|
-
values: updatesArray.map(({path, value}) => ({
|
|
124
|
-
path,
|
|
125
|
-
value
|
|
126
|
-
}))
|
|
127
|
-
}
|
|
128
|
-
]
|
|
129
|
-
};
|
|
132
|
+
function toDegrees(rad) {
|
|
133
|
+
return rad ? rad * (180 / Math.PI) : null;
|
|
134
|
+
}
|
|
130
135
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
136
|
+
function toRadians(degrees) {
|
|
137
|
+
return degrees ? degrees * (Math.PI / 180) : null;
|
|
138
|
+
}
|
|
134
139
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
140
|
+
function bowPosition(position) {
|
|
141
|
+
let headingTrue = app.getSelfPath("navigation.headingTrue");
|
|
142
|
+
if (!headingTrue)
|
|
143
|
+
headingTrue = app.getSelfPath("navigation.courseOverGroundTrue");
|
|
144
|
+
if (!headingTrue || (!state.gpsFromBow && !state.gpsFromCenter))
|
|
145
|
+
return position
|
|
146
|
+
|
|
147
|
+
headingTrue = toDegrees(headingTrue.value);
|
|
148
|
+
app.debug('headingTrue:' + headingTrue);
|
|
149
|
+
app.debug('position:' + JSON.stringify(position));
|
|
150
|
+
app.debug('gpsFromBow:' + JSON.stringify(state.gpsFromBow));
|
|
151
|
+
app.debug('gpsFromCenter:' + JSON.stringify(state.gpsFromCenter));
|
|
152
|
+
|
|
153
|
+
let bow = position;
|
|
154
|
+
if (state.gpsFromBow)
|
|
155
|
+
bow = geolib.computeDestinationPoint(bow, state.gpsFromBow, headingTrue);
|
|
156
|
+
if (state.gpsFromCenter)
|
|
157
|
+
bow = geolib.computeDestinationPoint(bow, state.gpsFromCenter, headingTrue + 90);
|
|
158
|
+
app.debug('bowPosition:' + JSON.stringify(bow));
|
|
159
|
+
return bow;
|
|
160
|
+
}
|
|
150
161
|
|
|
151
|
-
|
|
152
|
-
|
|
162
|
+
async function setTimeToStart(req, res) {
|
|
163
|
+
try {
|
|
164
|
+
const body = req.body;
|
|
165
|
+
if (!body || typeof body.timeToStart !== 'number') {
|
|
166
|
+
res.status(400).json({ error: 'Missing or invalid `timeToStart`' });
|
|
167
|
+
return;
|
|
153
168
|
}
|
|
154
169
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
170
|
+
const timeToStart = body.timeToStart; // value in seconds, or whatever unit you agree on
|
|
171
|
+
app.debug(`Setting timeToStart to ${timeToStart}`);
|
|
172
|
+
|
|
173
|
+
sendDeltas([
|
|
174
|
+
{path: 'navigation.racing.timeToStart', value: timeToStart},
|
|
175
|
+
]);
|
|
176
|
+
|
|
177
|
+
res.json({
|
|
178
|
+
message: 'OK',
|
|
179
|
+
timeToStart,
|
|
180
|
+
distanceToLine: state.distanceToLine ? state.distanceToLine : null,
|
|
181
|
+
});
|
|
182
|
+
} catch (err) {
|
|
183
|
+
app.error('Failed to set time to start:', err);
|
|
184
|
+
res.status(500).json({ error: 'Failed to set time to start' });
|
|
185
|
+
}
|
|
186
|
+
}
|
|
158
187
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
headingTrue = toDegrees(headingTrue.value);
|
|
167
|
-
app.debug('headingTrue:' + headingTrue);
|
|
168
|
-
app.debug('position:' + JSON.stringify(position));
|
|
169
|
-
app.debug('gpsFromBow:' + JSON.stringify(state.gpsFromBow));
|
|
170
|
-
app.debug('gpsFromCenter:' + JSON.stringify(state.gpsFromCenter));
|
|
171
|
-
|
|
172
|
-
let bow = position;
|
|
173
|
-
if (state.gpsFromBow)
|
|
174
|
-
bow = geolib.computeDestinationPoint(bow, state.gpsFromBow, headingTrue);
|
|
175
|
-
if (state.gpsFromCenter)
|
|
176
|
-
bow = geolib.computeDestinationPoint(bow, state.gpsFromCenter, headingTrue + 90);
|
|
177
|
-
app.debug('bowPosition:' + JSON.stringify(bow));
|
|
178
|
-
return bow;
|
|
188
|
+
async function setStartLineEnd(req, res) {
|
|
189
|
+
try {
|
|
190
|
+
const end = req.params.end; // "port" or "stb"
|
|
191
|
+
if (end !== 'port' && end !== 'stb') {
|
|
192
|
+
res.status(400).send({error: 'End must be "port" or "stb"'});
|
|
193
|
+
return;
|
|
179
194
|
}
|
|
180
195
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
196
|
+
let position = app.getSelfPath('navigation.position');
|
|
197
|
+
if (position)
|
|
198
|
+
position = position.value;
|
|
199
|
+
else {
|
|
200
|
+
res.status(500).send({error: 'No valid position available'});
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
const bow = bowPosition(position);
|
|
204
|
+
const startLine = state.startLine;
|
|
205
|
+
const waypointName = state.options[`startLine${end.charAt(0).toUpperCase() + end.slice(1)}`];
|
|
206
|
+
|
|
207
|
+
let waypointId = startLine ? startLine[end + 'Id'] : null;
|
|
208
|
+
if (!waypointId) {
|
|
209
|
+
// Get the ID of the last existing waypoint with the given name.
|
|
210
|
+
const waypoints = await app.resourcesApi.listResources('waypoints', {});
|
|
211
|
+
for (const [id, resource] of Object.entries(waypoints)) {
|
|
212
|
+
if (resource.name === waypointName) {
|
|
213
|
+
waypointId = id;
|
|
214
|
+
// don't break here as we want the last waypoint with the given name
|
|
187
215
|
}
|
|
188
|
-
|
|
189
|
-
const timeToStart = body.timeToStart; // value in seconds, or whatever unit you agree on
|
|
190
|
-
app.debug(`Setting timeToStart to ${timeToStart}`);
|
|
191
|
-
|
|
192
|
-
sendDeltas([
|
|
193
|
-
{path: 'navigation.racing.timeToStart', value: timeToStart},
|
|
194
|
-
]);
|
|
195
|
-
|
|
196
|
-
res.json({
|
|
197
|
-
message: 'OK',
|
|
198
|
-
timeToStart,
|
|
199
|
-
distanceToLine: state.distanceToLine ? state.distanceToLine : null,
|
|
200
|
-
});
|
|
201
|
-
} catch (err) {
|
|
202
|
-
app.error('Failed to set time to start:', err);
|
|
203
|
-
res.status(500).json({ error: 'Failed to set time to start' });
|
|
204
216
|
}
|
|
205
217
|
}
|
|
206
218
|
|
|
207
|
-
|
|
208
|
-
try {
|
|
209
|
-
const end = req.params.end; // "port" or "stb"
|
|
210
|
-
if (end !== 'port' && end !== 'stb') {
|
|
211
|
-
res.status(400).send({error: 'End must be "port" or "stb"'});
|
|
212
|
-
return;
|
|
213
|
-
}
|
|
219
|
+
app.debug(`POSITION to set ${end}(${waypointName}/${waypointId}): ${JSON.stringify(bow)}`);
|
|
214
220
|
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
let waypointId = startLine ? startLine[end + 'Id'] : null;
|
|
227
|
-
if (!waypointId) {
|
|
228
|
-
// Get the ID of the last existing waypoint with the given name.
|
|
229
|
-
const waypoints = await app.resourcesApi.listResources('waypoints', {});
|
|
230
|
-
for (const [id, resource] of Object.entries(waypoints)) {
|
|
231
|
-
if (resource.name === waypointName) {
|
|
232
|
-
waypointId = id;
|
|
233
|
-
// don't break here as we want the last waypoint with the given name
|
|
234
|
-
}
|
|
235
|
-
}
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
app.debug(`POSITION to set ${end}(${waypointName}/${waypointId}): ${JSON.stringify(bow)}`);
|
|
239
|
-
|
|
240
|
-
const waypoint = {
|
|
241
|
-
name: waypointName,
|
|
242
|
-
feature: {
|
|
243
|
-
type: 'Feature',
|
|
244
|
-
geometry: {
|
|
245
|
-
type: 'Point',
|
|
246
|
-
coordinates: [bow.longitude, bow.latitude]
|
|
247
|
-
},
|
|
248
|
-
properties: {}
|
|
249
|
-
}
|
|
250
|
-
}
|
|
221
|
+
const waypoint = {
|
|
222
|
+
name: waypointName,
|
|
223
|
+
feature: {
|
|
224
|
+
type: 'Feature',
|
|
225
|
+
geometry: {
|
|
226
|
+
type: 'Point',
|
|
227
|
+
coordinates: [bow.longitude, bow.latitude]
|
|
228
|
+
},
|
|
229
|
+
properties: {}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
251
232
|
|
|
252
|
-
|
|
233
|
+
app.debug(`waypoint: ${waypointId} -> ${end}/${waypointName} : ${JSON.stringify(waypoint)}`);
|
|
253
234
|
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
235
|
+
if (waypointId)
|
|
236
|
+
await app.resourcesApi.setResource('waypoints', waypointId, waypoint);
|
|
237
|
+
else
|
|
238
|
+
await app.resourcesApi.createResource('waypoints', waypoint);
|
|
258
239
|
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
240
|
+
res.json({
|
|
241
|
+
message: 'OK'
|
|
242
|
+
});
|
|
262
243
|
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
244
|
+
if (startLine)
|
|
245
|
+
{
|
|
246
|
+
const newStartLine = { ...state.startLine };
|
|
247
|
+
newStartLine[end + 'Id'] = waypointId;
|
|
248
|
+
newStartLine[end] = bow;
|
|
249
|
+
startLine.length = geolib.getPreciseDistance(newStartLine.port, newStartLine.stb, 0.1);
|
|
250
|
+
startLine.bearing = geolib.getRhumbLineBearing(newStartLine.stb, newStartLine.port);
|
|
251
|
+
state.startLine = newStartLine;
|
|
252
|
+
sendDeltas([
|
|
253
|
+
{ path: `navigation.racing.startLine${end.charAt(0).toUpperCase() + end.slice(1)}`, value: bow },
|
|
254
|
+
{ path: 'navigation.racing.startLineLength', value: newStartLine.length },
|
|
255
|
+
]);
|
|
275
256
|
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
}
|
|
280
|
-
} catch (err) {
|
|
281
|
-
app.error('Failed to set start line end:', err);
|
|
282
|
-
res.status(500).json({ error: 'Failed to set start line end' });
|
|
283
|
-
}
|
|
257
|
+
processPosition(position);
|
|
258
|
+
} else {
|
|
259
|
+
findLineAndThenProcess(position, true);
|
|
284
260
|
}
|
|
261
|
+
} catch (err) {
|
|
262
|
+
app.error('Failed to set start line end:', err);
|
|
263
|
+
res.status(500).json({ error: 'Failed to set start line end' });
|
|
264
|
+
}
|
|
265
|
+
}
|
|
285
266
|
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
267
|
+
function findLineAndThenProcess(position, alwaysFindLine = false) {
|
|
268
|
+
if (!state.startLine || alwaysFindLine) {
|
|
269
|
+
app.resourcesApi.listResources(
|
|
270
|
+
'waypoints',
|
|
271
|
+
{}
|
|
272
|
+
).then(data => {
|
|
273
|
+
state.wayPointsScanned = true;
|
|
274
|
+
|
|
275
|
+
const startLine = {
|
|
276
|
+
stb: null,
|
|
277
|
+
port: null,
|
|
278
|
+
length: null,
|
|
279
|
+
bearing: null,
|
|
280
|
+
}
|
|
281
|
+
for (const [key, value] of Object.entries(data)) {
|
|
282
|
+
app.debug(`WAYPOINT: ${key}, Value:`, JSON.stringify(value));
|
|
283
|
+
if (value.name === state.options.startLinePort) {
|
|
284
|
+
let pos = waypointToPosition(value);
|
|
285
|
+
if (pos) {
|
|
286
|
+
startLine.portId = key;
|
|
287
|
+
startLine.port = pos;
|
|
299
288
|
}
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
startLine.port = pos;
|
|
307
|
-
}
|
|
308
|
-
}
|
|
309
|
-
if (value.name === options.startLineStb) {
|
|
310
|
-
let pos = waypointToPosition(value);
|
|
311
|
-
if (pos) {
|
|
312
|
-
startLine.stbId = key;
|
|
313
|
-
startLine.stb = pos;
|
|
314
|
-
}
|
|
315
|
-
}
|
|
289
|
+
}
|
|
290
|
+
if (value.name === state.options.startLineStb) {
|
|
291
|
+
let pos = waypointToPosition(value);
|
|
292
|
+
if (pos) {
|
|
293
|
+
startLine.stbId = key;
|
|
294
|
+
startLine.stb = pos;
|
|
316
295
|
}
|
|
296
|
+
}
|
|
297
|
+
}
|
|
317
298
|
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
299
|
+
if (startLine.port && startLine.stb) {
|
|
300
|
+
startLine.length = geolib.getPreciseDistance(startLine.port, startLine.stb, 0.1);
|
|
301
|
+
startLine.bearing = geolib.getRhumbLineBearing(startLine.stb, startLine.port);
|
|
321
302
|
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
303
|
+
app.debug(`STARTLINE: startLinePort: ${JSON.stringify(startLine)}`);
|
|
304
|
+
state.startLine = startLine;
|
|
305
|
+
} else {
|
|
306
|
+
app.debug(`STARTLINE: undefined`);
|
|
307
|
+
state.startLine = null;
|
|
308
|
+
}
|
|
328
309
|
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
310
|
+
sendDeltas([
|
|
311
|
+
{path: 'navigation.racing.startLinePort', value: startLine.port},
|
|
312
|
+
{path: 'navigation.racing.startLineStb', value: startLine.stb},
|
|
313
|
+
{path: 'navigation.racing.startLineLength', value: startLine.length},
|
|
314
|
+
]);
|
|
334
315
|
|
|
335
|
-
|
|
336
|
-
|
|
316
|
+
processPosition(position);
|
|
317
|
+
processWind()
|
|
337
318
|
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
319
|
+
}).catch(err => {
|
|
320
|
+
app.error(err);
|
|
321
|
+
});
|
|
322
|
+
} else {
|
|
323
|
+
processPosition(position);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
345
326
|
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
327
|
+
function processPosition(position) {
|
|
328
|
+
if (!position) {
|
|
329
|
+
position = app.getSelfPath('navigation.position');
|
|
330
|
+
app.debug('POSITION:' + JSON.stringify(position));
|
|
331
|
+
position = position ? position.value : null;
|
|
332
|
+
}
|
|
352
333
|
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
}
|
|
381
|
-
angle = ((angle + 180) % 360 + 360) % 360 - 180;
|
|
382
|
-
app.debug('angle:' + angle);
|
|
383
|
-
app.debug('toEnd:' + toEnd);
|
|
384
|
-
ocs = angle < 0;
|
|
385
|
-
app.debug('ocs:' + ocs);
|
|
386
|
-
|
|
387
|
-
let absAngle = Math.abs(angle);
|
|
388
|
-
const farFromLine = absAngle > 135;
|
|
389
|
-
app.debug('farFromLine:' + farFromLine);
|
|
390
|
-
const perpendicularToLine = toEnd * Math.sin(toRadians(absAngle));
|
|
391
|
-
app.debug('perpendicularToLine:' + perpendicularToLine);
|
|
392
|
-
let toLine = farFromLine ? Math.sqrt(toEnd * toEnd - perpendicularToLine * perpendicularToLine) : perpendicularToLine;
|
|
393
|
-
toLine = Math.round(10 * toLine) / 10;
|
|
394
|
-
app.debug('toLine:' + toLine);
|
|
395
|
-
const distanceToLine = ocs ? -toLine : toLine;
|
|
396
|
-
app.debug('distanceToLine:' + distanceToLine);
|
|
397
|
-
state.distanceToLine = distanceToLine;
|
|
398
|
-
sendDeltas([
|
|
399
|
-
{path: 'navigation.racing.distanceStartline', value: distanceToLine},
|
|
400
|
-
]);
|
|
401
|
-
}
|
|
402
|
-
else if (state.distanceToLine) {
|
|
403
|
-
state.distanceToLine = null;
|
|
404
|
-
sendDeltas([
|
|
405
|
-
{path: 'navigation.racing.distanceStartline', value: null},
|
|
406
|
-
]);
|
|
407
|
-
}
|
|
334
|
+
const startLine = state.startLine;
|
|
335
|
+
|
|
336
|
+
app.debug(`processPosition ${JSON.stringify(position)} to ${JSON.stringify(startLine)}`);
|
|
337
|
+
if (position && startLine && (!state.activeRoute || state.activeRoute.pointIndex < 2)) {
|
|
338
|
+
// handle the bow offset
|
|
339
|
+
const bow = bowPosition(position);
|
|
340
|
+
|
|
341
|
+
// Get the distance to each end of the line
|
|
342
|
+
const toPort = geolib.getPreciseDistance(bow, startLine.port, 0.1);
|
|
343
|
+
const toStb = geolib.getPreciseDistance(bow, startLine.stb, 0.1);
|
|
344
|
+
|
|
345
|
+
const closest = toPort < toStb ? startLine.port : startLine.stb;
|
|
346
|
+
let toEnd;
|
|
347
|
+
let ocs;
|
|
348
|
+
let angle;
|
|
349
|
+
if (closest === state.startLine.port) {
|
|
350
|
+
toEnd = toPort;
|
|
351
|
+
app.debug('closest to Port(pin):' + toPort);
|
|
352
|
+
const toBowBearing = geolib.getRhumbLineBearing(startLine.port, bow);
|
|
353
|
+
app.debug('toBowBearing:' + toBowBearing);
|
|
354
|
+
angle = toBowBearing - (startLine.bearing + 180) % 360;
|
|
355
|
+
} else {
|
|
356
|
+
toEnd = toStb;
|
|
357
|
+
app.debug('closest to Stb(boat):' + toStb);
|
|
358
|
+
const toBowBearing = geolib.getRhumbLineBearing(startLine.stb, bow);
|
|
359
|
+
app.debug('toBowBearing:' + toBowBearing);
|
|
360
|
+
angle = startLine.bearing - toBowBearing;
|
|
408
361
|
}
|
|
362
|
+
angle = ((angle + 180) % 360 + 360) % 360 - 180;
|
|
363
|
+
app.debug('angle:' + angle);
|
|
364
|
+
app.debug('toEnd:' + toEnd);
|
|
365
|
+
ocs = angle < 0;
|
|
366
|
+
app.debug('ocs:' + ocs);
|
|
367
|
+
|
|
368
|
+
let absAngle = Math.abs(angle);
|
|
369
|
+
const farFromLine = absAngle > 135;
|
|
370
|
+
app.debug('farFromLine:' + farFromLine);
|
|
371
|
+
const perpendicularToLine = toEnd * Math.sin(toRadians(absAngle));
|
|
372
|
+
app.debug('perpendicularToLine:' + perpendicularToLine);
|
|
373
|
+
let toLine = farFromLine ? Math.sqrt(toEnd * toEnd - perpendicularToLine * perpendicularToLine) : perpendicularToLine;
|
|
374
|
+
toLine = Math.round(10 * toLine) / 10;
|
|
375
|
+
app.debug('toLine:' + toLine);
|
|
376
|
+
const distanceToLine = ocs ? -toLine : toLine;
|
|
377
|
+
app.debug('distanceToLine:' + distanceToLine);
|
|
378
|
+
state.distanceToLine = distanceToLine;
|
|
379
|
+
sendDeltas([
|
|
380
|
+
{path: 'navigation.racing.distanceStartline', value: distanceToLine},
|
|
381
|
+
]);
|
|
382
|
+
}
|
|
383
|
+
else if (state.distanceToLine) {
|
|
384
|
+
state.distanceToLine = null;
|
|
385
|
+
sendDeltas([
|
|
386
|
+
{path: 'navigation.racing.distanceStartline', value: null},
|
|
387
|
+
]);
|
|
388
|
+
}
|
|
389
|
+
}
|
|
409
390
|
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
391
|
+
function processWind(twd) {
|
|
392
|
+
if (!twd) {
|
|
393
|
+
twd = app.getSelfPath('environment.wind.directionTrue');
|
|
394
|
+
app.debug('TWD:' + JSON.stringify(twd));
|
|
395
|
+
twd = twd ? twd.value : null;
|
|
396
|
+
}
|
|
416
397
|
|
|
417
|
-
|
|
418
|
-
|
|
398
|
+
if (!twd)
|
|
399
|
+
return;
|
|
419
400
|
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
401
|
+
let nextTwa = null;
|
|
402
|
+
const activeRoute = state.activeRoute;
|
|
403
|
+
if (activeRoute) {
|
|
404
|
+
const nextLegHeading = activeRoute.nextLegHeading;
|
|
405
|
+
if (nextLegHeading) {
|
|
406
|
+
nextTwa = (540 + toDegrees(twd) - nextLegHeading) % 360 - 180;
|
|
407
|
+
app.debug(`nextTwa: ${nextTwa}`);
|
|
408
|
+
}
|
|
409
|
+
}
|
|
429
410
|
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
411
|
+
let bias = null;
|
|
412
|
+
const startLine = state.startLine;
|
|
413
|
+
if (startLine && (!activeRoute || activeRoute.pointIndex < 2)) {
|
|
414
|
+
app.debug(`processWind ${twd} to ${JSON.stringify(startLine)}`);
|
|
415
|
+
bias = startLine.length * Math.cos(toRadians(startLine.bearing) - (twd + Math.PI));
|
|
416
|
+
}
|
|
436
417
|
|
|
418
|
+
sendDeltas([
|
|
419
|
+
{path: 'navigation.racing.stbLineBias', value: bias},
|
|
420
|
+
{path: 'navigation.racing.nextLegTrueWindAngle', value: toRadians(nextTwa)},
|
|
421
|
+
]);
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
function processRoute(activeRoute) {
|
|
425
|
+
if (activeRoute && (activeRoute.pointIndex + 1) < activeRoute.pointTotal) {
|
|
426
|
+
// e.g. {"href":"/resources/routes/a12eefe7-8fd3-49f7-81af-ce1f3440d3ff","name":"Course 1","reverse":false,"pointIndex":1,"pointTotal":5}
|
|
427
|
+
const routeId = activeRoute.href.split('/').pop(); // e.g. "a12eefe7-8fd3-49f7-81af-ce1f3440d3ff"
|
|
428
|
+
app.debug('processRoute:' + routeId);
|
|
429
|
+
app.resourcesApi.getResource('routes', routeId).then(route => {
|
|
430
|
+
app.debug('found active route:', JSON.stringify(route));
|
|
431
|
+
// e.g. {name:"Course 1",description:"Test race course",distance:3027,feature:{type:"Feature",geometry:{type:"LineString",coordinates:[[151.2789194160523,-33.80427227074185],[151.28230238234357,-33.813458268981435],[151.2758044350174,-33.80201284252691],[151.27864400784992,-33.801915364141514],[151.27982384226053,-33.80429179893497]]},properties:{coordinatesMeta:[{href:"/resources/waypoints/bb35d9d0-c04e-4721-89f2-a1d8e697ab81"},{href:"/resources/waypoints/65082ddd-6fa6-4538-9a01-49d5d795b0bb"},{href:"/resources/waypoints/0bbce508-67c6-4f03-8de0-fb0532f88917"},{href:"/resources/waypoints/602ee562-c6dc-42c3-a775-512597063ca4"},{href:"/resources/waypoints/2e3cf194-8835-498d-abaa-a5d2104550b3"}]},id:""},timestamp:"2025-06-09T22:22:54.739Z",$source:"resources-provider"}
|
|
432
|
+
const coordinates = route.feature.geometry.coordinates;
|
|
433
|
+
const [fromLongitude, fromLatitude] = coordinates[activeRoute.pointIndex];
|
|
434
|
+
const [toLongitude, toLatitude] = coordinates[activeRoute.pointIndex + 1];
|
|
435
|
+
app.debug(`from: {longitude:${fromLongitude}, latitude:${fromLatitude}}, to: {longitude:${toLongitude}, latitude:${toLatitude}}`);
|
|
436
|
+
const nextLegHeading = geolib.getRhumbLineBearing({
|
|
437
|
+
latitude: fromLatitude,
|
|
438
|
+
longitude: fromLongitude
|
|
439
|
+
}, {latitude: toLatitude, longitude: toLongitude});
|
|
440
|
+
app.debug(`nextLegHeading: ${nextLegHeading}`);
|
|
441
|
+
|
|
442
|
+
activeRoute.nextLegHeading = nextLegHeading;
|
|
443
|
+
state.activeRoute = activeRoute;
|
|
437
444
|
sendDeltas([
|
|
438
|
-
{path: 'navigation.racing.
|
|
439
|
-
{path: 'navigation.racing.nextLegTrueWindAngle', value: toRadians(nextTwa)},
|
|
445
|
+
{path: 'navigation.racing.nextLegHeading', value: toRadians(nextLegHeading)}
|
|
440
446
|
]);
|
|
441
|
-
}
|
|
447
|
+
}).catch(err => {
|
|
448
|
+
app.error(err);
|
|
449
|
+
});
|
|
450
|
+
} else {
|
|
451
|
+
state.activeRoute = null;
|
|
452
|
+
sendDeltas([
|
|
453
|
+
{path: 'navigation.racing.nextLegHeading', value: null}
|
|
454
|
+
]);
|
|
455
|
+
}
|
|
456
|
+
}
|
|
442
457
|
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
const coordinates = route.feature.geometry.coordinates;
|
|
452
|
-
const [fromLongitude, fromLatitude] = coordinates[activeRoute.pointIndex];
|
|
453
|
-
const [toLongitude, toLatitude] = coordinates[activeRoute.pointIndex + 1];
|
|
454
|
-
app.debug(`from: {longitude:${fromLongitude}, latitude:${fromLatitude}}, to: {longitude:${toLongitude}, latitude:${toLatitude}}`);
|
|
455
|
-
const nextLegHeading = geolib.getRhumbLineBearing({
|
|
456
|
-
latitude: fromLatitude,
|
|
457
|
-
longitude: fromLongitude
|
|
458
|
-
}, {latitude: toLatitude, longitude: toLongitude});
|
|
459
|
-
app.debug(`nextLegHeading: ${nextLegHeading}`);
|
|
460
|
-
|
|
461
|
-
activeRoute.nextLegHeading = nextLegHeading;
|
|
462
|
-
state.activeRoute = activeRoute;
|
|
463
|
-
sendDeltas([
|
|
464
|
-
{path: 'navigation.racing.nextLegHeading', value: toRadians(nextLegHeading)}
|
|
465
|
-
]);
|
|
466
|
-
}).catch(err => {
|
|
467
|
-
app.error(err);
|
|
468
|
-
});
|
|
469
|
-
} else {
|
|
470
|
-
state.activeRoute = null;
|
|
471
|
-
sendDeltas([
|
|
472
|
-
{path: 'navigation.racing.nextLegHeading', value: null}
|
|
473
|
-
]);
|
|
474
|
-
}
|
|
458
|
+
// Return the plugin
|
|
459
|
+
return {
|
|
460
|
+
id: 'signalk-racer',
|
|
461
|
+
name: 'SignalK Racing Plugin',
|
|
462
|
+
start: (options) => {
|
|
463
|
+
if (!options.startLinePort || !options.startLineStb) {
|
|
464
|
+
app.error('Missing waypoint names: startLinePort and/or startLineStb not configured.');
|
|
465
|
+
return;
|
|
475
466
|
}
|
|
467
|
+
app.debug('startLinePort:' + options.startLinePort);
|
|
468
|
+
app.debug('startLineStb:' + options.startLineStb);
|
|
469
|
+
app.debug('app.selfId:' + app.selfId);
|
|
470
|
+
|
|
471
|
+
let fromBow = app.getSelfPath('sensors.gps.fromBow');
|
|
472
|
+
let fromCenter = app.getSelfPath('sensors.gps.fromCenter');
|
|
473
|
+
state.options = options;
|
|
474
|
+
state.wayPointsScanned = false;
|
|
475
|
+
state.gpsFromBow = fromBow ? fromBow.value : null;
|
|
476
|
+
state.gpsFromCenter = fromCenter ? fromCenter.value : null;
|
|
476
477
|
|
|
477
478
|
// Subscribe to position updates.
|
|
478
479
|
app.subscriptionmanager.subscribe(
|
|
@@ -596,7 +597,7 @@ module.exports = (app) => {
|
|
|
596
597
|
if (route)
|
|
597
598
|
processRoute(route);
|
|
598
599
|
}
|
|
599
|
-
)
|
|
600
|
+
);
|
|
600
601
|
},
|
|
601
602
|
|
|
602
603
|
stop: () => {
|
|
@@ -609,13 +610,11 @@ module.exports = (app) => {
|
|
|
609
610
|
registerWithRouter: (router) => {
|
|
610
611
|
router.get('/startline/port', (req, res) => res.json(state.startLine ? state.startLine.port : null));
|
|
611
612
|
router.get('/startline/stb', (req, res) => res.json(state.startLine ? state.startLine.stb : null));
|
|
612
|
-
router.put('/startline/:end',
|
|
613
|
-
router.put('/timeToStart',
|
|
613
|
+
router.put('/startline/:end', setStartLineEnd);
|
|
614
|
+
router.put('/timeToStart', setTimeToStart);
|
|
614
615
|
},
|
|
615
616
|
|
|
616
617
|
getOpenApi: () => require('./openapi.json'),
|
|
617
618
|
};
|
|
618
|
-
|
|
619
|
-
return plugin;
|
|
620
619
|
};
|
|
621
620
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "signalk-racer",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.5",
|
|
4
4
|
"description": "Signalk plugin to calculate values of interest to sail racers: such as Distance to Line; Next leg TWA.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"scripts": {
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
],
|
|
18
18
|
"signalk-plugin-enabled-by-default": false,
|
|
19
19
|
"signalk": {
|
|
20
|
-
"appIcon": "
|
|
20
|
+
"appIcon": "assets/signalk-racer-72x72.png",
|
|
21
21
|
"displayName": "Signalk Racer WebApp"
|
|
22
22
|
},
|
|
23
23
|
"author": "gregw@mortbay.com",
|
/package/{README.MD → README.md}
RENAMED
|
File without changes
|
|
File without changes
|