Skip to content

Instantly share code, notes, and snippets.

@SkySails
Last active June 3, 2025 14:20
Show Gist options
  • Save SkySails/264c2b51ee567c48ed1087cfd92ccc21 to your computer and use it in GitHub Desktop.
Save SkySails/264c2b51ee567c48ed1087cfd92ccc21 to your computer and use it in GitHub Desktop.
// Main Features:
// - Manual fix: Draws a line from a clicked position at a specified bearing and distance, with a distance-dependent multiplier for realism.
// Optionally, draws a 90-degree arc (distance ring) centered on the bearing axis.
// - Fix: Marks a position with a timestamp and formatted latitude/longitude.
// - Context menu setup: Ensures the custom context menu items are available after each action.
//
// Utility functions:
// - formatLatLon: Formats latitude/longitude in degrees and decimal minutes (DMM) with directional suffixes.
// - setupContextMenu: Registers the context menu items.
//
// Integration:
// - Uses OCPN* API functions for map interaction, route/waypoint creation, and context menu management.
// - Parks the console on script load for debugging.
// Handle updates
version = 1.2;
require("checkForUpdate")("ManualFix", version, 0,"https://gist.githubusercontent.com/SkySails/264c2b51ee567c48ed1087cfd92ccc21/raw/version.json"); // prettier-ignore
// ------------------------------------------------------------------------
const manualFixMenuName = "Manual fix";
OCPNonContextMenu(drawBearing, manualFixMenuName);
var clickedPosition; // The last position to be right-clicked to trigger the context menu
/**
* Handles the drawing of a bearing from a given location.
* Opens a dialogue for user input (bearing and range), and sets up a context menu.
*
* @param {Object} location - The location object representing the clicked position.
*/
function drawBearing(location) {
clickedPosition = location;
onDialogue(handleDialogue, [
{ type: "field", label: "BRG" },
{ type: "field", label: "RNG" },
{ type: "button", label: ["*OK"] },
]);
setupContextMenu();
}
/**
* Handles the user dialogue input to create a route and optionally a distance ring.
*
* - If neither bearing nor distance is provided, does nothing.
* - Calculates a new position from a clicked position using the provided bearing and distance.
* - Adds a route (bearing line) from the clicked position to the new position.
* - If distance is provided, also creates a 90-degree arc (distance ring) centered on the bearing axis.
* - If only distance is provided, creates a distance ring around the clicked position at the specified distance
*/
function handleDialogue(dialogue) {
const isBearingProvided = dialogue[0].value && dialogue[0].value.trim() !== "";
const isDistanceProvided = dialogue[1].value && dialogue[1].value.trim() !== "";
// No input provided, exit early (do nothing)
if (!isBearingProvided && !isDistanceProvided) return;
const bearing = isBearingProvided ? (parseFloat(dialogue[0].value) + 180) % 360 : null;
const distance = isDistanceProvided ? parseFloat(dialogue[1].value) : null;
if (isBearingProvided) {
// Default distance to 50NM if not provided
const distanceWithFallback = distance || 50;
// Apply distance multiplier (extending the line to create an intersecting point)
const vectorLength = distanceWithFallback * getDistanceMultiplier(distanceWithFallback);
// Calculate the terminal point of the bearing line using the clicked position and a vector (bearing and distance)
const terminalPoint = OCPNgetPositionPV(clickedPosition, { bearing, distance: vectorLength });
// Create a route (bearing line) from the clicked position to the new position
const bearingLine = {
name: "Bearing vector line",
isActive: false,
isVisible: true,
waypoints: [
{ position: clickedPosition, GUID: OCPNgetNewGUID(), isVisible: true, iconName: "empty" },
{ position: terminalPoint, GUID: OCPNgetNewGUID(), isVisible: true, iconName: "empty" },
],
};
// Add the bearing line to the map
OCPNaddRoute(bearingLine);
}
if (isDistanceProvided) {
// Create waypoints for a full circle if no bearing, or 40-degree arc if bearing exists
const numPoints = bearing === null ? 72 : 19; // 5 degree steps (72 for circle, 19 for arc)
const arcWaypoints = [];
// The amount of degrees, in total, to draw when bearing is provided
const arcDegrees = 90;
const arcStart = bearing === null ? 0 : bearing - arcDegrees / 2;
const arcEnd = bearing === null ? 360 : bearing + arcDegrees / 2;
for (var i = 0; i < numPoints; i++) {
const angle = arcStart + ((arcEnd - arcStart) * i) / (numPoints - 1);
const pos = OCPNgetPositionPV(clickedPosition, { bearing: (angle + 360) % 360, distance });
arcWaypoints.push({
position: pos,
markName: "",
GUID: OCPNgetNewGUID(),
description: "",
isVisible: true,
iconName: "empty",
iconDescription: "Empty",
isNameVisible: false,
isFreeStanding: false,
isActive: false,
hyperlinkList: [],
});
}
const distanceRing = {
name: "Distance ring",
from: "",
to: "",
description: "Distance ring at " + distance + "m",
isActive: false,
isVisible: true,
color: "#eb4034",
waypoints: arcWaypoints,
};
OCPNaddRoute(distanceRing);
}
}
// ------------------------------------------------------------------------
// "Fix" menu
const fixMenuName = "Fix";
OCPNonContextMenu(drawPositionFix, fixMenuName);
/**
* Draws a position fix waypoint on the map at the specified location.
* Formats the latitude and longitude in degrees and decimal minutes (DMM) with directional indicators.
* The waypoint is labeled with the current time and formatted coordinates.
*
* @param {Object} location - The geographic location for the waypoint.
* @param {number} location.latitude - The latitude of the position.
* @param {number} location.longitude - The longitude of the position.
*/
function drawPositionFix(location) {
waypoint = {
position: location,
markName: new Date().toTimeString().split(".")[0] + "\n" + formatLatLon(location.latitude, location.longitude),
isNameVisible: true,
iconName: "Symbol-X-Large-Black",
};
OCPNaddSingleWaypoint(waypoint);
setupContextMenu();
}
// ------------------------------------------------------------------------
// Utilities
function getDistanceMultiplier(distance) {
if (distance < 5) {
return 2;
} else if (distance > 10) {
return 1.1;
} else {
// Smooth curve between 2 (at 5NM) and 1.1 (at 10NM)
// Linear interpolation: multiplier = m1 + (m2 - m1) * ((distance - d1) / (d2 - d1))
const d1 = 5,
d2 = 10,
m1 = 2,
m2 = 1.1;
return m1 + (m2 - m1) * ((distance - d1) / (d2 - d1));
}
}
function formatLatLon(lat, lon) {
function toDMM(deg, isLat) {
var abs = Math.abs(deg);
var d = Math.floor(abs);
var m = ((abs - d) * 60).toFixed(1);
var dir = isLat ? (deg >= 0 ? "N" : "S") : deg >= 0 ? "E" : "W";
var degStr;
if (isLat) {
degStr = d < 10 ? "0" + d : "" + d;
} else {
if (d < 10) degStr = "00" + d;
else if (d < 100) degStr = "0" + d;
else degStr = "" + d;
}
return degStr + "* " + m + "' " + dir;
}
return toDMM(lat, true) + " " + toDMM(lon, false);
}
function setupContextMenu() {
OCPNonContextMenu();
OCPNonContextMenu(drawBearing, manualFixMenuName);
OCPNonContextMenu(drawPositionFix, fixMenuName);
}
// Park console as soon as script is run
consolePark();
{
"name": "ManualFix",
"version": 1.2,
"date": "3 Jun 2025",
"script": "https://gist.githubusercontent.com/SkySails/264c2b51ee567c48ed1087cfd92ccc21/raw/opencpn-manual-fix.js",
"new": "Added full distance ring visualization when only range is provided as input",
"pluginVersion": 3.1
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment