@jjlmoya/utils-drones 1.35.0 → 1.36.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/entries.ts +4 -0
- package/src/index.ts +1 -0
- package/src/tests/locale_completeness.test.ts +2 -2
- package/src/tests/tool_validation.test.ts +2 -2
- package/src/tool/fpv-drone-speed-calculator/bibliography.astro +6 -0
- package/src/tool/fpv-drone-speed-calculator/bibliography.ts +14 -0
- package/src/tool/fpv-drone-speed-calculator/component.astro +115 -0
- package/src/tool/fpv-drone-speed-calculator/controller.ts +105 -0
- package/src/tool/fpv-drone-speed-calculator/dom-views.ts +80 -0
- package/src/tool/fpv-drone-speed-calculator/entry.ts +27 -0
- package/src/tool/fpv-drone-speed-calculator/fpv-drone-speed-calculator.css +558 -0
- package/src/tool/fpv-drone-speed-calculator/i18n/de.ts +20 -0
- package/src/tool/fpv-drone-speed-calculator/i18n/en.ts +47 -0
- package/src/tool/fpv-drone-speed-calculator/i18n/es.ts +40 -0
- package/src/tool/fpv-drone-speed-calculator/i18n/fr.ts +13 -0
- package/src/tool/fpv-drone-speed-calculator/i18n/id.ts +13 -0
- package/src/tool/fpv-drone-speed-calculator/i18n/it.ts +13 -0
- package/src/tool/fpv-drone-speed-calculator/i18n/ja.ts +13 -0
- package/src/tool/fpv-drone-speed-calculator/i18n/ko.ts +13 -0
- package/src/tool/fpv-drone-speed-calculator/i18n/nl.ts +13 -0
- package/src/tool/fpv-drone-speed-calculator/i18n/pl.ts +13 -0
- package/src/tool/fpv-drone-speed-calculator/i18n/pt.ts +13 -0
- package/src/tool/fpv-drone-speed-calculator/i18n/ru.ts +13 -0
- package/src/tool/fpv-drone-speed-calculator/i18n/sv.ts +13 -0
- package/src/tool/fpv-drone-speed-calculator/i18n/tr.ts +13 -0
- package/src/tool/fpv-drone-speed-calculator/i18n/zh.ts +13 -0
- package/src/tool/fpv-drone-speed-calculator/index.ts +11 -0
- package/src/tool/fpv-drone-speed-calculator/logic.test.ts +53 -0
- package/src/tool/fpv-drone-speed-calculator/logic.ts +126 -0
- package/src/tool/fpv-drone-speed-calculator/seo.astro +16 -0
- package/src/tool/fpv-drone-speed-calculator/ui.ts +47 -0
- package/src/tools.ts +3 -0
package/package.json
CHANGED
package/src/entries.ts
CHANGED
|
@@ -14,6 +14,8 @@ export { fpvDroneThrustToWeightRatio } from './tool/fpv-drone-thrust-to-weight-r
|
|
|
14
14
|
export type { FpvDroneThrustToWeightRatioUI, FpvDroneThrustToWeightRatioLocaleContent } from './tool/fpv-drone-thrust-to-weight-ratio/entry';
|
|
15
15
|
export { droneMotorPropellerCalculator } from './tool/drone-motor-propeller-calculator/entry';
|
|
16
16
|
export type { DroneMotorPropellerUI, DroneMotorPropellerLocaleContent } from './tool/drone-motor-propeller-calculator/entry';
|
|
17
|
+
export { fpvDroneSpeedCalculator } from './tool/fpv-drone-speed-calculator/entry';
|
|
18
|
+
export type { FpvDroneSpeedUI, FpvDroneSpeedLocaleContent } from './tool/fpv-drone-speed-calculator/entry';
|
|
17
19
|
export { dronesCategory } from './category';
|
|
18
20
|
|
|
19
21
|
import { antennaLengthCalculator } from './tool/antenna-length-calculator/entry';
|
|
@@ -24,6 +26,7 @@ import { gsdFlightPlanner } from './tool/gsd-flight-planner/entry';
|
|
|
24
26
|
import { droneBatteryCRatingCalculator } from './tool/drone-battery-c-rating-calculator/entry';
|
|
25
27
|
import { fpvDroneThrustToWeightRatio } from './tool/fpv-drone-thrust-to-weight-ratio/entry';
|
|
26
28
|
import { droneMotorPropellerCalculator } from './tool/drone-motor-propeller-calculator/entry';
|
|
29
|
+
import { fpvDroneSpeedCalculator } from './tool/fpv-drone-speed-calculator/entry';
|
|
27
30
|
|
|
28
31
|
export const ALL_ENTRIES = [
|
|
29
32
|
antennaLengthCalculator,
|
|
@@ -34,4 +37,5 @@ export const ALL_ENTRIES = [
|
|
|
34
37
|
droneBatteryCRatingCalculator,
|
|
35
38
|
fpvDroneThrustToWeightRatio,
|
|
36
39
|
droneMotorPropellerCalculator,
|
|
40
|
+
fpvDroneSpeedCalculator,
|
|
37
41
|
];
|
package/src/index.ts
CHANGED
|
@@ -22,3 +22,4 @@ export { GPS_COORDINATES_CONVERTER_TOOL } from './tool/gps-coordinates-converter
|
|
|
22
22
|
export { DRONE_POWER_ANALYZER_TOOL } from './tool/drone-power-analyzer/index';
|
|
23
23
|
export { GSD_FLIGHT_PLANNER_TOOL } from './tool/gsd-flight-planner/index';
|
|
24
24
|
export { DRONE_MOTOR_PROPELLER_CALCULATOR_TOOL } from './tool/drone-motor-propeller-calculator/index';
|
|
25
|
+
export { FPV_DRONE_SPEED_CALCULATOR_TOOL } from './tool/fpv-drone-speed-calculator/index';
|
|
@@ -4,8 +4,8 @@ import { dronesCategory } from '../data';
|
|
|
4
4
|
|
|
5
5
|
describe('Tool Validation Suite', () => {
|
|
6
6
|
describe('Library Registration', () => {
|
|
7
|
-
it('should have
|
|
8
|
-
expect(ALL_TOOLS.length).toBe(
|
|
7
|
+
it('should have 9 tools in ALL_TOOLS', () => {
|
|
8
|
+
expect(ALL_TOOLS.length).toBe(9);
|
|
9
9
|
});
|
|
10
10
|
|
|
11
11
|
it('dronesCategory should be defined', () => {
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { BibliographyEntry } from '../../types';
|
|
2
|
+
|
|
3
|
+
export const bibliography: BibliographyEntry[] = [
|
|
4
|
+
{
|
|
5
|
+
name: 'Universidad Politécnica de Madrid - Comparación de una hélice de paso fijo con una de paso variable para una aplicación de un avión VTOL',
|
|
6
|
+
url: 'https://oa.upm.es/64973/',
|
|
7
|
+
},
|
|
8
|
+
{
|
|
9
|
+
name: 'NASA Technical Reports Server - Efficient Variable-Pitch Propeller Aerodynamic Model Development for Vectored-Thrust eVTOL Aircraft',
|
|
10
|
+
url: 'https://ntrs.nasa.gov/citations/20220005627',
|
|
11
|
+
},
|
|
12
|
+
];
|
|
13
|
+
|
|
14
|
+
export const BIBLIOGRAPHY_ITEMS = bibliography;
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
---
|
|
2
|
+
import type { FpvDroneSpeedUI } from './ui';
|
|
3
|
+
|
|
4
|
+
interface Props {
|
|
5
|
+
ui: FpvDroneSpeedUI;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
const { ui } = Astro.props;
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
<div class="fpv-speed-tool" data-fpv-speed-calculator data-unit="metric">
|
|
12
|
+
<script is:inline type="application/json" data-fpv-speed-ui set:html={JSON.stringify(ui)}></script>
|
|
13
|
+
|
|
14
|
+
<div class="speed-brief">
|
|
15
|
+
<div>
|
|
16
|
+
<span class="brief-kicker">{ui.calculateFromLabel}</span>
|
|
17
|
+
<p>{ui.assumptionsText}</p>
|
|
18
|
+
</div>
|
|
19
|
+
<div class="unit-switch" role="group" aria-label={ui.unitsLabel}>
|
|
20
|
+
<button type="button" class="unit-button is-selected" data-unit="metric" aria-pressed="true">{ui.metricUnit}</button>
|
|
21
|
+
<button type="button" class="unit-button" data-unit="imperial" aria-pressed="false">{ui.imperialUnit}</button>
|
|
22
|
+
</div>
|
|
23
|
+
</div>
|
|
24
|
+
|
|
25
|
+
<section class="flight-deck" aria-labelledby="speed-inputs-title">
|
|
26
|
+
<div class="control-rail">
|
|
27
|
+
<div class="rail-heading">
|
|
28
|
+
<span class="brief-kicker">{ui.inputsLabel}</span>
|
|
29
|
+
<h2 id="speed-inputs-title">{ui.presetsLabel}</h2>
|
|
30
|
+
</div>
|
|
31
|
+
<div class="preset-track">
|
|
32
|
+
<button type="button" class="preset-button" data-preset="racing">{ui.presetRacing}</button>
|
|
33
|
+
<button type="button" class="preset-button is-selected" data-preset="freestyle">{ui.presetFreestyle}</button>
|
|
34
|
+
<button type="button" class="preset-button" data-preset="cruiser">{ui.presetCruiser}</button>
|
|
35
|
+
</div>
|
|
36
|
+
<div class="input-list">
|
|
37
|
+
<label class="input-row"><span>{ui.motorKvLabel}</span><input data-field="motorKv" type="number" min="100" max="10000" step="50" inputmode="decimal" /><small>KV</small></label>
|
|
38
|
+
<label class="input-row"><span>{ui.batteryVoltageLabel}</span><input data-field="batteryVoltage" type="number" min="1" max="80" step="0.1" inputmode="decimal" /><small>V</small></label>
|
|
39
|
+
<label class="input-row"><span>{ui.propellerPitchLabel}</span><input data-field="propellerPitchInches" type="number" min="0.5" max="25" step="0.1" inputmode="decimal" /><small>in</small></label>
|
|
40
|
+
<label class="input-row"><span>{ui.efficiencyLabel}</span><input data-field="efficiencyPercent" type="number" min="50" max="98" step="1" inputmode="decimal" /><small>%</small></label>
|
|
41
|
+
<label class="input-row"><span>{ui.aircraftMassLabel}</span><input data-field="aircraftMassGrams" type="number" min="1" max="50000" step="10" inputmode="decimal" /><small data-unit-label="mass">g</small></label>
|
|
42
|
+
</div>
|
|
43
|
+
</div>
|
|
44
|
+
|
|
45
|
+
<div class="speed-orchestra">
|
|
46
|
+
<div class="speed-orchestra-header">
|
|
47
|
+
<div>
|
|
48
|
+
<span class="brief-kicker">{ui.resultsLabel}</span>
|
|
49
|
+
<h2>{ui.speedLaneLabel}</h2>
|
|
50
|
+
</div>
|
|
51
|
+
<div class="diagnosis" data-output="diagnosis" aria-live="polite">{ui.diagnosisPlanning}</div>
|
|
52
|
+
</div>
|
|
53
|
+
|
|
54
|
+
<div class="speed-lane" aria-label={ui.speedLaneLabel}>
|
|
55
|
+
<div class="lane-grid" aria-hidden="true"></div>
|
|
56
|
+
<div class="lane-horizon" aria-hidden="true"></div>
|
|
57
|
+
<div class="lane-header">
|
|
58
|
+
<span>{ui.speedLaneLabel}</span>
|
|
59
|
+
<span><b data-output="speed-axis-start">{ui.speedAxisStart}</b><i></i><b data-output="speed-axis-end">{ui.speedAxisEnd}</b></span>
|
|
60
|
+
</div>
|
|
61
|
+
<div class="speed-comparison">
|
|
62
|
+
<div class="comparison-scale" aria-hidden="true">
|
|
63
|
+
<span data-output="speed-axis-start">{ui.speedAxisStart}</span>
|
|
64
|
+
<span data-output="speed-axis-mid">150 km/h</span>
|
|
65
|
+
<span data-output="speed-axis-end">{ui.speedAxisEnd}</span>
|
|
66
|
+
</div>
|
|
67
|
+
<div class="comparison-row">
|
|
68
|
+
<div class="comparison-label"><span>{ui.estimatedSpeedLabel}</span><strong data-output="estimated-speed">178 km/h</strong></div>
|
|
69
|
+
<div class="comparison-track"><i class="comparison-fill comparison-fill-estimated"></i></div>
|
|
70
|
+
</div>
|
|
71
|
+
<div class="comparison-row">
|
|
72
|
+
<div class="comparison-label"><span>{ui.pitchSpeedLabel}</span><strong data-output="pitch-speed">217 km/h</strong></div>
|
|
73
|
+
<div class="comparison-track"><i class="comparison-fill comparison-fill-pitch"></i></div>
|
|
74
|
+
</div>
|
|
75
|
+
<div class="comparison-gap"><span>{ui.slipLabel}</span><strong data-output="speed-gap">39 km/h</strong><small data-output="slip">18%</small></div>
|
|
76
|
+
</div>
|
|
77
|
+
</div>
|
|
78
|
+
|
|
79
|
+
<div class="speed-readouts">
|
|
80
|
+
<div class="hero-readout"><strong data-output="estimated-speed">178 km/h</strong><span>{ui.estimatedSpeedLabel}</span></div>
|
|
81
|
+
<div class="readout"><strong data-output="pitch-speed">217 km/h</strong><span>{ui.pitchSpeedLabel}</span></div>
|
|
82
|
+
<div class="readout"><strong data-output="loaded-rpm">36,000 rpm</strong><span>{ui.loadedRpmLabel}</span></div>
|
|
83
|
+
</div>
|
|
84
|
+
|
|
85
|
+
<p class="diagnosis-advice" data-output="diagnosis-advice" aria-live="polite">{ui.diagnosisPlanningAdvice}</p>
|
|
86
|
+
|
|
87
|
+
<section class="sensitivity" aria-labelledby="sensitivity-title">
|
|
88
|
+
<div class="subheading"><span class="brief-kicker">{ui.sensitivityLabel}</span><h3 id="sensitivity-title">{ui.propellerPitchLabel}</h3></div>
|
|
89
|
+
<div class="sensitivity-row">
|
|
90
|
+
<div class="sensitivity-point"><span>{ui.lowerPitchLabel}</span><strong data-output="lower-speed">158 km/h</strong><small data-output="lower-pitch">3.8 in</small><div class="sensitivity-bar"><i data-bar="lower"></i></div></div>
|
|
91
|
+
<div class="sensitivity-point is-current"><span>{ui.selectedPitchLabel}</span><strong data-output="current-speed">178 km/h</strong><small data-output="current-pitch">4.3 in</small><div class="sensitivity-bar"><i data-bar="current"></i></div></div>
|
|
92
|
+
<div class="sensitivity-point"><span>{ui.higherPitchLabel}</span><strong data-output="higher-speed">199 km/h</strong><small data-output="higher-pitch">4.8 in</small><div class="sensitivity-bar"><i data-bar="higher"></i></div></div>
|
|
93
|
+
</div>
|
|
94
|
+
</section>
|
|
95
|
+
|
|
96
|
+
<div class="metric-strip">
|
|
97
|
+
<div><span>{ui.noLoadRpmLabel}</span><strong data-output="no-load-rpm">43,290 rpm</strong></div>
|
|
98
|
+
<div><span>{ui.slipLabel}</span><strong data-output="slip">18%</strong></div>
|
|
99
|
+
<div><span>{ui.loadEffectLabel}</span><strong data-output="load-effect">7%</strong></div>
|
|
100
|
+
</div>
|
|
101
|
+
</div>
|
|
102
|
+
</section>
|
|
103
|
+
|
|
104
|
+
<div class="assumptions-row">
|
|
105
|
+
<p><strong>{ui.assumptionsLabel}</strong> {ui.assumptionsText}</p>
|
|
106
|
+
<p>{ui.safetyText}</p>
|
|
107
|
+
</div>
|
|
108
|
+
</div>
|
|
109
|
+
|
|
110
|
+
<script>
|
|
111
|
+
import { initFpvDroneSpeedCalculator } from './controller';
|
|
112
|
+
|
|
113
|
+
const root = document.querySelector<HTMLElement>('[data-fpv-speed-calculator]');
|
|
114
|
+
if (root) initFpvDroneSpeedCalculator(root);
|
|
115
|
+
</script>
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { calculateFpvDroneSpeed, type FpvDroneSpeedInputs } from './logic';
|
|
2
|
+
import { formatMassInput, parseMassInput, renderResults, updateUnitLabels, type DisplayUnit } from './dom-views';
|
|
3
|
+
import type { FpvDroneSpeedUI } from './ui';
|
|
4
|
+
|
|
5
|
+
interface Preset {
|
|
6
|
+
id: string;
|
|
7
|
+
inputs: FpvDroneSpeedInputs;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const PRESETS: Preset[] = [
|
|
11
|
+
{ id: 'racing', inputs: { motorKv: 2450, batteryVoltage: 16.8, propellerPitchInches: 4.3, efficiencyPercent: 84, aircraftMassGrams: 620 } },
|
|
12
|
+
{ id: 'freestyle', inputs: { motorKv: 1950, batteryVoltage: 22.2, propellerPitchInches: 4.3, efficiencyPercent: 82, aircraftMassGrams: 700 } },
|
|
13
|
+
{ id: 'cruiser', inputs: { motorKv: 1500, batteryVoltage: 22.2, propellerPitchInches: 3.5, efficiencyPercent: 78, aircraftMassGrams: 1200 } },
|
|
14
|
+
];
|
|
15
|
+
|
|
16
|
+
const DEFAULT_INPUTS = PRESETS[1]!.inputs;
|
|
17
|
+
|
|
18
|
+
function input(root: HTMLElement, field: string): HTMLInputElement | null {
|
|
19
|
+
return root.querySelector<HTMLInputElement>(`[data-field="${field}"]`);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function setInput(root: HTMLElement, field: string, value: string): void {
|
|
23
|
+
const element = input(root, field);
|
|
24
|
+
if (element) element.value = value;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function readInputs(root: HTMLElement, unit: DisplayUnit): FpvDroneSpeedInputs {
|
|
28
|
+
const read = (field: string): number => Number(input(root, field)?.value ?? 0);
|
|
29
|
+
return {
|
|
30
|
+
motorKv: read('motorKv'),
|
|
31
|
+
batteryVoltage: read('batteryVoltage'),
|
|
32
|
+
propellerPitchInches: read('propellerPitchInches'),
|
|
33
|
+
efficiencyPercent: read('efficiencyPercent'),
|
|
34
|
+
aircraftMassGrams: parseMassInput(input(root, 'aircraftMassGrams')?.value ?? '', unit),
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function setInputs(root: HTMLElement, values: FpvDroneSpeedInputs, unit: DisplayUnit): void {
|
|
39
|
+
setInput(root, 'motorKv', String(values.motorKv));
|
|
40
|
+
setInput(root, 'batteryVoltage', String(values.batteryVoltage));
|
|
41
|
+
setInput(root, 'propellerPitchInches', String(values.propellerPitchInches));
|
|
42
|
+
setInput(root, 'efficiencyPercent', String(values.efficiencyPercent));
|
|
43
|
+
setInput(root, 'aircraftMassGrams', formatMassInput(values.aircraftMassGrams, unit));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function selectPreset(root: HTMLElement, id: string): void {
|
|
47
|
+
root.querySelectorAll<HTMLElement>('[data-preset]').forEach((element) => {
|
|
48
|
+
element.classList.toggle('is-selected', element.dataset.preset === id);
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function selectUnit(root: HTMLElement, unit: DisplayUnit): void {
|
|
53
|
+
root.querySelectorAll<HTMLElement>('[data-unit]').forEach((element) => {
|
|
54
|
+
const active = element.dataset.unit === unit;
|
|
55
|
+
element.classList.toggle('is-selected', active);
|
|
56
|
+
element.setAttribute('aria-pressed', String(active));
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function recalculate(root: HTMLElement, ui: FpvDroneSpeedUI, unit: DisplayUnit): void {
|
|
61
|
+
const results = calculateFpvDroneSpeed(readInputs(root, unit));
|
|
62
|
+
renderResults({ root, results, ui, unit });
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function changeUnit(root: HTMLElement, ui: FpvDroneSpeedUI, from: DisplayUnit, to: DisplayUnit): void {
|
|
66
|
+
if (from === to) return;
|
|
67
|
+
const values = readInputs(root, from);
|
|
68
|
+
setInputs(root, values, to);
|
|
69
|
+
root.dataset.unit = to;
|
|
70
|
+
selectUnit(root, to);
|
|
71
|
+
updateUnitLabels(root, to, ui);
|
|
72
|
+
recalculate(root, ui, to);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function readUi(root: HTMLElement): FpvDroneSpeedUI {
|
|
76
|
+
const element = root.querySelector<HTMLScriptElement>('[data-fpv-speed-ui]');
|
|
77
|
+
return JSON.parse(element?.textContent ?? '{}') as FpvDroneSpeedUI;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function initFpvDroneSpeedCalculator(root: HTMLElement): void {
|
|
81
|
+
const ui = readUi(root);
|
|
82
|
+
let unit: DisplayUnit = 'metric';
|
|
83
|
+
setInputs(root, DEFAULT_INPUTS, unit);
|
|
84
|
+
updateUnitLabels(root, unit, ui);
|
|
85
|
+
selectUnit(root, unit);
|
|
86
|
+
root.dataset.unit = unit;
|
|
87
|
+
|
|
88
|
+
root.querySelectorAll<HTMLElement>('[data-preset]').forEach((element) => element.addEventListener('click', () => {
|
|
89
|
+
const preset = PRESETS.find((item) => item.id === element.dataset.preset);
|
|
90
|
+
if (!preset) return;
|
|
91
|
+
setInputs(root, preset.inputs, unit);
|
|
92
|
+
selectPreset(root, preset.id);
|
|
93
|
+
recalculate(root, ui, unit);
|
|
94
|
+
}));
|
|
95
|
+
root.querySelectorAll<HTMLElement>('[data-unit]').forEach((element) => element.addEventListener('click', () => {
|
|
96
|
+
const next = element.dataset.unit === 'imperial' ? 'imperial' : 'metric';
|
|
97
|
+
changeUnit(root, ui, unit, next);
|
|
98
|
+
unit = next;
|
|
99
|
+
}));
|
|
100
|
+
root.querySelectorAll<HTMLInputElement>('input[data-field]').forEach((element) => element.addEventListener('input', () => {
|
|
101
|
+
selectPreset(root, '');
|
|
102
|
+
recalculate(root, ui, unit);
|
|
103
|
+
}));
|
|
104
|
+
recalculate(root, ui, unit);
|
|
105
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import type { FpvDroneSpeedResults } from './logic';
|
|
2
|
+
import type { FpvDroneSpeedUI } from './ui';
|
|
3
|
+
|
|
4
|
+
export type DisplayUnit = 'metric' | 'imperial';
|
|
5
|
+
|
|
6
|
+
interface RenderContext {
|
|
7
|
+
root: HTMLElement;
|
|
8
|
+
results: FpvDroneSpeedResults;
|
|
9
|
+
ui: FpvDroneSpeedUI;
|
|
10
|
+
unit: DisplayUnit;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function number(value: number, digits = 0): string {
|
|
14
|
+
return new Intl.NumberFormat(undefined, { maximumFractionDigits: digits }).format(value);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function speed(value: number, unit: DisplayUnit): string {
|
|
18
|
+
return `${number(unit === 'metric' ? value : value / 1.60934)} ${unit === 'metric' ? 'km/h' : 'mph'}`;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function mass(value: number, unit: DisplayUnit): string {
|
|
22
|
+
return `${number(unit === 'metric' ? value : value / 28.3495, unit === 'metric' ? 0 : 1)} ${unit === 'metric' ? 'g' : 'oz'}`;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function setText(root: HTMLElement, key: string, value: string): void {
|
|
26
|
+
root.querySelectorAll<HTMLElement>(`[data-output="${key}"]`).forEach((target) => {
|
|
27
|
+
target.textContent = value;
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function diagnosisText(results: FpvDroneSpeedResults, ui: FpvDroneSpeedUI): [string, string] {
|
|
32
|
+
if (results.diagnosis === 'overspeed') return [ui.diagnosisOverspeed, ui.diagnosisOverspeedAdvice];
|
|
33
|
+
if (results.diagnosis === 'high-slip') return [ui.diagnosisHighSlip, ui.diagnosisHighSlipAdvice];
|
|
34
|
+
if (results.diagnosis === 'heavy-load') return [ui.diagnosisHeavyLoad, ui.diagnosisHeavyLoadAdvice];
|
|
35
|
+
return [ui.diagnosisPlanning, ui.diagnosisPlanningAdvice];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function renderResults({ root, results, ui, unit }: RenderContext): void {
|
|
39
|
+
const [diagnosis, advice] = diagnosisText(results, ui);
|
|
40
|
+
setText(root, 'estimated-speed', speed(results.estimatedSpeedKmh, unit));
|
|
41
|
+
setText(root, 'pitch-speed', speed(results.pitchSpeedKmh, unit));
|
|
42
|
+
setText(root, 'loaded-rpm', `${number(results.loadedRpm)} rpm`);
|
|
43
|
+
setText(root, 'no-load-rpm', `${number(results.noLoadRpm)} rpm`);
|
|
44
|
+
setText(root, 'slip', `${number(results.slipPercent, 1)}%`);
|
|
45
|
+
setText(root, 'load-effect', `${number(results.loadPenaltyPercent, 1)}%`);
|
|
46
|
+
setText(root, 'speed-gap', speed(Math.max(0, results.pitchSpeedKmh - results.estimatedSpeedKmh), unit));
|
|
47
|
+
setText(root, 'speed-axis-start', speed(0, unit));
|
|
48
|
+
setText(root, 'speed-axis-end', speed(300, unit));
|
|
49
|
+
setText(root, 'diagnosis', diagnosis);
|
|
50
|
+
setText(root, 'diagnosis-advice', advice);
|
|
51
|
+
results.sensitivity.forEach((point) => {
|
|
52
|
+
setText(root, `${point.label}-pitch`, `${number(point.pitchInches, 1)} in`);
|
|
53
|
+
setText(root, `${point.label}-speed`, speed(point.speedKmh, unit));
|
|
54
|
+
const bar = root.querySelector<HTMLElement>(`[data-bar="${point.label}"]`);
|
|
55
|
+
if (bar) bar.style.setProperty('--bar-width', `${Math.min(100, Math.max(4, point.speedKmh / Math.max(results.sensitivity[2]?.speedKmh ?? 1, 1) * 100))}%`);
|
|
56
|
+
});
|
|
57
|
+
root.dataset.diagnosis = results.diagnosis;
|
|
58
|
+
const speedPosition = Math.min(92, Math.max(8, results.estimatedSpeedKmh / 300 * 84 + 8));
|
|
59
|
+
const pitchPosition = Math.min(92, Math.max(8, results.pitchSpeedKmh / 300 * 84 + 8));
|
|
60
|
+
root.style.setProperty('--speed-position', `${speedPosition}%`);
|
|
61
|
+
root.style.setProperty('--pitch-position', `${pitchPosition}%`);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function updateUnitLabels(root: HTMLElement, unit: DisplayUnit, ui: FpvDroneSpeedUI): void {
|
|
65
|
+
root.querySelectorAll<HTMLElement>('[data-unit-label="mass"]').forEach((element) => { element.textContent = unit === 'metric' ? 'g' : 'oz'; });
|
|
66
|
+
root.querySelectorAll<HTMLElement>('[data-unit-label="speed"]').forEach((element) => { element.textContent = unit === 'metric' ? ui.speedUnit : 'mph'; });
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function formatMassInput(grams: number, unit: DisplayUnit): string {
|
|
70
|
+
return unit === 'metric' ? String(Math.round(grams)) : String(Math.round(grams / 28.3495 * 10) / 10);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function parseMassInput(value: string, unit: DisplayUnit): number {
|
|
74
|
+
const parsed = Number(value);
|
|
75
|
+
return unit === 'metric' ? parsed : parsed * 28.3495;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function formatMassLabel(grams: number, unit: DisplayUnit): string {
|
|
79
|
+
return mass(grams, unit);
|
|
80
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { DronesToolEntry, ToolLocaleContent } from '../../types';
|
|
2
|
+
import type { FpvDroneSpeedUI } from './ui';
|
|
3
|
+
|
|
4
|
+
export type { FpvDroneSpeedUI };
|
|
5
|
+
export type FpvDroneSpeedLocaleContent = ToolLocaleContent<FpvDroneSpeedUI>;
|
|
6
|
+
|
|
7
|
+
export const fpvDroneSpeedCalculator: DronesToolEntry<FpvDroneSpeedUI> = {
|
|
8
|
+
id: 'fpv-drone-speed-calculator',
|
|
9
|
+
icons: { bg: 'mdi:quadcopter', fg: 'mdi:speedometer' },
|
|
10
|
+
i18n: {
|
|
11
|
+
es: () => import('./i18n/es').then((module) => module.content),
|
|
12
|
+
en: () => import('./i18n/en').then((module) => module.content),
|
|
13
|
+
fr: () => import('./i18n/fr').then((module) => module.content),
|
|
14
|
+
de: () => import('./i18n/de').then((module) => module.content),
|
|
15
|
+
it: () => import('./i18n/it').then((module) => module.content),
|
|
16
|
+
pt: () => import('./i18n/pt').then((module) => module.content),
|
|
17
|
+
nl: () => import('./i18n/nl').then((module) => module.content),
|
|
18
|
+
pl: () => import('./i18n/pl').then((module) => module.content),
|
|
19
|
+
ru: () => import('./i18n/ru').then((module) => module.content),
|
|
20
|
+
ja: () => import('./i18n/ja').then((module) => module.content),
|
|
21
|
+
ko: () => import('./i18n/ko').then((module) => module.content),
|
|
22
|
+
zh: () => import('./i18n/zh').then((module) => module.content),
|
|
23
|
+
tr: () => import('./i18n/tr').then((module) => module.content),
|
|
24
|
+
sv: () => import('./i18n/sv').then((module) => module.content),
|
|
25
|
+
id: () => import('./i18n/id').then((module) => module.content),
|
|
26
|
+
},
|
|
27
|
+
};
|