@jjlmoya/utils-drones 1.36.0 → 1.38.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 +17 -5
- package/scripts/postinstall.mjs +2 -4
- package/src/category/index.ts +4 -0
- package/src/components/ProductionBreadcrumb.astro +134 -0
- package/src/components/ProductionStructuredData.astro +46 -0
- package/src/components/ProductionWidget.astro +182 -0
- package/src/entries.ts +4 -0
- package/src/i18n/header-ui.ts +15 -0
- package/src/i18n/language-ui.ts +9 -0
- package/src/i18n/languages.ts +24 -0
- package/src/identity/brands.ts +5 -0
- package/src/layouts/ProductionCategoryPage.astro +184 -0
- package/src/layouts/ProductionPage.astro +218 -0
- package/src/layouts/ProductionUtilityPage.astro +283 -0
- package/src/mfe/assets.ts +39 -0
- package/src/mfe/category-ui.ts +34 -0
- package/src/mfe/manifest.ts +45 -0
- package/src/mfe/routes.ts +50 -0
- package/src/pages/[locale]/[utilities]/[categories]/[category]/[slug]/manifest.json.ts +49 -0
- package/src/pages/[locale]/[utilities]/[categories]/[category]/[slug].astro +100 -0
- package/src/pages/[locale]/[utilities]/[categories]/[category].astro +44 -0
- package/src/pages/index.astro +3 -4
- package/src/pages/utilidades/[slug]/manifest.json.ts +28 -0
- package/src/pages/utilidades/[slug].astro +90 -0
- package/src/pages/utilidades/categorias/[category].astro +38 -0
- package/src/tests/diacritics_density.test.ts +1 -1
- package/src/tests/inverted_punctuation.test.ts +1 -1
- package/src/tests/locale_completeness.test.ts +2 -2
- package/src/tests/mfe_cache_contract.test.ts +11 -0
- package/src/tests/mfe_manifest.test.ts +28 -0
- package/src/tests/registry_contract.test.ts +67 -0
- package/src/tests/script_density.test.ts +1 -1
- package/src/tests/seo_length.test.ts +1 -0
- package/src/tests/tool_validation.test.ts +2 -2
- package/src/tests/translation_copy.test.ts +2 -2
- package/src/tool/antenna-length-calculator/index.ts +1 -0
- package/src/tool/drone-flight-time/index.ts +1 -0
- package/src/tool/drone-mission-battery-reserve-planner/bibliography.astro +6 -0
- package/src/tool/drone-mission-battery-reserve-planner/bibliography.ts +7 -0
- package/src/tool/drone-mission-battery-reserve-planner/component.astro +229 -0
- package/src/tool/drone-mission-battery-reserve-planner/controller.ts +124 -0
- package/src/tool/drone-mission-battery-reserve-planner/dom-views.ts +114 -0
- package/src/tool/drone-mission-battery-reserve-planner/drone-mission-battery-reserve-planner.css +392 -0
- package/src/tool/drone-mission-battery-reserve-planner/entry.ts +27 -0
- package/src/tool/drone-mission-battery-reserve-planner/evaluator.ts +23 -0
- package/src/tool/drone-mission-battery-reserve-planner/i18n/de.ts +223 -0
- package/src/tool/drone-mission-battery-reserve-planner/i18n/en.ts +223 -0
- package/src/tool/drone-mission-battery-reserve-planner/i18n/es.ts +223 -0
- package/src/tool/drone-mission-battery-reserve-planner/i18n/fr.ts +223 -0
- package/src/tool/drone-mission-battery-reserve-planner/i18n/id.ts +223 -0
- package/src/tool/drone-mission-battery-reserve-planner/i18n/it.ts +223 -0
- package/src/tool/drone-mission-battery-reserve-planner/i18n/ja.ts +223 -0
- package/src/tool/drone-mission-battery-reserve-planner/i18n/ko.ts +223 -0
- package/src/tool/drone-mission-battery-reserve-planner/i18n/nl.ts +223 -0
- package/src/tool/drone-mission-battery-reserve-planner/i18n/pl.ts +223 -0
- package/src/tool/drone-mission-battery-reserve-planner/i18n/pt.ts +223 -0
- package/src/tool/drone-mission-battery-reserve-planner/i18n/ru.ts +223 -0
- package/src/tool/drone-mission-battery-reserve-planner/i18n/sv.ts +223 -0
- package/src/tool/drone-mission-battery-reserve-planner/i18n/tr.ts +223 -0
- package/src/tool/drone-mission-battery-reserve-planner/i18n/zh.ts +223 -0
- package/src/tool/drone-mission-battery-reserve-planner/index.ts +11 -0
- package/src/tool/drone-mission-battery-reserve-planner/logic.test.ts +94 -0
- package/src/tool/drone-mission-battery-reserve-planner/logic.ts +203 -0
- package/src/tool/drone-mission-battery-reserve-planner/seo.astro +15 -0
- package/src/tool/drone-mission-battery-reserve-planner/storage.ts +59 -0
- package/src/tool/drone-mission-battery-reserve-planner/ui.ts +78 -0
- package/src/tool/gps-coordinates-converter/index.ts +1 -0
- package/src/tools.ts +4 -0
- package/src/types.ts +4 -6
- package/src/worker.ts +25 -0
- package/src/pages/[locale]/[slug].astro +0 -161
- package/src/pages/[locale].astro +0 -251
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
export type UnitSystem = 'metric' | 'imperial';
|
|
2
|
+
export type WindDirection = 'headwind' | 'tailwind' | 'crosswind';
|
|
3
|
+
export type MissionStatusKey = 'optimal' | 'tight' | 'critical' | 'exceeded';
|
|
4
|
+
|
|
5
|
+
export interface MissionInputs {
|
|
6
|
+
unitSystem: UnitSystem;
|
|
7
|
+
batteryCapacityMah: number;
|
|
8
|
+
voltageNominal: number;
|
|
9
|
+
averageCurrentAmps: number;
|
|
10
|
+
cruiseSpeedKmh: number;
|
|
11
|
+
oneWayDistanceKm: number;
|
|
12
|
+
targetHoverTimeMin: number;
|
|
13
|
+
windSpeedKmh: number;
|
|
14
|
+
windDirection: WindDirection;
|
|
15
|
+
reservePolicyPercent: number;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface MissionResults {
|
|
19
|
+
totalEnergyWh: number;
|
|
20
|
+
usableEnergyWh: number;
|
|
21
|
+
reserveEnergyWh: number;
|
|
22
|
+
basePowerDrawWatts: number;
|
|
23
|
+
outboundPowerWatts: number;
|
|
24
|
+
returnPowerWatts: number;
|
|
25
|
+
totalAutonomyMinutes: number;
|
|
26
|
+
outboundSpeedKmh: number;
|
|
27
|
+
returnSpeedKmh: number;
|
|
28
|
+
outboundTimeMinutes: number;
|
|
29
|
+
targetHoverTimeMinutes: number;
|
|
30
|
+
returnTimeMinutes: number;
|
|
31
|
+
totalMissionTimeMinutes: number;
|
|
32
|
+
outboundEnergyWh: number;
|
|
33
|
+
targetHoverEnergyWh: number;
|
|
34
|
+
returnEnergyWh: number;
|
|
35
|
+
missionEnergyUsedWh: number;
|
|
36
|
+
remainingEnergyLandingWh: number;
|
|
37
|
+
remainingEnergyLandingPercent: number;
|
|
38
|
+
maxSafeRadiusKm: number;
|
|
39
|
+
voltageSagWhLoss: number;
|
|
40
|
+
statusKey: MissionStatusKey;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const KMH_TO_MPH = 0.621371;
|
|
44
|
+
const KM_TO_MILES = 0.621371;
|
|
45
|
+
|
|
46
|
+
export function clampValue(val: number, min: number, max: number): number {
|
|
47
|
+
return Math.min(Math.max(val, min), max);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function computeEffectiveSpeeds(
|
|
51
|
+
cruiseKmh: number,
|
|
52
|
+
windKmh: number,
|
|
53
|
+
dir: WindDirection
|
|
54
|
+
): { outbound: number; returnLeg: number } {
|
|
55
|
+
if (dir === 'headwind') {
|
|
56
|
+
return { outbound: Math.max(1, cruiseKmh - windKmh), returnLeg: cruiseKmh + windKmh * 0.85 };
|
|
57
|
+
}
|
|
58
|
+
if (dir === 'tailwind') {
|
|
59
|
+
return { outbound: cruiseKmh + windKmh * 0.85, returnLeg: Math.max(1, cruiseKmh - windKmh) };
|
|
60
|
+
}
|
|
61
|
+
const effective = Math.max(1, Math.sqrt(Math.max(0, cruiseKmh * cruiseKmh - windKmh * windKmh * 0.5)));
|
|
62
|
+
return { outbound: effective, returnLeg: effective };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function calculateWindPowerPenalty(
|
|
66
|
+
basePowerWatts: number,
|
|
67
|
+
cruiseKmh: number,
|
|
68
|
+
windKmh: number,
|
|
69
|
+
dir: WindDirection
|
|
70
|
+
): { outboundPower: number; returnPower: number } {
|
|
71
|
+
const windRatio = windKmh / Math.max(10, cruiseKmh);
|
|
72
|
+
|
|
73
|
+
if (dir === 'headwind') {
|
|
74
|
+
return {
|
|
75
|
+
outboundPower: basePowerWatts * Math.pow(1 + 0.65 * windRatio, 1.3),
|
|
76
|
+
returnPower: basePowerWatts * Math.max(0.75, 1 - 0.35 * windRatio),
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
if (dir === 'tailwind') {
|
|
80
|
+
return {
|
|
81
|
+
outboundPower: basePowerWatts * Math.max(0.75, 1 - 0.35 * windRatio),
|
|
82
|
+
returnPower: basePowerWatts * Math.pow(1 + 0.65 * windRatio, 1.3),
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
const crossPower = basePowerWatts * (1 + 0.25 * windRatio);
|
|
86
|
+
return { outboundPower: crossPower, returnPower: crossPower };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function calculateEnergyMetrics(inputs: MissionInputs): {
|
|
90
|
+
totalEnergyWh: number;
|
|
91
|
+
usableEnergyWh: number;
|
|
92
|
+
reserveEnergyWh: number;
|
|
93
|
+
basePowerDrawWatts: number;
|
|
94
|
+
totalAutonomyMinutes: number;
|
|
95
|
+
voltageSagWhLoss: number;
|
|
96
|
+
} {
|
|
97
|
+
const cap = Math.max(1, inputs.batteryCapacityMah);
|
|
98
|
+
const volt = Math.max(0.1, inputs.voltageNominal);
|
|
99
|
+
const current = Math.max(0.1, inputs.averageCurrentAmps);
|
|
100
|
+
const reservePct = clampValue(inputs.reservePolicyPercent, 5, 50) / 100;
|
|
101
|
+
|
|
102
|
+
const grossWh = (cap * volt) / 1000;
|
|
103
|
+
const sagLoss = grossWh * Math.min(0.12, 0.02 + 0.015 * (current / (cap / 1000)));
|
|
104
|
+
const totalWh = Math.max(0.1, grossWh - sagLoss);
|
|
105
|
+
const reserveWh = totalWh * reservePct;
|
|
106
|
+
const usableWh = totalWh - reserveWh;
|
|
107
|
+
const baseWatts = volt * current;
|
|
108
|
+
|
|
109
|
+
return {
|
|
110
|
+
totalEnergyWh: totalWh,
|
|
111
|
+
usableEnergyWh: usableWh,
|
|
112
|
+
reserveEnergyWh: reserveWh,
|
|
113
|
+
basePowerDrawWatts: baseWatts,
|
|
114
|
+
totalAutonomyMinutes: (usableWh / baseWatts) * 60,
|
|
115
|
+
voltageSagWhLoss: sagLoss,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export function determineMissionStatus(
|
|
120
|
+
remainingWh: number,
|
|
121
|
+
reserveWh: number,
|
|
122
|
+
_totalEnergyWh: number
|
|
123
|
+
): MissionStatusKey {
|
|
124
|
+
if (remainingWh < 0) return 'exceeded';
|
|
125
|
+
if (remainingWh < reserveWh * 0.5) return 'critical';
|
|
126
|
+
if (remainingWh < reserveWh) return 'tight';
|
|
127
|
+
return 'optimal';
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
interface LegParams {
|
|
131
|
+
distKm: number;
|
|
132
|
+
hoverMin: number;
|
|
133
|
+
baseWatts: number;
|
|
134
|
+
outboundSpeed: number;
|
|
135
|
+
returnSpeed: number;
|
|
136
|
+
outboundPower: number;
|
|
137
|
+
returnPower: number;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function calculateTransitLegs(p: LegParams) {
|
|
141
|
+
const outboundTimeMin = (p.distKm / p.outboundSpeed) * 60;
|
|
142
|
+
const returnTimeMin = (p.distKm / p.returnSpeed) * 60;
|
|
143
|
+
const outboundEnergyWh = (outboundTimeMin / 60) * p.outboundPower;
|
|
144
|
+
const hoverEnergyWh = (p.hoverMin / 60) * p.baseWatts;
|
|
145
|
+
const returnEnergyWh = (returnTimeMin / 60) * p.returnPower;
|
|
146
|
+
|
|
147
|
+
return {
|
|
148
|
+
outboundTimeMin, returnTimeMin, hoverTimeMin: p.hoverMin,
|
|
149
|
+
totalMissionTimeMin: outboundTimeMin + p.hoverMin + returnTimeMin,
|
|
150
|
+
outboundEnergyWh, hoverEnergyWh, returnEnergyWh,
|
|
151
|
+
totalUsedWh: outboundEnergyWh + hoverEnergyWh + returnEnergyWh,
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
interface SafeRadiusParams {
|
|
156
|
+
usableWh: number;
|
|
157
|
+
hoverWh: number;
|
|
158
|
+
outboundPower: number;
|
|
159
|
+
returnPower: number;
|
|
160
|
+
outboundSpeed: number;
|
|
161
|
+
returnSpeed: number;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function calculateSafeRadius(p: SafeRadiusParams): number {
|
|
165
|
+
const transitWh = Math.max(0, p.usableWh - p.hoverWh);
|
|
166
|
+
const perKm = (p.outboundPower / p.outboundSpeed) + (p.returnPower / p.returnSpeed);
|
|
167
|
+
return perKm > 0 ? Math.max(0, transitWh / perKm) : 0;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export function calculateMissionReserve(inputs: MissionInputs): MissionResults {
|
|
171
|
+
const e = calculateEnergyMetrics(inputs);
|
|
172
|
+
const s = computeEffectiveSpeeds(Math.max(1, inputs.cruiseSpeedKmh), Math.max(0, inputs.windSpeedKmh), inputs.windDirection);
|
|
173
|
+
const p = calculateWindPowerPenalty(e.basePowerDrawWatts, Math.max(1, inputs.cruiseSpeedKmh), Math.max(0, inputs.windSpeedKmh), inputs.windDirection);
|
|
174
|
+
const legs = calculateTransitLegs({
|
|
175
|
+
distKm: Math.max(0, inputs.oneWayDistanceKm), hoverMin: Math.max(0, inputs.targetHoverTimeMin),
|
|
176
|
+
baseWatts: e.basePowerDrawWatts, outboundSpeed: s.outbound, returnSpeed: s.returnLeg,
|
|
177
|
+
outboundPower: p.outboundPower, returnPower: p.returnPower,
|
|
178
|
+
});
|
|
179
|
+
const maxSafeRadiusKm = calculateSafeRadius({
|
|
180
|
+
usableWh: e.usableEnergyWh, hoverWh: legs.hoverEnergyWh, outboundPower: p.outboundPower,
|
|
181
|
+
returnPower: p.returnPower, outboundSpeed: s.outbound, returnSpeed: s.returnLeg,
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
const remWh = e.totalEnergyWh - legs.totalUsedWh;
|
|
185
|
+
return {
|
|
186
|
+
totalEnergyWh: e.totalEnergyWh, usableEnergyWh: e.usableEnergyWh, reserveEnergyWh: e.reserveEnergyWh,
|
|
187
|
+
basePowerDrawWatts: e.basePowerDrawWatts, outboundPowerWatts: p.outboundPower, returnPowerWatts: p.returnPower,
|
|
188
|
+
totalAutonomyMinutes: e.totalAutonomyMinutes, outboundSpeedKmh: s.outbound, returnSpeedKmh: s.returnLeg,
|
|
189
|
+
outboundTimeMinutes: legs.outboundTimeMin, targetHoverTimeMinutes: legs.hoverTimeMin, returnTimeMinutes: legs.returnTimeMin,
|
|
190
|
+
totalMissionTimeMinutes: legs.totalMissionTimeMin, outboundEnergyWh: legs.outboundEnergyWh, targetHoverEnergyWh: legs.hoverEnergyWh,
|
|
191
|
+
returnEnergyWh: legs.returnEnergyWh, missionEnergyUsedWh: legs.totalUsedWh, remainingEnergyLandingWh: remWh,
|
|
192
|
+
remainingEnergyLandingPercent: (remWh / e.totalEnergyWh) * 100, maxSafeRadiusKm, voltageSagWhLoss: e.voltageSagWhLoss,
|
|
193
|
+
statusKey: determineMissionStatus(remWh, e.reserveEnergyWh, e.totalEnergyWh),
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
export function convertDistance(km: number, unit: UnitSystem): number {
|
|
198
|
+
return unit === 'imperial' ? km * KM_TO_MILES : km;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
export function convertSpeed(kmh: number, unit: UnitSystem): number {
|
|
202
|
+
return unit === 'imperial' ? kmh * KMH_TO_MPH : kmh;
|
|
203
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
---
|
|
2
|
+
import { SEORenderer } from '@jjlmoya/utils-shared';
|
|
3
|
+
import { droneMissionBatteryReservePlanner } from './entry';
|
|
4
|
+
import type { KnownLocale } from '../../types';
|
|
5
|
+
|
|
6
|
+
interface Props {
|
|
7
|
+
locale?: KnownLocale;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const { locale = 'en' } = Astro.props;
|
|
11
|
+
const content = await droneMissionBatteryReservePlanner.i18n[locale]?.();
|
|
12
|
+
if (!content) return null;
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
{content.seo?.length > 0 && <SEORenderer content={{ locale, sections: content.seo }} />}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import type { MissionInputs, UnitSystem, WindDirection } from './logic';
|
|
2
|
+
|
|
3
|
+
const STORAGE_KEY = 'jjlmoya_drone_mission_battery_reserve_planner_state';
|
|
4
|
+
|
|
5
|
+
export const DEFAULT_MISSION_INPUTS: MissionInputs = {
|
|
6
|
+
unitSystem: 'metric',
|
|
7
|
+
batteryCapacityMah: 10000,
|
|
8
|
+
voltageNominal: 22.2,
|
|
9
|
+
averageCurrentAmps: 18,
|
|
10
|
+
cruiseSpeedKmh: 45,
|
|
11
|
+
oneWayDistanceKm: 6,
|
|
12
|
+
targetHoverTimeMin: 5,
|
|
13
|
+
windSpeedKmh: 12,
|
|
14
|
+
windDirection: 'headwind',
|
|
15
|
+
reservePolicyPercent: 20,
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
function parseNumberField(val: unknown, fallback: number): number {
|
|
19
|
+
return typeof val === 'number' && !Number.isNaN(val) ? val : fallback;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function parseUnitSystem(val: unknown): UnitSystem {
|
|
23
|
+
return val === 'imperial' ? 'imperial' : 'metric';
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function parseWindDirection(val: unknown): WindDirection {
|
|
27
|
+
if (val === 'tailwind' || val === 'crosswind') return val;
|
|
28
|
+
return 'headwind';
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function loadSavedInputs(): MissionInputs {
|
|
32
|
+
try {
|
|
33
|
+
const raw = localStorage.getItem(STORAGE_KEY);
|
|
34
|
+
if (!raw) return DEFAULT_MISSION_INPUTS;
|
|
35
|
+
const parsed = JSON.parse(raw);
|
|
36
|
+
if (!parsed || typeof parsed !== 'object') return DEFAULT_MISSION_INPUTS;
|
|
37
|
+
|
|
38
|
+
return {
|
|
39
|
+
unitSystem: parseUnitSystem(parsed.unitSystem),
|
|
40
|
+
batteryCapacityMah: parseNumberField(parsed.batteryCapacityMah, DEFAULT_MISSION_INPUTS.batteryCapacityMah),
|
|
41
|
+
voltageNominal: parseNumberField(parsed.voltageNominal, DEFAULT_MISSION_INPUTS.voltageNominal),
|
|
42
|
+
averageCurrentAmps: parseNumberField(parsed.averageCurrentAmps, DEFAULT_MISSION_INPUTS.averageCurrentAmps),
|
|
43
|
+
cruiseSpeedKmh: parseNumberField(parsed.cruiseSpeedKmh, DEFAULT_MISSION_INPUTS.cruiseSpeedKmh),
|
|
44
|
+
oneWayDistanceKm: parseNumberField(parsed.oneWayDistanceKm, DEFAULT_MISSION_INPUTS.oneWayDistanceKm),
|
|
45
|
+
targetHoverTimeMin: parseNumberField(parsed.targetHoverTimeMin, DEFAULT_MISSION_INPUTS.targetHoverTimeMin),
|
|
46
|
+
windSpeedKmh: parseNumberField(parsed.windSpeedKmh, DEFAULT_MISSION_INPUTS.windSpeedKmh),
|
|
47
|
+
windDirection: parseWindDirection(parsed.windDirection),
|
|
48
|
+
reservePolicyPercent: parseNumberField(parsed.reservePolicyPercent, DEFAULT_MISSION_INPUTS.reservePolicyPercent),
|
|
49
|
+
};
|
|
50
|
+
} catch {
|
|
51
|
+
return DEFAULT_MISSION_INPUTS;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function saveInputs(inputs: MissionInputs): void {
|
|
56
|
+
try {
|
|
57
|
+
localStorage.setItem(STORAGE_KEY, JSON.stringify(inputs));
|
|
58
|
+
} catch {}
|
|
59
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
export interface DroneMissionBatteryReservePlannerUI {
|
|
2
|
+
[key: string]: unknown;
|
|
3
|
+
title: string;
|
|
4
|
+
subtitle: string;
|
|
5
|
+
description: string;
|
|
6
|
+
sections: {
|
|
7
|
+
batteryPropulsion: string;
|
|
8
|
+
flightAtmosphere: string;
|
|
9
|
+
};
|
|
10
|
+
inputs: {
|
|
11
|
+
unitSystemLabel: string;
|
|
12
|
+
metricLabel: string;
|
|
13
|
+
imperialLabel: string;
|
|
14
|
+
presetLabel: string;
|
|
15
|
+
batteryCapacityLabel: string;
|
|
16
|
+
batteryVoltageLabel: string;
|
|
17
|
+
averageCurrentLabel: string;
|
|
18
|
+
cruiseSpeedLabel: string;
|
|
19
|
+
oneWayDistanceLabel: string;
|
|
20
|
+
targetHoverTimeLabel: string;
|
|
21
|
+
windSpeedLabel: string;
|
|
22
|
+
windDirectionLabel: string;
|
|
23
|
+
windHeadwindLabel: string;
|
|
24
|
+
windTailwindLabel: string;
|
|
25
|
+
windCrosswindLabel: string;
|
|
26
|
+
reservePolicyLabel: string;
|
|
27
|
+
};
|
|
28
|
+
presets: {
|
|
29
|
+
mappingSurvey: string;
|
|
30
|
+
fpvRecon: string;
|
|
31
|
+
cinematicInspection: string;
|
|
32
|
+
microRecon: string;
|
|
33
|
+
surveyMeta: string;
|
|
34
|
+
scoutMeta: string;
|
|
35
|
+
hoverMeta: string;
|
|
36
|
+
};
|
|
37
|
+
results: {
|
|
38
|
+
totalCapacityEnergy: string;
|
|
39
|
+
usableEnergy: string;
|
|
40
|
+
reserveEnergyBuffer: string;
|
|
41
|
+
totalAutonomyTime: string;
|
|
42
|
+
maxSafeMissionRadius: string;
|
|
43
|
+
outboundLegTime: string;
|
|
44
|
+
targetHoverTime: string;
|
|
45
|
+
returnLegTime: string;
|
|
46
|
+
totalMissionTime: string;
|
|
47
|
+
remainingEnergyLanding: string;
|
|
48
|
+
feasibilityStatus: string;
|
|
49
|
+
voltageSagSubLabel: string;
|
|
50
|
+
maxRadiusSubLabel: string;
|
|
51
|
+
powerSubLabel: string;
|
|
52
|
+
};
|
|
53
|
+
statusBadges: {
|
|
54
|
+
optimalTitle: string;
|
|
55
|
+
optimalSubtitle: string;
|
|
56
|
+
tightTitle: string;
|
|
57
|
+
tightSubtitle: string;
|
|
58
|
+
criticalTitle: string;
|
|
59
|
+
criticalSubtitle: string;
|
|
60
|
+
exceededTitle: string;
|
|
61
|
+
exceededSubtitle: string;
|
|
62
|
+
};
|
|
63
|
+
chart: {
|
|
64
|
+
batteryProfileTitle: string;
|
|
65
|
+
modelTitle: string;
|
|
66
|
+
windLabel: string;
|
|
67
|
+
homeNode: string;
|
|
68
|
+
targetNode: string;
|
|
69
|
+
landNode: string;
|
|
70
|
+
launchPadLabel: string;
|
|
71
|
+
surveyHoverLabel: string;
|
|
72
|
+
safeRadiusLabel: string;
|
|
73
|
+
outboundSegment: string;
|
|
74
|
+
targetSegment: string;
|
|
75
|
+
returnSegment: string;
|
|
76
|
+
reserveSegment: string;
|
|
77
|
+
};
|
|
78
|
+
}
|
package/src/tools.ts
CHANGED
|
@@ -8,6 +8,7 @@ import { DRONE_BATTERY_C_RATING_CALCULATOR_TOOL } from './tool/drone-battery-c-r
|
|
|
8
8
|
import { FPV_DRONE_THRUST_TO_WEIGHT_RATIO_TOOL } from './tool/fpv-drone-thrust-to-weight-ratio/index';
|
|
9
9
|
import { DRONE_MOTOR_PROPELLER_CALCULATOR_TOOL } from './tool/drone-motor-propeller-calculator/index';
|
|
10
10
|
import { FPV_DRONE_SPEED_CALCULATOR_TOOL } from './tool/fpv-drone-speed-calculator/index';
|
|
11
|
+
import { DRONE_MISSION_BATTERY_RESERVE_PLANNER_TOOL } from './tool/drone-mission-battery-reserve-planner/index';
|
|
11
12
|
import type { ToolDefinition } from './types';
|
|
12
13
|
|
|
13
14
|
export const ALL_TOOLS: ToolDefinition[] = [
|
|
@@ -20,6 +21,7 @@ export const ALL_TOOLS: ToolDefinition[] = [
|
|
|
20
21
|
FPV_DRONE_THRUST_TO_WEIGHT_RATIO_TOOL,
|
|
21
22
|
DRONE_MOTOR_PROPELLER_CALCULATOR_TOOL,
|
|
22
23
|
FPV_DRONE_SPEED_CALCULATOR_TOOL,
|
|
24
|
+
DRONE_MISSION_BATTERY_RESERVE_PLANNER_TOOL,
|
|
23
25
|
];
|
|
24
26
|
|
|
25
27
|
export {
|
|
@@ -32,4 +34,6 @@ export {
|
|
|
32
34
|
FPV_DRONE_THRUST_TO_WEIGHT_RATIO_TOOL,
|
|
33
35
|
DRONE_MOTOR_PROPELLER_CALCULATOR_TOOL,
|
|
34
36
|
FPV_DRONE_SPEED_CALCULATOR_TOOL,
|
|
37
|
+
DRONE_MISSION_BATTERY_RESERVE_PLANNER_TOOL,
|
|
35
38
|
};
|
|
39
|
+
|
package/src/types.ts
CHANGED
|
@@ -1,12 +1,10 @@
|
|
|
1
1
|
import type { SEOSection } from '@jjlmoya/utils-shared';
|
|
2
|
+
import type { UtilityLocale } from '@jjlmoya/utils-shared/routing';
|
|
2
3
|
import type { WithContext, Thing } from 'schema-dts';
|
|
3
4
|
|
|
4
5
|
export type { SEOSection };
|
|
5
6
|
|
|
6
|
-
export type KnownLocale =
|
|
7
|
-
| 'ar' | 'da' | 'de' | 'en' | 'es' | 'fi'
|
|
8
|
-
| 'fr' | 'id' | 'it' | 'ja' | 'ko' | 'nb' | 'nl'
|
|
9
|
-
| 'pl' | 'pt' | 'ru' | 'sv' | 'tr' | 'zh';
|
|
7
|
+
export type KnownLocale = UtilityLocale;
|
|
10
8
|
|
|
11
9
|
export interface FAQItem {
|
|
12
10
|
question: string;
|
|
@@ -23,7 +21,7 @@ export interface HowToStep {
|
|
|
23
21
|
text: string;
|
|
24
22
|
}
|
|
25
23
|
|
|
26
|
-
export interface ToolLocaleContent<TUI extends
|
|
24
|
+
export interface ToolLocaleContent<TUI extends object = Record<string, string>> {
|
|
27
25
|
slug: string;
|
|
28
26
|
title: string;
|
|
29
27
|
description: string;
|
|
@@ -46,7 +44,7 @@ export type LocaleLoader<T> = () => Promise<T>;
|
|
|
46
44
|
|
|
47
45
|
export type LocaleMap<T> = Partial<Record<KnownLocale, LocaleLoader<T>>>;
|
|
48
46
|
|
|
49
|
-
export interface DronesToolEntry<TUI extends
|
|
47
|
+
export interface DronesToolEntry<TUI extends object = Record<string, string>> {
|
|
50
48
|
id: string;
|
|
51
49
|
icons: {
|
|
52
50
|
bg: string;
|
package/src/worker.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
interface UtilityMfeEnvironment {
|
|
2
|
+
ASSETS: { fetch(request: Request): Promise<Response> };
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
export const LONG_LIVED_ASSET_CACHE = "public, max-age=31536000, immutable";
|
|
6
|
+
|
|
7
|
+
export const getCacheControl = (pathname: string): string | undefined => {
|
|
8
|
+
if (pathname.endsWith("/manifest.json")) return LONG_LIVED_ASSET_CACHE;
|
|
9
|
+
if (pathname.startsWith("/_utilities/")) {
|
|
10
|
+
return LONG_LIVED_ASSET_CACHE;
|
|
11
|
+
}
|
|
12
|
+
return undefined;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export default {
|
|
16
|
+
async fetch(request: Request, environment: UtilityMfeEnvironment): Promise<Response> {
|
|
17
|
+
const response = await environment.ASSETS.fetch(request);
|
|
18
|
+
const cacheControl = getCacheControl(new URL(request.url).pathname);
|
|
19
|
+
if (!cacheControl) return response;
|
|
20
|
+
|
|
21
|
+
const headers = new Headers(response.headers);
|
|
22
|
+
headers.set("Cache-Control", cacheControl);
|
|
23
|
+
return new Response(response.body, { status: response.status, headers });
|
|
24
|
+
},
|
|
25
|
+
};
|
|
@@ -1,161 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
import PreviewLayout from "../../layouts/PreviewLayout.astro";
|
|
3
|
-
import PreviewNavSidebar from "../../components/PreviewNavSidebar.astro";
|
|
4
|
-
import { ALL_TOOLS } from "../../index";
|
|
5
|
-
import {
|
|
6
|
-
UtilityHeader,
|
|
7
|
-
FAQSection,
|
|
8
|
-
Bibliography,
|
|
9
|
-
SEORenderer,
|
|
10
|
-
} from "@jjlmoya/utils-shared";
|
|
11
|
-
import type { KnownLocale, ToolLocaleContent } from "../../types";
|
|
12
|
-
import type { UtilitySEOContent } from "@jjlmoya/utils-shared";
|
|
13
|
-
|
|
14
|
-
export async function getStaticPaths() {
|
|
15
|
-
const paths = [];
|
|
16
|
-
|
|
17
|
-
for (const { entry, Component: lazyComp } of ALL_TOOLS) {
|
|
18
|
-
const { default: Component } = await lazyComp();
|
|
19
|
-
const localeEntries = Object.entries(entry.i18n) as [
|
|
20
|
-
KnownLocale,
|
|
21
|
-
() => Promise<ToolLocaleContent>,
|
|
22
|
-
][];
|
|
23
|
-
const localeContents = await Promise.all(
|
|
24
|
-
localeEntries.map(async ([locale, loader]) => ({
|
|
25
|
-
locale,
|
|
26
|
-
content: await loader(),
|
|
27
|
-
})),
|
|
28
|
-
);
|
|
29
|
-
|
|
30
|
-
const localeUrls = Object.fromEntries(
|
|
31
|
-
localeContents.map(({ locale, content }) => [
|
|
32
|
-
locale,
|
|
33
|
-
`/${locale}/${content.slug}`,
|
|
34
|
-
]),
|
|
35
|
-
) as Partial<Record<KnownLocale, string>>;
|
|
36
|
-
|
|
37
|
-
const firstLoader = entry.i18n.en ?? Object.values(entry.i18n)[0];
|
|
38
|
-
const englishSlug = firstLoader ? (await firstLoader()).slug : entry.id;
|
|
39
|
-
|
|
40
|
-
for (const { locale, content } of localeContents) {
|
|
41
|
-
const allToolsNav = (
|
|
42
|
-
await Promise.all(
|
|
43
|
-
ALL_TOOLS.map(async ({ entry: navEntry }) => {
|
|
44
|
-
const loader = navEntry.i18n[locale] ?? navEntry.i18n.en;
|
|
45
|
-
if (!loader) return null;
|
|
46
|
-
const navContent = await loader();
|
|
47
|
-
return {
|
|
48
|
-
id: navEntry.id,
|
|
49
|
-
title: navContent.title,
|
|
50
|
-
href: `/${locale}/${navContent.slug}`,
|
|
51
|
-
isActive: navEntry.id === entry.id,
|
|
52
|
-
};
|
|
53
|
-
}),
|
|
54
|
-
)
|
|
55
|
-
).filter(Boolean) as NavItem[];
|
|
56
|
-
paths.push({
|
|
57
|
-
params: { locale, slug: content.slug },
|
|
58
|
-
props: { Component, locale, content, localeUrls, allToolsNav, englishSlug },
|
|
59
|
-
});
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
return paths;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
interface NavItem {
|
|
67
|
-
id: string;
|
|
68
|
-
title: string;
|
|
69
|
-
href: string;
|
|
70
|
-
isActive?: boolean;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
interface Props {
|
|
74
|
-
Component: unknown;
|
|
75
|
-
locale: KnownLocale;
|
|
76
|
-
content: ToolLocaleContent;
|
|
77
|
-
localeUrls: Partial<Record<KnownLocale, string>>;
|
|
78
|
-
allToolsNav: NavItem[];
|
|
79
|
-
englishSlug: string;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
const { Component, locale, content, localeUrls, allToolsNav, englishSlug } = Astro.props;
|
|
83
|
-
|
|
84
|
-
const cssFiles = import.meta.glob("../../tool/*/*.css", { query: "?raw", import: "default" });
|
|
85
|
-
const cssKey = Object.keys(cssFiles).find((k) => k.endsWith(`/${englishSlug}.css`));
|
|
86
|
-
const toolCss = cssKey ? await cssFiles[cssKey]() as string : "";
|
|
87
|
-
|
|
88
|
-
const seoContent: UtilitySEOContent = { locale, sections: content.seo ?? [] };
|
|
89
|
-
|
|
90
|
-
const words = content.title.split(" ");
|
|
91
|
-
const titleHighlight = words[0] || "";
|
|
92
|
-
const titleBase = words.slice(1).join(" ") || "";
|
|
93
|
-
---
|
|
94
|
-
|
|
95
|
-
<PreviewLayout
|
|
96
|
-
title={content.title}
|
|
97
|
-
currentLocale={locale}
|
|
98
|
-
localeUrls={localeUrls}
|
|
99
|
-
hasSidebar={true}
|
|
100
|
-
>
|
|
101
|
-
<PreviewNavSidebar
|
|
102
|
-
slot="sidebar"
|
|
103
|
-
categoryTitle="Tools"
|
|
104
|
-
tools={allToolsNav}
|
|
105
|
-
/>
|
|
106
|
-
<Fragment slot="head">
|
|
107
|
-
{toolCss ? <Fragment set:html={`<style is:inline>${toolCss}</style>`} /> : null}
|
|
108
|
-
{
|
|
109
|
-
( content.schemas ?? []).map((schema: unknown) => (
|
|
110
|
-
<script
|
|
111
|
-
is:inline
|
|
112
|
-
type="application/ld+json"
|
|
113
|
-
set:html={JSON.stringify(schema)}
|
|
114
|
-
/>
|
|
115
|
-
))
|
|
116
|
-
}
|
|
117
|
-
</Fragment>
|
|
118
|
-
|
|
119
|
-
<div class="tool-page">
|
|
120
|
-
<UtilityHeader
|
|
121
|
-
titleHighlight={titleHighlight}
|
|
122
|
-
titleBase={titleBase}
|
|
123
|
-
description={content.description}
|
|
124
|
-
/>
|
|
125
|
-
|
|
126
|
-
<section class="section-tool">
|
|
127
|
-
<Component ui={content.ui} />
|
|
128
|
-
</section>
|
|
129
|
-
|
|
130
|
-
<section class="section-seo">
|
|
131
|
-
<SEORenderer content={seoContent} />
|
|
132
|
-
</section>
|
|
133
|
-
|
|
134
|
-
<section class="section-faq">
|
|
135
|
-
<FAQSection items={content.faq} inLanguage={locale} />
|
|
136
|
-
</section>
|
|
137
|
-
|
|
138
|
-
<section class="section-bibliography">
|
|
139
|
-
<Bibliography links={content.bibliography} />
|
|
140
|
-
</section>
|
|
141
|
-
</div>
|
|
142
|
-
</PreviewLayout>
|
|
143
|
-
|
|
144
|
-
<style>
|
|
145
|
-
.tool-page {
|
|
146
|
-
display: flex;
|
|
147
|
-
flex-direction: column;
|
|
148
|
-
gap: 2rem;
|
|
149
|
-
}
|
|
150
|
-
.section-tool {
|
|
151
|
-
max-width: 1200px;
|
|
152
|
-
margin: 0 auto;
|
|
153
|
-
width: 100%;
|
|
154
|
-
}
|
|
155
|
-
.section-seo,
|
|
156
|
-
.section-faq,
|
|
157
|
-
.section-bibliography {
|
|
158
|
-
padding-top: 2rem;
|
|
159
|
-
border-top: 1px solid var(--border-color);
|
|
160
|
-
}
|
|
161
|
-
</style>
|