Skip to content

Instantly share code, notes, and snippets.

@LogIN-
Last active January 28, 2024 20:12
Show Gist options
  • Save LogIN-/c7d3d7c537d46269d7e100d424dddfce to your computer and use it in GitHub Desktop.
Save LogIN-/c7d3d7c537d46269d7e100d424dddfce to your computer and use it in GitHub Desktop.
A comprehensive JavaScript class for evaluating sailing boat performance and safety. This class, `SailingBoatMetrics`, includes methods to calculate essential metrics such as the Capsize Screening Formula (CSF), Angle of Vanishing Stability (AVS), Displacement to Length Ratio (D/L), Sail Area to Displacement Ratio (SA/D), Ballast Ratio, Length t…
/**
* Class representing the metrics of a sailing boat.
*
* This class provides functionalities to calculate various metrics relevant to a sailing boat's
* performance and safety. It includes methods to compute ratios and coefficients such as the
* Capsize Screening Formula (CSF), Displacement to Length Ratio (D/L), and others.
* It also allows for normalization and scoring of these metrics for a comprehensive evaluation.
*
* Measurements can be input in either metric or imperial units, with conversion capabilities
* included. The class provides a scoring system to evaluate the overall performance and safety
* of the boat based on its characteristics.
*
* Example metrics calculated:
* - Capsize Screening Formula (CSF)
* - Displacement to Length Ratio (D/L)
* - Sail Area to Displacement Ratio (SA/D)
* - Ballast Ratio
* - Length to Beam Ratio (LBR)
* - Prismatic Coefficient
* - Block Coefficient
*
* The class also includes a method to calculate the overall score of the sailboat based on these metrics,
* which can be used to gauge the boat's suitability for specific conditions or purposes.
*
* Usage:
* - Instantiate the class with the boat's specifications.
* - Call the `calculateScore()` method to evaluate the boat's performance and safety.
*/
class SailingBoatMetrics {
/**
* Constructs an instance of the SailingBoatMetrics class.
* @param {Object} options - Configuration object for the sailing boat.
* @param {number} options.displacement - The displacement of the boat.
* @param {number} options.beam - The beam (width) of the boat.
* @param {number} options.LWL - The Length at Water Line of the boat.
* @param {number} options.sailArea - The sail area of the boat.
* @param {number} options.ballastWeight - The ballast weight of the boat.
* @param {number} options.LOA - The Length Overall of the boat.
* @param {number} options.midshipArea - The midship area of the boat.
* @param {number} options.draft - The draft of the boat.
* @param {number} options.AVS - The Angle of Vanishing Stability (optional).
* @param {boolean} options.isMetric - Flag to indicate if the measurements are in metric units.
*/
constructor(options, boatType = 'liveaboard') {
this.displacement = options.displacement;
this.beam = options.beam;
this.LWL = options.LWL;
this.sailArea = options.sailArea;
this.ballastWeight = options.ballastWeight;
this.LOA = options.LOA;
this.midshipArea = options.midshipArea;
this.draft = options.draft;
this.AVS = options.AVS || 0;
this.isMetric = options.isMetric || false;
this.boatType = boatType;
this.validateInputs();
this.convertToImperialIfNeeded();
}
/**
* Validates the input properties. Throws an error if any property is invalid.
*/
validateInputs() {
const properties = ['displacement', 'beam', 'LWL', 'sailArea', 'LOA', 'draft'];
properties.forEach(prop => {
if (typeof this[prop] !== 'number' || this[prop] <= 0) {
throw new Error(`Invalid value for ${prop}`);
}
});
if (this.ballastWeight !== undefined && (typeof this.ballastWeight !== 'number' || this.ballastWeight < 0)) {
throw new Error("Invalid value for ballastWeight");
}
if (this.midshipArea !== undefined && (typeof this.midshipArea !== 'number' || this.midshipArea <= 0)) {
throw new Error("Invalid value for midshipArea");
}
}
/**
* Validates the input properties. Throws an error if any property is invalid.
*/
convertToImperialIfNeeded() {
if (this.isMetric) {
this.displacement *= 2.20462;
this.beam *= 3.28084;
this.LWL *= 3.28084;
this.sailArea *= 10.7639;
this.ballastWeight *= 2.20462;
this.LOA *= 3.28084;
this.midshipArea *= 10.7639;
this.draft *= 3.28084;
}
}
/**
* Calculates the Capsize Screening Formula (CSF).
* @return {number} The CSF score.
*/
capsizeScreeningFormula() {
return this.beam / Math.cbrt(this.displacement);
}
/**
* Sets the Angle of Vanishing Stability.
* @param {number} angle - The angle to set for AVS.
*/
setAngleOfVanishingStability(angle) {
if (typeof angle !== 'number' || angle < 0) {
throw new Error("Invalid value for angle");
}
this.AVS = angle;
}
/**
* Calculates the Displacement to Length Ratio (D/L).
* @return {number} The D/L ratio.
*/
displacementToLengthRatio() {
return (this.displacement * 2240) / (0.01 * Math.pow(this.LWL, 3));
}
/**
* Calculates the Sail Area to Displacement Ratio (SA/D).
* @return {number} The SA/D ratio.
*/
sailAreaToDisplacementRatio() {
return this.sailArea / Math.cbrt(Math.pow(this.displacement, 1 / 35.3147));
}
/**
* Calculates the Ballast Ratio.
* @return {number|undefined} The Ballast Ratio, or undefined if ballastWeight is not set.
*/
ballastRatio() {
if (this.ballastWeight === undefined) {
return undefined;
}
return this.ballastWeight / this.displacement;
}
/**
* Calculates the Length to Beam Ratio (LBR).
* @return {number} The LBR score.
*/
lengthToBeamRatio() {
return this.LOA / this.beam;
}
/**
* Calculates the Prismatic Coefficient.
* @return {number|undefined} The Prismatic Coefficient, or undefined if midshipArea is not set.
*/
prismaticCoefficient() {
if (this.midshipArea === undefined) {
return undefined;
}
return this.displacement / (this.LWL * this.midshipArea * this.LWL);
}
/**
* Calculates the Block Coefficient.
* @return {number} The Block Coefficient score.
*/
blockCoefficient() {
return this.displacement / (this.LWL * this.beam * this.draft);
}
/**
* Calculates an estimated Living Space Index (LSI) based on boat dimensions.
* @return {number} Estimated LSI score.
*/
calculateLivingSpaceIndex() {
// Simplistic calculation: Use a combination of LOA, Beam, and Draft
return this.LOA * this.beam * this.draft;
}
/**
* Normalizes the Capsize Screening Formula score.
* @param {number} value - The CSF value to normalize.
* @return {number} The normalized CSF score.
*/
normalizeCSF(value) {
// Adjusted range for Capsize Screening Formula
return this.isValidNumber(value) ? this.normalizeValue(value, 1.7, 2.0) : 5.5;
}
normalizeAVS(value) {
// Adjusted range for Angle of Vanishing Stability
return this.isValidNumber(value) ? this.normalizeValue(value, 130, 150) : 5.5;
}
normalizeDL(value) {
// Adjusted range for Displacement to Length Ratio
return this.isValidNumber(value) ? this.normalizeValue(value, 150, 300) : 5.5;
}
normalizeSAD(value) {
// Adjusted range for Sail Area to Displacement Ratio
return this.isValidNumber(value) ? this.normalizeValue(value, 16, 24) : 5.5;
}
normalizeBR(value) {
// Adjusted range for Ballast Ratio
return this.isValidNumber(value) ? this.normalizeValue(value * 100, 35, 55) : 5.5;
}
normalizeLBR(value) {
// Adjusted range for Length to Beam Ratio
return this.isValidNumber(value) ? this.normalizeValue(value, 2.7, 3.5) : 5.5;
}
normalizeCP(value) {
// Adjusted range for Prismatic Coefficient
return this.isValidNumber(value) ? this.normalizeValue(value, 0.53, 0.63) : 5.5;
}
normalizeCB(value) {
// Adjusted range for Block Coefficient
return this.isValidNumber(value) ? this.normalizeValue(value, 0.33, 0.43) : 5.5;
}
normalizeLSI(value, lsiMin, lsiMax) {
// Normalize the Living Space Index (LSI)
return this.isValidNumber(value) ? this.normalizeValue(value, lsiMin, lsiMax) : 5.5;
}
/**
* Calculates the overall score of the sailboat based on various metrics.
* @return {number} The calculated score.
*/
calculateScore() {
// Define LSI min and max for a couple
const lsiMin = 100; // Represents modest living space
const lsiMax = 300; // Represents ideal living space
// Define default weights
let weights = this.getDefaultWeights();
// Adjust weights based on boat type
switch (this.boatType) {
case 'racer':
weights = this.getRacerWeights();
break;
case 'cruiser':
weights = this.getCruiserWeights();
break;
case 'liveaboard':
weights = this.getLiveaboardCruiserWeights();
weights.lsi = 0.2; // Assign significant weight to living space for liveaboards
break;
// Add more cases for other boat types
default:
// Keep default weights
break;
}
// Include LSI in the score calculation for liveaboards
const lsiScore = this.boatType === 'liveaboard' ? this.normalizeLSI(this.calculateLivingSpaceIndex(), lsiMin, lsiMax) : 0;
// Calculate the total score based on the adjusted weights
const totalScore = (this.normalizeCSF(this.capsizeScreeningFormula()) * weights.csf +
this.normalizeAVS(this.AVS) * weights.avs +
this.normalizeDL(this.displacementToLengthRatio()) * weights.dl +
this.normalizeSAD(this.sailAreaToDisplacementRatio()) * weights.sad +
this.normalizeBR(this.ballastRatio()) * weights.br +
this.normalizeLBR(this.lengthToBeamRatio()) * weights.lbr +
this.normalizeCP(this.prismaticCoefficient()) * weights.cp +
this.normalizeCB(this.blockCoefficient()) * weights.cb +
lsiScore * weights.lsi) /
(Object.values(weights).reduce((a, b) => a + b) + (this.boatType === 'liveaboard' ? weights.lsi : 0)); // Normalize the total by the sum of weights
return totalScore;
}
/**
* Normalizes a given value based on a specified range.
* @param {number} value - The value to normalize.
* @param {number} min - The minimum of the range.
* @param {number} max - The maximum of the range.
* @return {number} The normalized value.
*/
normalizeValue(value, min, max) {
if (value <= min) return 10;
if (value >= max) return 1;
return 10 - ((value - min) / (max - min) * 9);
}
/**
* Checks if a value is a valid number.
* @param {number} value - The value to check.
* @return {boolean} True if the value is a valid number, false otherwise.
*/
isValidNumber(value) {
return typeof value === 'number' && !isNaN(value);
}
// Helper methods to get weights for different boat types
getDefaultWeights() {
return {
csf: 0.15,
avs: 0.15,
dl: 0.15,
sad: 0.15,
br: 0.1,
lbr: 0.1,
cp: 0.1,
cb: 0.1
};
}
getRacerWeights() {
return {
csf: 0.1,
avs: 0.1,
dl: 0.1,
sad: 0.25, // Higher importance for Sail Area to Displacement
br: 0.05,
lbr: 0.15, // Higher importance for Length to Beam Ratio
cp: 0.15,
cb: 0.1
};
}
getCruiserWeights() {
return {
csf: 0.2,
avs: 0.2,
dl: 0.15,
sad: 0.1,
br: 0.1,
lbr: 0.05,
cp: 0.1,
cb: 0.1
};
}
getLiveaboardCruiserWeights() {
return {
csf: 0.2, // Stability is crucial for liveaboards
avs: 0.2, // High priority for safety in various sea conditions
dl: 0.15, // Displacement to Length is important for comfort in waves
sad: 0.1, // Sail Area to Displacement is less crucial for liveaboards
br: 0.1, // Adequate ballast for stability
lbr: 0.05, // Length to Beam Ratio is less critical
cp: 0.1, // Prismatic Coefficient for efficient hull shape
cb: 0.1 // Block Coefficient for seaworthiness
}
}
/**
* Example usage of the SailingBoatMetrics class.
*/
static exampleUsage() {
let options = {
displacement: 40786, // in pounds
beam: 14.76, // in feet
LWL: 43.47, // in feet
sailArea: 1328.27, // in square feet
ballastWeight: 17086, // in pounds
LOA: 49.18, // in feet
midshipArea: undefined, // This value is not provided, so we set it to undefined
draft: 7.71, // in feet
AVS: undefined, // Angle of Vanishing Stability is not provided
isMetric: false // Measurements are in imperial units
};
let sailingBoat = new SailingBoatMetrics(options);
let score = sailingBoat.calculateScore();
console.log("Hallberg-Rassy 48 Sailboat Score:", score);
}
}
SailingBoatMetrics.exampleUsage();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment