@canterbury-air-patrol/marine-search-area-coverage
Version:
Marine Search Area Coverage Calculator
370 lines • 17.2 kB
JavaScript
// marine-search-area-coverage.tsx
import { marine_sweep_widths as marineSweepWidths, marine_sweep_width_weather_corrections as weatherCorrections } from "@canterbury-air-patrol/marine-sweep-width-data";
import React from "react";
import Table from "react-bootstrap/Table";
var MarineSAC = class {
constructor(column, assetType) {
this.column = column;
this.asset_type = assetType;
this.asset_speed = 0;
this.wu = 0;
this.speed_correction = 1;
this.fw = 0;
this.sweep_width = 0;
this.fatigue = false;
this.corrected_sweep_width = 0;
this.practical_track_spacing = 0;
this.search_area = 144;
this.search_hours = 0;
this.available_search_hours = 0;
this.modified_area = 0;
this.whole_area_practical_track_spacing = 0;
}
recaclculate() {
this.sweep_width = this.wu * this.speed_correction * this.fw;
if (this.fatigue) {
this.corrected_sweep_width = this.sweep_width * 0.9;
} else {
this.corrected_sweep_width = this.sweep_width;
}
this.search_hours = this.search_area / (this.asset_speed * this.practical_track_spacing);
this.modified_area = this.practical_track_spacing * this.asset_speed * this.available_search_hours;
this.whole_area_practical_track_spacing = this.search_area / (this.available_search_hours * this.asset_speed);
}
};
var tableRows = [
{
display_name: "Uncorrected Sweep Width (Wu) `NM`",
column_name: "wu"
},
{
display_name: "Speed Correction Factor (Sc)",
column_name: "speed_correction"
},
{
display_name: "Weather Corrected Factor (Fw)",
column_name: "fw"
},
{
display_name: "Sweep Width (W) (Wu x Sc x Fw)",
column_name: "sweep_width"
},
{
display_name: "Fatigue Factor (Ff)",
column_name: "fatigue"
},
{
display_name: "Corrected Sweep Width (W X Ff)",
column_name: "corrected_sweep_width"
},
{
display_name: "Practical Track Spacing `NM`",
column_name: "practicalTrackSpacing",
input: true,
input_type: "number"
},
{
display_name: "Search Area",
column_name: "search_area",
input: true,
input_type: "number"
},
{
display_name: "Search Hours (T) Total",
column_name: "search_hours"
},
{
display_name: "Available Search Hours",
column_name: "availableSearchHours",
input: true,
input_type: "number"
},
{
display_name: "Modified Area at Practical Spacing in Available Hours",
column_name: "modified_area"
},
{
display_name: "Track Spacing for Whole Area in Available Time",
column_name: "whole_area_practical_track_spacing"
}
];
var WeatherDataTable = class extends React.Component {
constructor(props) {
super(props);
this.windSpeed = 0;
this.seaHeight = 0;
this.updateWeatherImpact();
this.handleChange = this.handleChange.bind(this);
}
updateWeatherImpact() {
let weatherImpact = "low";
if (this.windSpeed >= 25 || this.seaHeight >= 1.5) {
weatherImpact = "high";
} else if (this.windSpeed >= 15 || this.seaHeight >= 1) {
weatherImpact = "medium";
}
this.props.weatherImpactChange(weatherImpact);
}
handleChange(event) {
const target = event.target;
const value = target.value;
const name = target.name;
if (name === "met_visibility") {
this.props.weatherVisibilityChange(parseFloat(value));
} else {
if (name === "wind_speed") {
this.windSpeed = parseFloat(value);
} else if (name === "sea_height") {
this.seaHeight = parseFloat(value);
}
this.updateWeatherImpact();
}
}
render() {
return /* @__PURE__ */ React.createElement("tbody", null, /* @__PURE__ */ React.createElement("tr", null, /* @__PURE__ */ React.createElement("td", null, /* @__PURE__ */ React.createElement("label", { htmlFor: "met_visibility" }, "Meteorological Visibility (NM)")), /* @__PURE__ */ React.createElement("td", null, /* @__PURE__ */ React.createElement("input", { type: "number", id: "met_visibility", name: "met_visibility", onChange: this.handleChange, defaultValue: this.props.metVisibility }))), /* @__PURE__ */ React.createElement("tr", null, /* @__PURE__ */ React.createElement("td", null, /* @__PURE__ */ React.createElement("label", { htmlFor: "wind_speed" }, "Wind Speed (knots)")), /* @__PURE__ */ React.createElement("td", null, /* @__PURE__ */ React.createElement("input", { type: "number", id: "wind_speed", name: "wind_speed", onChange: this.handleChange, defaultValue: this.windSpeed }))), /* @__PURE__ */ React.createElement("tr", null, /* @__PURE__ */ React.createElement("td", null, /* @__PURE__ */ React.createElement("label", { htmlFor: "sea_height" }, "Sea Height (meters)")), /* @__PURE__ */ React.createElement("td", null, /* @__PURE__ */ React.createElement("input", { type: "number", id: "sea_height", name: "sea_height", onChange: this.handleChange, defaultValue: this.seaHeight }))));
}
};
var TargetTypeSelector = class extends React.Component {
constructor(props) {
super(props);
this.handleChange = this.handleChange.bind(this);
}
handleChange(event) {
const target = event.target;
const value = target.value;
this.props.targetTypeChange(value);
}
render() {
const selectObjects = [];
for (const idx in this.props.possible_targets) {
const target = this.props.possible_targets[idx];
selectObjects.push(
/* @__PURE__ */ React.createElement("option", { key: target, value: target }, target)
);
}
return /* @__PURE__ */ React.createElement("tbody", null, /* @__PURE__ */ React.createElement("tr", null, /* @__PURE__ */ React.createElement("td", null), /* @__PURE__ */ React.createElement("td", null, /* @__PURE__ */ React.createElement("select", { defaultValue: this.props.selected, onChange: this.handleChange }, selectObjects))));
}
};
var AssetSpeed = class extends React.Component {
constructor(props) {
super(props);
this.handleChange = this.handleChange.bind(this);
}
handleChange(event) {
const target = event.target;
const value = target.value;
this.props.assetSpeedChange(this.props.assetType, parseFloat(value));
}
render() {
return /* @__PURE__ */ React.createElement("tbody", null, /* @__PURE__ */ React.createElement("tr", null, /* @__PURE__ */ React.createElement("td", null, /* @__PURE__ */ React.createElement("label", { htmlFor: this.props.assetType + "_speed" }, this.props.assetType, " search speed (knots)")), /* @__PURE__ */ React.createElement("td", null, /* @__PURE__ */ React.createElement("input", { type: "number", name: "asset_speed", onChange: this.handleChange }))));
}
};
var Fatigue = class extends React.Component {
constructor(props) {
super(props);
this.handleChange = this.handleChange.bind(this);
}
handleChange(event) {
const target = event.target;
const value = target.checked;
this.props.fatigueChanged(value);
}
render() {
return /* @__PURE__ */ React.createElement("tbody", null, /* @__PURE__ */ React.createElement("tr", null, /* @__PURE__ */ React.createElement("td", null, /* @__PURE__ */ React.createElement("label", { htmlFor: "fatigue" }, "Fatigue")), /* @__PURE__ */ React.createElement("td", null, /* @__PURE__ */ React.createElement("input", { type: "checkbox", name: "fatigue", id: "fatigue", defaultChecked: this.props.fatigue, onChange: this.handleChange }))));
}
};
var DataTable = class extends React.Component {
constructor(props) {
super(props);
this.handleChange = this.handleChange.bind(this);
}
handleChange(event) {
const target = event.target;
const value = target.value;
const name = target.id;
const idParts = name.split("_");
const assetID = idParts.slice(0, 2).join("_");
const columnName = idParts.slice(2).join("_");
this.props.updateData(assetID, columnName, parseFloat(value));
}
render() {
const rows = [];
const assetTypes = [/* @__PURE__ */ React.createElement("th", { key: "head" }, "Asset Type:")];
const assetHeights = [/* @__PURE__ */ React.createElement("th", { key: "head" }, "Height of Eye:")];
for (const idx in tableRows) {
const htmlColumns = [];
htmlColumns.push(/* @__PURE__ */ React.createElement("th", { key: "head" }, tableRows[idx].display_name));
for (const colIdx in this.props.columns) {
const column = this.props.columns[colIdx];
if (tableRows[idx].input) {
htmlColumns.push(
/* @__PURE__ */ React.createElement("td", { key: idx + "_" + colIdx }, /* @__PURE__ */ React.createElement(
"input",
{
type: tableRows[idx].input_type,
id: column.asset_type + "_" + column.column + "_" + tableRows[idx].column_name,
defaultValue: column[tableRows[idx].column_name],
onChange: this.handleChange
}
))
);
} else if (tableRows[idx].column_name === "fatigue") {
htmlColumns.push(/* @__PURE__ */ React.createElement("td", { key: idx + "_" + colIdx }, column[tableRows[idx].column_name] ? 0.9 : 1));
} else {
htmlColumns.push(/* @__PURE__ */ React.createElement("td", { key: idx + "_" + colIdx }, column[tableRows[idx].column_name]));
}
}
rows.push(/* @__PURE__ */ React.createElement("tr", { key: idx }, htmlColumns));
}
for (const colIdx in this.props.columns) {
const column = this.props.columns[colIdx];
assetTypes.push(/* @__PURE__ */ React.createElement("th", { key: colIdx }, column.asset_type));
assetHeights.push(/* @__PURE__ */ React.createElement("th", { key: colIdx }, column.column));
}
return /* @__PURE__ */ React.createElement("form", null, /* @__PURE__ */ React.createElement(Table, null, /* @__PURE__ */ React.createElement("thead", null, /* @__PURE__ */ React.createElement("tr", null, assetTypes), /* @__PURE__ */ React.createElement("tr", null, assetHeights)), /* @__PURE__ */ React.createElement("tbody", null, rows)));
}
};
var MarineSACTable = class extends React.Component {
constructor(props) {
super(props);
this.possibleTargetsList = Object.keys(marineSweepWidths);
this.state = {
columns: [],
targetType: this.possibleTargetsList[0],
fatigue: false,
weatherImpact: "low",
assetSpeeds: {
Boat: 0,
Aircraft: 0
},
metVisibility: 10,
practicalTrackSpacing: /* @__PURE__ */ new Map(),
availableSearchHours: /* @__PURE__ */ new Map()
};
this.weatherImpactChange = this.weatherImpactChange.bind(this);
this.weatherVisibilityChange = this.weatherVisibilityChange.bind(this);
this.targetTypeChange = this.targetTypeChange.bind(this);
this.assetSpeedChange = this.assetSpeedChange.bind(this);
this.fatigueChange = this.fatigueChange.bind(this);
this.updateData = this.updateData.bind(this);
const defaultAssets = [
{ asset_type: "Boat", search_height: "8ft" },
{ asset_type: "Boat", search_height: "14ft" },
{ asset_type: "Aircraft", search_height: "500ft" },
{ asset_type: "Aircraft", search_height: "1000ft" }
];
for (const assetIdx in defaultAssets) {
const asset = defaultAssets[assetIdx];
this.state.columns.push(new MarineSAC(asset.search_height, asset.asset_type));
this.state.practicalTrackSpacing.set(`${asset.asset_type}_${asset.search_height}`, 0);
this.state.availableSearchHours.set(`${asset.asset_type}_${asset.search_height}`, 0);
}
}
weatherImpactChange(newWeatherImpact) {
this.setState({ weatherImpact: newWeatherImpact });
}
weatherVisibilityChange(metVisibility) {
this.setState({ metVisibility });
}
targetTypeChange(newTargetType) {
this.setState({ targetType: newTargetType });
}
assetSpeedChange(assetType, speed) {
this.setState(function(oldState) {
const currentAssetSpeeds = oldState.assetSpeeds;
currentAssetSpeeds[assetType] = speed;
return { assetSpeeds: currentAssetSpeeds };
});
}
fatigueChange(value) {
this.setState({ fatigue: value });
}
updateData(assetID, fieldName, value) {
if (fieldName === "practicalTrackSpacing") {
this.setState(function(oldState) {
const currentData = oldState.practicalTrackSpacing;
currentData.set(assetID, value);
return { practicalTrackSpacing: currentData };
});
}
if (fieldName === "availableSearchHours") {
this.setState(function(oldState) {
const currentData = oldState.availableSearchHours;
currentData.set(assetID, value);
return { availableSearchHours: currentData };
});
}
}
recalculate() {
const targetData = marineSweepWidths[this.state.targetType];
for (const colIdx in this.state.columns) {
const column = this.state.columns[colIdx];
const searchHeight = column.column;
const assetType = column.asset_type;
const columnName = `${assetType}_${searchHeight}`;
const visibleDistanceData = assetType == "Boat" ? targetData.Boat[searchHeight] : assetType === "Heliocopter" ? targetData.Helicopter[searchHeight] : assetType === "Aircraft" ? targetData.Aircraft[searchHeight] : null;
const speedCorrections = assetType === "Aircraft" ? targetData.speed_corrections.Aircraft : assetType === "Helicopter" ? targetData.speed_corrections.Helicopter : null;
column.asset_speed = Number(this.state.assetSpeeds[column.asset_type]);
let lowerSpeedSeen = null;
let higherSpeedSeen = null;
if (speedCorrections === null) {
column.speed_correction = 1;
} else {
for (const idx in speedCorrections) {
const data = speedCorrections[idx];
if ((lowerSpeedSeen === null || data.speed > lowerSpeedSeen.speed) && data.speed <= column.asset_speed) {
lowerSpeedSeen = data;
}
if ((higherSpeedSeen === null || data.speed < higherSpeedSeen.speed) && data.speed >= column.asset_speed) {
higherSpeedSeen = data;
}
}
if (lowerSpeedSeen === null && higherSpeedSeen !== null) {
column.speed_correction = higherSpeedSeen.correction;
} else if (lowerSpeedSeen !== null && higherSpeedSeen === null) {
column.speed_correction = lowerSpeedSeen.correction;
} else if (lowerSpeedSeen !== null && higherSpeedSeen !== null) {
if (lowerSpeedSeen.correction === higherSpeedSeen.correction) {
column.speed_correction = lowerSpeedSeen.correction;
} else {
const speedRange = higherSpeedSeen.speed - lowerSpeedSeen.speed;
const correctionRange = higherSpeedSeen.correction - lowerSpeedSeen.correction;
const assetSpeedOffset = column.asset_speed - lowerSpeedSeen.speed;
const ratio = assetSpeedOffset / speedRange;
column.speed_correction = lowerSpeedSeen.correction + ratio * correctionRange;
}
} else {
column.speed_correction = 1;
}
}
let highestSeenSweepWidth = 0;
let highestSeenVis = 0;
if (visibleDistanceData) {
for (const idx in visibleDistanceData) {
const data = visibleDistanceData[idx];
if (data.vis <= this.state.metVisibility && data.vis > highestSeenVis) {
highestSeenSweepWidth = data.sw;
highestSeenVis = data.vis;
}
}
}
column.wu = highestSeenSweepWidth;
const weatherCorrectionsTable = targetData.weather_corrections === "large" ? weatherCorrections.large : weatherCorrections.small;
if (this.state.weatherImpact === "low" || this.state.weatherImpact === "medium" || this.state.weatherImpact === "high") {
column.fw = weatherCorrectionsTable.IAMSAR[this.state.weatherImpact];
}
column.fatigue = this.state.fatigue;
column.practical_track_spacing = this.state.practicalTrackSpacing.get(columnName);
column.available_search_hours = this.state.availableSearchHours.get(columnName);
column.recaclculate();
}
}
render() {
this.recalculate();
return /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement(Table, null, /* @__PURE__ */ React.createElement(WeatherDataTable, { weatherImpactChange: this.weatherImpactChange, weatherVisibilityChange: this.weatherVisibilityChange, metVisibility: this.state.metVisibility }), /* @__PURE__ */ React.createElement(AssetSpeed, { assetType: "Boat", assetSpeedChange: this.assetSpeedChange }), /* @__PURE__ */ React.createElement(AssetSpeed, { assetType: "Aircraft", assetSpeedChange: this.assetSpeedChange }), /* @__PURE__ */ React.createElement(TargetTypeSelector, { possible_targets: this.possibleTargetsList, targetTypeChange: this.targetTypeChange, selected: this.state.targetType }), /* @__PURE__ */ React.createElement(Fatigue, { fatigueChanged: this.fatigueChange, fatigue: this.state.fatigue })), /* @__PURE__ */ React.createElement(DataTable, { columns: this.state.columns, updateData: this.updateData }));
}
};
export {
MarineSACTable
};
//# sourceMappingURL=marine-search-area-coverage.mjs.map