Skip to content

Instantly share code, notes, and snippets.

@antipole2
Last active September 27, 2021 11:05
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save antipole2/62bda896741229369783c67fb4b4c064 to your computer and use it in GitHub Desktop.
Save antipole2/62bda896741229369783c67fb4b4c064 to your computer and use it in GitHub Desktop.
// Tack advisor v0.2.1 27 Sep 2021
// Since v0.1 added down-wind tacking and onExit clean-up
// v0.2.1 removes degree character from comments in three places as Windows objects
// configuration
repeatInterval = 5; // seconds
// wind angles we will work at
minWindAngle = 10; // Min wind angle to work with
maxWindAngle = 60; // Max angle off wind we will work with
maxRunWindAngle = 179; // Don't work if virtually straight down wind
autoHide = false
displayDialogue = true;
debug = false;
routeName = "tackRoute"; // name of route to be created
// Enduring variables
var routeGUID = false;
var lastPosString = false;
var windDirection = -1; // < 0 until wind received
var windSpeed;
var tackString; // the text summarising tack legs
routeDisplayDialog = [
{type:"text", value:"", style:{font:"courier"}},
{type:"caption", value:"Tack advisor"},
{type:"button", label:["Stop", "Hide"]}
];
Position = require("Position");
Waypoint = require("Waypoint");
Route = require("Route");
hereWaypoint = new Waypoint;
hereWaypoint.markName = "From";
targetWaypoint = new Waypoint;
targetWaypoint.markName = "To";
tackWaypoint = new Waypoint;
tackWaypoint.markName = "Tack";
tackRoute = new Route;
tackRoute.name = "Tack route";
tackRoute.waypoints[0] = hereWaypoint;
tackRoute.waypoints[1] = tackWaypoint;
tackRoute.waypoints[2] = targetWaypoint;
// start listening out for wind data
windRetry = windRetryMax =10; // times to wait for wind data
OCPNonNMEAsentence(processNMEA);
// check if we need to delete an old route
OCPNonMessageName(handleRL, "OCPN_ROUTELIST_RESPONSE");
OCPNsendMessage("OCPN_ROUTELIST_REQUEST", JSON.stringify({"mode": "Not track"}));
consoleHide();
onExit(cleanUp); // Make sure we cclean up after ourself
function handleRL(routeListJS){ // receive list of routes in JSON
var i;
routeList = JSON.parse(routeListJS);
if (routeList != null) {
if (routeList.length > 1){ // (changed from 0 because null entry when no routes)
if (!routeList[0]) routeList.shift(); // drop first null entry - don't know why it is there ''
}
count = routeList.length; // number of existing routes
for (i = 0; i < count; i++){
if (routeList[i].name == routeName){
routeGUID = routeList[i].GUID;
break;
}
}
if (!routeGUID) routeGUID = OCPNgetNewGUID();
tackRoute.GUID =routeGUID;
tackRoute.name = routeName;
}
onSeconds(update, repeatInterval); // start the update cycle
}
function processNMEA(input){
if (input.OK){
sentence = input.value;
if (sentence.slice(3,6) == "MWV"){
splut = sentence.split(",");
if (splut[5] == "A"){ // valid data
if (splut[2] != "T") throw("Code written for true wind direction only");
windDirection = splut[1]; windSpeed = splut[3];
}
}
}
OCPNonNMEAsentence(processNMEA); // listen for next sentence
return;
}
function update(){
// real work is done in updateWorks which returns true if advisor active, else false
// this simplifies breaking out of an update while still scheduling the next one
active = updateWorks();
onDialogue(false); // clear any existing dialogue
if (displayDialogue){
if (active) {
routeDisplayDialog[0].value = tackString;
onDialogue(dismissDisplay, routeDisplayDialog);
}
else if (!autoHide){
routeDisplayDialog[0].value = "On standby";
onDialogue(dismissDisplay, routeDisplayDialog);
}
}
onSeconds(update, repeatInterval);
if (!active){
try { OCPNdeleteRoute(tackRoute.GUID);}
catch(err){}
}
}
function updateWorks(){
// returns true if showing tack, else false
if (windDirection < 0){ // cannot start yet
if (windRetry-- > 0) return false;
alert(false);
alert("Not found valid wind data");
windRetry = windRetryMax;
return false;
}
else alert(false);
APRgpx = OCPNgetARPgpx(); // get Active Route Point as GPX
if (APRgpx.length == 0) return false; // no active waypoint
// we have an Active Route Waypoint - extract its name
// print(APRgpx, "\n");
posString = /<wpt lat=\".*\"/.exec(APRgpx);
if (posString != lastPosString){ // position of target is new
lat = 1 * posString[0].slice(10,22); // force to number
lon = 1 * posString[0].slice(29,-3);
targetWaypoint.position.latitude = lat;
targetWaypoint.position.longitude = lon;
lastPosString = posString;
}
here = OCPNgetNavigation();
var angleToWind; // angle of wind from course made good
// NB if windAngle is positive, wind is on starboard side
windAngle = angle(here.COG, windDirection); // angle off wind
absWindAngle = Math.abs(windAngle);
if (absWindAngle < minWindAngle){
if (debug) print("Head to wind\n");
return false;
}
if (debug) print("Initial wind angle:", windAngle, "\n");
if (absWindAngle > 90){
// we are running
running = true;
windDirection = angle(180, windDirection); // use reciprocal windDrection if running
windAngle = angle(here.COG, windDirection); // angle off wind
absWindAngle = Math.abs(windAngle);
if (debug) print("Running\twindDirection:", windDirection, "\t windAngle:", windAngle, "\n");
if (absWindAngle <= 1) return; // don't bother if dead run
if (absWindAngle > maxRunWindAngle) {
if (debug) print("Dead run\n");
return false;
}
}
else { // on the wind
running = false;
if (absWindAngle > maxWindAngle){
if (debug) print("Reaching\n");
return false; // too close to wind
}
}
if (debug) print("COG:", here.COG, "\twindDirction:",
windDirection, "\twindAngle:", windAngle, "\tRunning:", running, "\n");
var vectorToWpt; // vector from here to target waypoint
var oppositeTack; // bearing if we tack
var alpha; // angle between course to waypoint and CMG (+ve clockwise)
var beta; //inside angle of tack (180 - change of course)
var gamma; // inside angle at target waypoint between direct course and arriving from tack point
var a, b, c; // length of passages opposite altha, bete & gamma respectively
// trigonometry: a/sin(alpha) = b/sin(beta) = c/sin(gamma)
hereWaypoint.position = here.position;
vectorToWpt = OCPNgetVectorPP(hereWaypoint.position, targetWaypoint.position);
if (debug) print("Bearing to waypoint:", vectorToWpt.bearing, "\n");
alpha = angle(vectorToWpt.bearing, here.COG); // is angle between bearing to WP and our COG
if (Math.abs(alpha) > 90){
// making away from mark
if (debug) print("Making away from mark\n");
return false;
}
oppositeTack = bearing(here.COG, 2 * windAngle);
angleOppositeTackToWpt = angle(vectorToWpt.bearing, oppositeTack);
// angleOppositeTackToWpt is angle between bearing to WP and our COG if we had tacked
// if this has same sign as alpha, tacking is not relevant/needed
if (Math.sign(angleOppositeTackToWpt) == Math.sign(alpha)){
if (debug) print("Tacking not relevant\n");
return false;
}
if (debug) print("Tacking relevant\n");
if (running){ // running - things work differently
alpha = -alpha;
angleOppositeTackToWpt = -angleOppositeTackToWpt;
if (debug) print("Wind angle:", windAngle, "\tRunning - opposite tack is:", oppositeTack, "\n");
}
else if (debug) print("vectorToWpt.bearing:", vectorToWpt.bearing, "\talpha:", alpha, "\toppositeTack:", oppositeTack, "\n");
// now for the trigonometry
beta = angle(here.COG, oppositeTack);
if (running) {
beta = angle(180, beta);
if (Math.abs(beta) < 2){
if (debug) print("Dead run\n");
return false;
}
}
b = vectorToWpt.distance;
alpha = Math.abs(alpha); beta = Math.abs(beta);
if (debug) print("angleOppositeTackToWpt:", angleOppositeTackToWpt, "\tbeta:", beta, "\tb:", b, "\talpha:", alpha, "\n");
// when at tack point...
a = b*Math.sin(alpha*Math.PI/180)/Math.sin(beta*Math.PI/180); // distance from tack point to waypoint
gamma = Math.abs(180 - alpha - beta); // angle between bearing to waypoint and bearing from tackpoint to waypoint
bearingTpToWp = bearing(windDirection, windAngle);
if (running) bearingBackToTp = bearing(180, bearingTpToWp);// reciprocal
else bearingBackToTp = bearing(bearingTpToWp, 180);// reciprocal
vectorBackToTp = {distance: a, bearing: bearingBackToTp};
if (debug) print("gamma:", gamma, "\tbearingTpToWp:", bearingTpToWp, "\tbearingBackToTp:", bearingBackToTp,
"\nvectorBackToTp:", vectorBackToTp, "\n")
tackPosition = OCPNgetPositionPV(targetWaypoint.position, vectorBackToTp);
tackWaypoint.position.latitude = tackPosition.latitude;
tackWaypoint.position.longitude = tackPosition.longitude;
// now to create/update the route - not sure if we have one already or are updating
try { OCPNupdateRoute(tackRoute); }
catch (err) {OCPNaddRoute(tackRoute);}
// now to display the outcome
if (displayDialogue){
vectorToTp = OCPNgetVectorPP(here.position, tackPosition);
c = vectorToTp.distance;
SOG = (here.SOG)*1;
onDialogue(false); // clear any existing
tackString = "Leg 1: " + (" " + c.toFixed(1)).slice(-4) + "nm " + (" " +(c/SOG*60).toFixed(1)).slice(-4) + "mins\n" +
"Leg 2: " + (" " + a.toFixed(1)).slice(-4) + "nm " + (" " +(a/SOG*60).toFixed(1)).slice(-4) + "mins\n" +
"Total: " + (" " + (c+a).toFixed(1)).slice(-4) + "nm " + (" " +((c+a)/SOG*60).toFixed(1)).slice(-4) + "mins\n";
}
return true;
}
function angle(bearing1, bearing2){
// returns angle between two bearings
// result is +ve if bearing2 up to 180 clockwise of bearing1
// result is -ve if bearing2 up to 179.99 anticlockwise of bearing1
bearing2 = bearing2*1 + 360; // ensure next result is +ve
diff = (bearing2 - bearing1)%360;
if (diff > 180) diff -= 360;
return diff;
}
function bearing(bearing, angle){
// given a bearing, returns the bearing changed by angle
newBearing = 1*bearing + angle;
if (newBearing < 0) newBearing += 360;
newBearing = newBearing % 360
return newBearing;
}
function dismissDisplay(dialogue){
if (dialogue[dialogue.length-1].label == "Stop") {
try { OCPNdeleteRoute(tackRoute.GUID);}
catch(err){}
stopScript("Script was stopped");
}
else displayDialogue = false;
}
function updateSimulator(dialogue){
button = dialogue[dialogue.length-1].label;
if (button == "Stop") {
try { OCPNdeleteRoute(tackRoute.GUID);}
catch(err){}
stopScript("Script was stopped");
}
simWind = dialogue[3].value;
simCMG = dialogue[4].value;
simSOG = dialogue[5].value;
update();
}
function cleanUp(){ // cleanup any left-over route
try { OCPNdeleteRoute(tackRoute.GUID);}
catch(err){}
}
@antipole2
Copy link
Author

Degree symbol removed from comment lines in three places so it runs on Windows

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment