Last active
June 3, 2025 14:20
-
-
Save SkySails/264c2b51ee567c48ed1087cfd92ccc21 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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(); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
{ | |
"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