@jjlmoya/utils-drones 1.15.0 → 1.17.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.
Files changed (118) hide show
  1. package/package.json +1 -1
  2. package/src/category/index.ts +2 -1
  3. package/src/entries.ts +7 -1
  4. package/src/index.ts +2 -0
  5. package/src/pages/[locale]/[slug].astro +30 -14
  6. package/src/tests/locale_completeness.test.ts +4 -22
  7. package/src/tests/shared-test-helpers.ts +56 -0
  8. package/src/tests/tool_exports.test.ts +34 -0
  9. package/src/tests/tool_validation.test.ts +2 -2
  10. package/src/tool/antenna-length-calculator/bibliography.astro +3 -11
  11. package/src/tool/antenna-length-calculator/bibliography.ts +6 -0
  12. package/src/tool/antenna-length-calculator/i18n/de.ts +2 -8
  13. package/src/tool/antenna-length-calculator/i18n/en.ts +2 -8
  14. package/src/tool/antenna-length-calculator/i18n/es.ts +2 -8
  15. package/src/tool/antenna-length-calculator/i18n/fr.ts +2 -8
  16. package/src/tool/antenna-length-calculator/i18n/id.ts +2 -8
  17. package/src/tool/antenna-length-calculator/i18n/it.ts +2 -8
  18. package/src/tool/antenna-length-calculator/i18n/ja.ts +2 -8
  19. package/src/tool/antenna-length-calculator/i18n/ko.ts +2 -8
  20. package/src/tool/antenna-length-calculator/i18n/nl.ts +2 -8
  21. package/src/tool/antenna-length-calculator/i18n/pl.ts +2 -8
  22. package/src/tool/antenna-length-calculator/i18n/pt.ts +2 -8
  23. package/src/tool/antenna-length-calculator/i18n/ru.ts +2 -8
  24. package/src/tool/antenna-length-calculator/i18n/sv.ts +2 -8
  25. package/src/tool/antenna-length-calculator/i18n/tr.ts +2 -8
  26. package/src/tool/antenna-length-calculator/i18n/zh.ts +2 -8
  27. package/src/tool/antenna-length-calculator/seo.astro +2 -26
  28. package/src/tool/drone-flight-time/bibliography.astro +3 -11
  29. package/src/tool/drone-flight-time/bibliography.ts +7 -0
  30. package/src/tool/drone-flight-time/i18n/de.ts +2 -9
  31. package/src/tool/drone-flight-time/i18n/en.ts +2 -9
  32. package/src/tool/drone-flight-time/i18n/es.ts +2 -9
  33. package/src/tool/drone-flight-time/i18n/fr.ts +2 -9
  34. package/src/tool/drone-flight-time/i18n/id.ts +2 -9
  35. package/src/tool/drone-flight-time/i18n/it.ts +2 -9
  36. package/src/tool/drone-flight-time/i18n/ja.ts +2 -9
  37. package/src/tool/drone-flight-time/i18n/ko.ts +2 -9
  38. package/src/tool/drone-flight-time/i18n/nl.ts +2 -9
  39. package/src/tool/drone-flight-time/i18n/pl.ts +2 -9
  40. package/src/tool/drone-flight-time/i18n/pt.ts +2 -9
  41. package/src/tool/drone-flight-time/i18n/ru.ts +2 -9
  42. package/src/tool/drone-flight-time/i18n/sv.ts +2 -9
  43. package/src/tool/drone-flight-time/i18n/tr.ts +2 -9
  44. package/src/tool/drone-flight-time/i18n/zh.ts +2 -9
  45. package/src/tool/drone-flight-time/seo.astro +2 -18
  46. package/src/tool/drone-power-analyzer/bibliography.astro +6 -0
  47. package/src/tool/drone-power-analyzer/bibliography.ts +7 -0
  48. package/src/tool/drone-power-analyzer/component.astro +210 -0
  49. package/src/tool/drone-power-analyzer/components/FlightProfile.astro +37 -0
  50. package/src/tool/drone-power-analyzer/components/MotorConfig.astro +46 -0
  51. package/src/tool/drone-power-analyzer/components/PowerGauge.astro +48 -0
  52. package/src/tool/drone-power-analyzer/components/WeightConfig.astro +49 -0
  53. package/src/tool/drone-power-analyzer/drone-power-analyzer.css +535 -0
  54. package/src/tool/drone-power-analyzer/entry.ts +29 -0
  55. package/src/tool/drone-power-analyzer/i18n/de.ts +203 -0
  56. package/src/tool/drone-power-analyzer/i18n/en.ts +203 -0
  57. package/src/tool/drone-power-analyzer/i18n/es.ts +203 -0
  58. package/src/tool/drone-power-analyzer/i18n/fr.ts +203 -0
  59. package/src/tool/drone-power-analyzer/i18n/id.ts +203 -0
  60. package/src/tool/drone-power-analyzer/i18n/it.ts +203 -0
  61. package/src/tool/drone-power-analyzer/i18n/ja.ts +203 -0
  62. package/src/tool/drone-power-analyzer/i18n/ko.ts +203 -0
  63. package/src/tool/drone-power-analyzer/i18n/nl.ts +203 -0
  64. package/src/tool/drone-power-analyzer/i18n/pl.ts +203 -0
  65. package/src/tool/drone-power-analyzer/i18n/pt.ts +203 -0
  66. package/src/tool/drone-power-analyzer/i18n/ru.ts +203 -0
  67. package/src/tool/drone-power-analyzer/i18n/sv.ts +203 -0
  68. package/src/tool/drone-power-analyzer/i18n/tr.ts +203 -0
  69. package/src/tool/drone-power-analyzer/i18n/zh.ts +203 -0
  70. package/src/tool/drone-power-analyzer/index.ts +8 -0
  71. package/src/tool/drone-power-analyzer/seo.astro +15 -0
  72. package/src/tool/gps-coordinates-converter/bibliography.astro +3 -11
  73. package/src/tool/gps-coordinates-converter/bibliography.ts +12 -0
  74. package/src/tool/gps-coordinates-converter/components/GpsMap.astro +3 -2
  75. package/src/tool/gps-coordinates-converter/gps-coordinates-converter.css +197 -119
  76. package/src/tool/gps-coordinates-converter/i18n/de.ts +35 -36
  77. package/src/tool/gps-coordinates-converter/i18n/en.ts +4 -14
  78. package/src/tool/gps-coordinates-converter/i18n/es.ts +4 -14
  79. package/src/tool/gps-coordinates-converter/i18n/fr.ts +4 -14
  80. package/src/tool/gps-coordinates-converter/i18n/id.ts +36 -37
  81. package/src/tool/gps-coordinates-converter/i18n/it.ts +35 -36
  82. package/src/tool/gps-coordinates-converter/i18n/ja.ts +19 -20
  83. package/src/tool/gps-coordinates-converter/i18n/ko.ts +20 -21
  84. package/src/tool/gps-coordinates-converter/i18n/nl.ts +33 -36
  85. package/src/tool/gps-coordinates-converter/i18n/pl.ts +19 -20
  86. package/src/tool/gps-coordinates-converter/i18n/pt.ts +19 -20
  87. package/src/tool/gps-coordinates-converter/i18n/ru.ts +19 -20
  88. package/src/tool/gps-coordinates-converter/i18n/sv.ts +19 -20
  89. package/src/tool/gps-coordinates-converter/i18n/tr.ts +19 -20
  90. package/src/tool/gps-coordinates-converter/i18n/zh.ts +20 -21
  91. package/src/tool/gps-coordinates-converter/seo.astro +2 -26
  92. package/src/tool/gsd-flight-planner/InputsColumn.astro +108 -0
  93. package/src/tool/gsd-flight-planner/ResultsColumn.astro +90 -0
  94. package/src/tool/gsd-flight-planner/bibliography.astro +6 -0
  95. package/src/tool/gsd-flight-planner/bibliography.ts +7 -0
  96. package/src/tool/gsd-flight-planner/component.astro +22 -0
  97. package/src/tool/gsd-flight-planner/entry.ts +29 -0
  98. package/src/tool/gsd-flight-planner/gsd-flight-planner.css +515 -0
  99. package/src/tool/gsd-flight-planner/i18n/de.ts +214 -0
  100. package/src/tool/gsd-flight-planner/i18n/en.ts +214 -0
  101. package/src/tool/gsd-flight-planner/i18n/es.ts +221 -0
  102. package/src/tool/gsd-flight-planner/i18n/fr.ts +214 -0
  103. package/src/tool/gsd-flight-planner/i18n/id.ts +214 -0
  104. package/src/tool/gsd-flight-planner/i18n/it.ts +214 -0
  105. package/src/tool/gsd-flight-planner/i18n/ja.ts +214 -0
  106. package/src/tool/gsd-flight-planner/i18n/ko.ts +214 -0
  107. package/src/tool/gsd-flight-planner/i18n/nl.ts +214 -0
  108. package/src/tool/gsd-flight-planner/i18n/pl.ts +214 -0
  109. package/src/tool/gsd-flight-planner/i18n/pt.ts +214 -0
  110. package/src/tool/gsd-flight-planner/i18n/ru.ts +214 -0
  111. package/src/tool/gsd-flight-planner/i18n/sv.ts +214 -0
  112. package/src/tool/gsd-flight-planner/i18n/tr.ts +214 -0
  113. package/src/tool/gsd-flight-planner/i18n/zh.ts +214 -0
  114. package/src/tool/gsd-flight-planner/index.ts +8 -0
  115. package/src/tool/gsd-flight-planner/logic.js +326 -0
  116. package/src/tool/gsd-flight-planner/seo.astro +15 -0
  117. package/src/tools.ts +6 -0
  118. package/src/types.ts +1 -3
@@ -0,0 +1,326 @@
1
+ const cameraPresets = {
2
+ mavic3e: { sensorW: 17.3, sensorH: 13, imgW: 5280, imgH: 3956, focal: 24 },
3
+ matrice300: { sensorW: 35.9, sensorH: 24, imgW: 8688, imgH: 5792, focal: 35 },
4
+ autelevoii: { sensorW: 21.3, sensorH: 12, imgW: 6000, imgH: 3376, focal: 20 },
5
+ air3s: { sensorW: 21.3, sensorH: 12, imgW: 6000, imgH: 3376, focal: 21.1 },
6
+ };
7
+ let currentUnit = 'metric';
8
+ function loadStateFromParams(params) {
9
+ if (!params.has('sensorW')) return;
10
+ sensorWidthInput.value = params.get('sensorW');
11
+ sensorHeightInput.value = params.get('sensorH');
12
+ focalLengthInput.value = params.get('focalLength');
13
+ imageWidthInput.value = params.get('imgW');
14
+ imageHeightInput.value = params.get('imgH');
15
+ altitudeInput.value = altitudeSlider.value = params.get('altitude');
16
+ forwardOverlapInput.value = params.get('forward');
17
+ lateralOverlapInput.value = params.get('lateral');
18
+ missionAreaInput.value = params.get('area');
19
+ if (params.has('unit') && params.get('unit') === 'imperial') imperialBtn.click();
20
+ }
21
+ function applyStoredState(state) {
22
+ const updates = [[sensorWidthInput, 'sensorW'], [sensorHeightInput, 'sensorH'], [focalLengthInput, 'focalLength'], [imageWidthInput, 'imgW'], [imageHeightInput, 'imgH'], [forwardOverlapInput, 'forward'], [lateralOverlapInput, 'lateral'], [missionAreaInput, 'area']];
23
+ updates.forEach(([input, key]) => { if (state[key]) input.value = state[key]; });
24
+ if (state.altitude) altitudeInput.value = altitudeSlider.value = state.altitude;
25
+ }
26
+ function loadStateFromStorage() {
27
+ const saved = localStorage.getItem('gsd-planner-state');
28
+ if (saved) try { applyStoredState(JSON.parse(saved)); } catch {}
29
+ if (localStorage.getItem('gsd-planner-unit') === 'imperial') imperialBtn.click();
30
+ }
31
+ function loadState() {
32
+ const params = new URLSearchParams(window.location.search);
33
+ if (params.size > 0) loadStateFromParams(params);
34
+ else loadStateFromStorage();
35
+ }
36
+ function saveState() {
37
+ const state = {
38
+ sensorW: sensorWidthInput.value,
39
+ sensorH: sensorHeightInput.value,
40
+ focalLength: focalLengthInput.value,
41
+ imgW: imageWidthInput.value,
42
+ imgH: imageHeightInput.value,
43
+ altitude: altitudeInput.value,
44
+ forward: forwardOverlapInput.value,
45
+ lateral: lateralOverlapInput.value,
46
+ area: missionAreaInput.value,
47
+ unit: currentUnit,
48
+ };
49
+ localStorage.setItem('gsd-planner-state', JSON.stringify(state));
50
+ localStorage.setItem('gsd-planner-unit', currentUnit);
51
+ }
52
+ function getShareUrl() {
53
+ const params = new URLSearchParams({
54
+ sensorW: sensorWidthInput.value,
55
+ sensorH: sensorHeightInput.value,
56
+ focalLength: focalLengthInput.value,
57
+ imgW: imageWidthInput.value,
58
+ imgH: imageHeightInput.value,
59
+ altitude: altitudeInput.value,
60
+ forward: forwardOverlapInput.value,
61
+ lateral: lateralOverlapInput.value,
62
+ area: missionAreaInput.value,
63
+ unit: currentUnit,
64
+ });
65
+ const url = new URL(window.location.href);
66
+ url.search = params.toString();
67
+ return url.toString();
68
+ }
69
+ function updateUnits() {
70
+ altUnit.textContent = currentUnit === 'metric' ? 'm' : 'ft';
71
+ areaUnit.textContent = currentUnit === 'metric' ? 'm²' : 'ft²';
72
+ spacingUnit.textContent = currentUnit === 'metric' ? 'm' : 'ft';
73
+ areaInputUnit.textContent = currentUnit === 'metric' ? 'm²' : 'ft²';
74
+ gsdUnitLabel.textContent = currentUnit === 'metric' ? 'cm/px' : 'in/px';
75
+ const cA = parseFloat(missionAreaInput.value) || 0;
76
+ missionAreaInput.value = (currentUnit === 'imperial' ? cA * 10.764 : cA / 10.764).toFixed(1);
77
+ }
78
+ function updateLabels() {
79
+ const u = currentUnit === 'metric';
80
+ const uI = (input) => {
81
+ const h = input.parentElement?.parentElement?.querySelector('.unit-hint');
82
+ if (h) h.textContent = u ? 'mm' : 'in';
83
+ };
84
+ uI(sensorWidthInput);
85
+ uI(sensorHeightInput);
86
+ focalLengthUnit.textContent = u ? 'mm' : 'in';
87
+ desiredGsdUnit.textContent = u ? 'cm/px' : 'in/px';
88
+ }
89
+ function updateSvgVisualization(fW, fH, fO) {
90
+ if (fW <= 0 || fH <= 0) return;
91
+ const sW = 220, sH = 100, s = Math.min((sW - 40) / fW, (sH - 40) / fW);
92
+ const rW = Math.max(20, Math.min(160, fW * s));
93
+ const rH = Math.max(10, Math.min(60, fH * s));
94
+ const p1X = 20, p1Y = (sH - rH) / 2;
95
+ const oW = rW * (fO / 100), p2X = p1X + rW - oW;
96
+ photo1Rect.setAttribute('x', p1X.toString());
97
+ photo1Rect.setAttribute('y', p1Y.toString());
98
+ photo1Rect.setAttribute('width', rW.toString());
99
+ photo1Rect.setAttribute('height', rH.toString());
100
+ photo2Rect.setAttribute('x', p2X.toString());
101
+ photo2Rect.setAttribute('y', p1Y.toString());
102
+ photo2Rect.setAttribute('width', rW.toString());
103
+ photo2Rect.setAttribute('height', rH.toString());
104
+ overlapRect.setAttribute('x', p2X.toString());
105
+ overlapRect.setAttribute('y', p1Y.toString());
106
+ overlapRect.setAttribute('width', oW.toString());
107
+ overlapRect.setAttribute('height', rH.toString());
108
+ overlapText.setAttribute('x', (p2X + oW / 2).toString());
109
+ overlapText.setAttribute('y', (p1Y + rH / 2 + 3).toString());
110
+ overlapText.textContent = fO + '%';
111
+ }
112
+ function updateAlerts(g, fO, tS) {
113
+ alertsContainer.innerHTML = '';
114
+ if (g < 0.5) addAlert(ui.ultraHighResAlert || 'Ultra-high resolution');
115
+ if (fO < 60) addAlert(ui.lowOverlapAlert || 'Forward overlap below 60%');
116
+ if (tS > 5000) addAlert(ui.largeDatasetAlert || 'Large dataset');
117
+ }
118
+ function calculateMissionMetrics(fW, fO, lO, a) {
119
+ const lN = Math.ceil(a / (fW * (1 - lO / 100)));
120
+ const sM = fW * (1 - fO / 100);
121
+ const sPL = Math.ceil(Math.sqrt(a) / sM);
122
+ const tS = Math.ceil(sPL * lN);
123
+ return [lN, sPL, tS, Math.ceil(tS * 2.5 / 60)];
124
+ }
125
+ function displayCoverage(p) {
126
+ let c = p.w * p.h;
127
+ if (currentUnit === 'imperial') c *= 10.764;
128
+ areaDisplay.textContent = c.toFixed(0);
129
+ let s = p.w * (1 - p.fwd / 100);
130
+ if (currentUnit === 'imperial') s *= 3.28084;
131
+ spacingDisplay.textContent = s.toFixed(1);
132
+ const [l, , t, tm] = calculateMissionMetrics(p.w, p.fwd, p.lat, p.area);
133
+ totalImages.textContent = t.toString();
134
+ flightLines.textContent = l.toString();
135
+ flightTime.textContent = tm.toString();
136
+ dataVolume.textContent = (t * 20 / 1024).toFixed(1);
137
+ }
138
+ function parseNum(v, d) {
139
+ const n = parseFloat(v);
140
+ return Number.isNaN(n) ? d : n;
141
+ }
142
+ function getInputValues() {
143
+ return {
144
+ sensorW: parseNum(sensorWidthInput.value, 0),
145
+ sensorH: parseNum(sensorHeightInput.value, 0),
146
+ imgW: parseNum(imageWidthInput.value, 1),
147
+ focal: parseNum(focalLengthInput.value, 1),
148
+ fwd: parseNum(forwardOverlapInput.value, 0),
149
+ lat: parseNum(lateralOverlapInput.value, 0),
150
+ alt: parseNum(altitudeInput.value, 0),
151
+ area: parseNum(missionAreaInput.value, 100),
152
+ };
153
+ }
154
+ function calculate() {
155
+ const v = getInputValues();
156
+ const imp = currentUnit === 'imperial';
157
+ const a = imp ? v.alt * 0.3048 : v.alt;
158
+ const ar = imp ? v.area / 10.764 : v.area;
159
+ const g = (a * v.sensorW) / (v.focal * v.imgW) * 100;
160
+ gsdNumberLarge.textContent = g.toFixed(2);
161
+ updatePrecisionScale(g);
162
+ const fW = (a * v.sensorW) / v.focal;
163
+ const fH = (a * v.sensorH) / v.focal;
164
+ displayCoverage({ w: fW, h: fH, fwd: v.fwd, lat: v.lat, area: ar });
165
+ updateSvgVisualization(fW, fH, v.fwd);
166
+ const [, , t] = calculateMissionMetrics(fW, v.fwd, v.lat, ar);
167
+ updateAlerts(g, v.fwd, t);
168
+ }
169
+ function updatePrecisionScale(g) {
170
+ const cG = Math.max(0.5, Math.min(g, 20));
171
+ const p = ((cG - 0.5) / 19.5) * 100;
172
+ scaleFill.style.width = p + '%';
173
+ let c = 'Unknown';
174
+ if (g <= 2) c = ui.classHighPrecision || 'High';
175
+ else if (g <= 5) c = ui.classStandard || 'Standard';
176
+ else if (g <= 10) c = ui.classInspection || 'Inspection';
177
+ else c = ui.classVisual || 'Visual';
178
+ classificationBadge.textContent = c;
179
+ }
180
+ function addAlert(m) {
181
+ const a = document.createElement('div');
182
+ a.className = 'alert';
183
+ a.textContent = m;
184
+ alertsContainer.appendChild(a);
185
+ }
186
+ const cameraPresetSelect = document.getElementById('cameraPreset');
187
+ const sensorWidthInput = document.getElementById('sensorWidth');
188
+ const sensorHeightInput = document.getElementById('sensorHeight');
189
+ const imageWidthInput = document.getElementById('imageWidth');
190
+ const imageHeightInput = document.getElementById('imageHeight');
191
+ const focalLengthInput = document.getElementById('focalLength');
192
+ const altitudeInput = document.getElementById('altitudeAgl');
193
+ const altitudeSlider = document.getElementById('altitudeSlider');
194
+ const forwardOverlapInput = document.getElementById('forwardOverlap');
195
+ const lateralOverlapInput = document.getElementById('lateralOverlap');
196
+ const desiredGsdInput = document.getElementById('desiredGsd');
197
+ const missionAreaInput = document.getElementById('missionArea');
198
+ const gsdNumberLarge = document.getElementById('gsdNumberLarge');
199
+ const scaleFill = document.getElementById('scaleFill');
200
+ const classificationBadge = document.getElementById('classificationBadge');
201
+ const totalImages = document.getElementById('totalImages');
202
+ const flightLines = document.getElementById('flightLines');
203
+ const flightTime = document.getElementById('flightTime');
204
+ const dataVolume = document.getElementById('dataVolume');
205
+ const areaDisplay = document.getElementById('areaDisplay');
206
+ const spacingDisplay = document.getElementById('spacingDisplay');
207
+ const areaUnit = document.getElementById('areaUnit');
208
+ const spacingUnit = document.getElementById('spacingUnit');
209
+ const altUnit = document.getElementById('altUnit');
210
+ const areaInputUnit = document.getElementById('areaInputUnit');
211
+ const focalLengthUnit = document.getElementById('focalLengthUnit');
212
+ const desiredGsdUnit = document.getElementById('desiredGsdUnit');
213
+ const maxAltResult = document.getElementById('maxAltResult');
214
+ const gsdUnitLabel = document.getElementById('gsdUnitLabel');
215
+ const alertsContainer = document.getElementById('alertsContainer');
216
+ const photo1Rect = document.getElementById('photo1Rect');
217
+ const photo2Rect = document.getElementById('photo2Rect');
218
+ const overlapRect = document.getElementById('overlapRect');
219
+ const overlapText = document.getElementById('overlapText');
220
+ const metricBtn = document.getElementById('metricBtn');
221
+ const imperialBtn = document.getElementById('imperialBtn');
222
+ const resetBtn = document.getElementById('resetBtn');
223
+ const shareBtn = document.getElementById('shareBtn');
224
+ const exportBtn = document.getElementById('exportBtn');
225
+ altitudeInput.addEventListener('input', () => {
226
+ altitudeSlider.value = altitudeInput.value;
227
+ calculate();
228
+ saveState();
229
+ });
230
+ altitudeSlider.addEventListener('input', () => {
231
+ altitudeInput.value = altitudeSlider.value;
232
+ calculate();
233
+ saveState();
234
+ });
235
+ cameraPresetSelect.addEventListener('change', () => {
236
+ const p = cameraPresets[cameraPresetSelect.value];
237
+ if (p) {
238
+ sensorWidthInput.value = p.sensorW;
239
+ sensorHeightInput.value = p.sensorH;
240
+ imageWidthInput.value = p.imgW;
241
+ imageHeightInput.value = p.imgH;
242
+ focalLengthInput.value = p.focal;
243
+ calculate();
244
+ saveState();
245
+ }
246
+ });
247
+ [sensorWidthInput, sensorHeightInput, imageWidthInput, imageHeightInput, focalLengthInput, forwardOverlapInput, lateralOverlapInput, missionAreaInput].forEach(input => {
248
+ input.addEventListener('input', () => {
249
+ calculate();
250
+ saveState();
251
+ });
252
+ });
253
+ desiredGsdInput.addEventListener('input', () => {
254
+ const sW = parseFloat(sensorWidthInput.value) || 0;
255
+ const f = parseFloat(focalLengthInput.value) || 1;
256
+ const iW = parseFloat(imageWidthInput.value) || 1;
257
+ const dG = parseFloat(desiredGsdInput.value) || 1;
258
+ const mA = (dG * f * iW) / (sW * 100);
259
+ let aV = mA;
260
+ if (currentUnit === 'imperial') aV = mA * 3.28084;
261
+ altitudeInput.value = aV.toFixed(0);
262
+ altitudeSlider.value = altitudeInput.value;
263
+ const dA = currentUnit === 'metric' ? mA : mA * 3.28084;
264
+ maxAltResult.textContent = dA.toFixed(2) + ' ' + (currentUnit === 'metric' ? 'm' : 'ft');
265
+ calculate();
266
+ saveState();
267
+ });
268
+ metricBtn.addEventListener('click', () => {
269
+ if (currentUnit === 'metric') return;
270
+ currentUnit = 'metric';
271
+ metricBtn.classList.add('active');
272
+ imperialBtn.classList.remove('active');
273
+ sensorWidthInput.value = (parseFloat(sensorWidthInput.value) / 0.03937).toFixed(1);
274
+ sensorHeightInput.value = (parseFloat(sensorHeightInput.value) / 0.03937).toFixed(1);
275
+ altitudeInput.value = (parseFloat(altitudeInput.value) * 0.3048).toFixed(0);
276
+ altitudeSlider.value = altitudeInput.value;
277
+ altitudeInput.max = '500';
278
+ altitudeSlider.max = '500';
279
+ updateUnits();
280
+ updateLabels();
281
+ calculate();
282
+ saveState();
283
+ });
284
+ imperialBtn.addEventListener('click', () => {
285
+ if (currentUnit === 'imperial') return;
286
+ currentUnit = 'imperial';
287
+ imperialBtn.classList.add('active');
288
+ metricBtn.classList.remove('active');
289
+ sensorWidthInput.value = (parseFloat(sensorWidthInput.value) * 0.03937).toFixed(2);
290
+ sensorHeightInput.value = (parseFloat(sensorHeightInput.value) * 0.03937).toFixed(2);
291
+ altitudeInput.value = (parseFloat(altitudeInput.value) / 0.3048).toFixed(0);
292
+ altitudeSlider.value = altitudeInput.value;
293
+ altitudeInput.max = '1640';
294
+ altitudeSlider.max = '1640';
295
+ updateUnits();
296
+ updateLabels();
297
+ calculate();
298
+ saveState();
299
+ });
300
+ resetBtn.addEventListener('click', () => location.reload());
301
+ shareBtn.addEventListener('click', async () => {
302
+ const sU = getShareUrl();
303
+ try {
304
+ await navigator.clipboard.writeText(sU);
305
+ const o = shareBtn.textContent;
306
+ shareBtn.textContent = ui.copiedToClipboard || 'Copied!';
307
+ setTimeout(() => {
308
+ shareBtn.textContent = o;
309
+ }, 2000);
310
+ } catch {
311
+ alert(sU);
312
+ }
313
+ });
314
+ exportBtn.addEventListener('click', () => {
315
+ const g = parseFloat(gsdNumberLarge.textContent || '0');
316
+ const rT = `GSD\n${sensorWidthInput.value}x${sensorHeightInput.value}mm\n${imageWidthInput.value}x${imageHeightInput.value}px\nAlt: ${altitudeInput.value}${altUnit.textContent}\nGSD: ${g.toFixed(2)}\nArea: ${areaDisplay.textContent}\nImages: ${totalImages.textContent}\n${new Date().toLocaleString()}`;
317
+ const b = new Blob([rT], { type: 'text/plain' });
318
+ const u = URL.createObjectURL(b);
319
+ const el = document.createElement('a');
320
+ el.href = u;
321
+ el.download = `gsd-${Date.now()}.txt`;
322
+ el.click();
323
+ });
324
+ loadState();
325
+ calculate();
326
+ desiredGsdInput.dispatchEvent(new Event('input'));
@@ -0,0 +1,15 @@
1
+ ---
2
+ import { SEORenderer } from '@jjlmoya/utils-shared';
3
+ import { gsdFlightPlanner } from './index';
4
+ import type { KnownLocale } from '../../types';
5
+
6
+ interface Props {
7
+ locale?: KnownLocale;
8
+ }
9
+
10
+ const { locale = 'es' } = Astro.props;
11
+ const content = await gsdFlightPlanner.i18n[locale]?.();
12
+ if (!content) return null;
13
+ ---
14
+
15
+ {content.seo?.length > 0 && <SEORenderer content={{ locale, sections: content.seo }} />}
package/src/tools.ts CHANGED
@@ -2,17 +2,23 @@ export { ALL_ENTRIES } from './entries';
2
2
  import { DRONE_FLIGHT_TIME_TOOL } from './tool/drone-flight-time/index';
3
3
  import { ANTENNA_LENGTH_CALCULATOR_TOOL } from './tool/antenna-length-calculator/index';
4
4
  import { GPS_COORDINATES_CONVERTER_TOOL } from './tool/gps-coordinates-converter/index';
5
+ import { DRONE_POWER_ANALYZER_TOOL } from './tool/drone-power-analyzer/index';
6
+ import { GSD_FLIGHT_PLANNER_TOOL } from './tool/gsd-flight-planner/index';
5
7
  import type { ToolDefinition } from './types';
6
8
 
7
9
  export const ALL_TOOLS: ToolDefinition[] = [
8
10
  DRONE_FLIGHT_TIME_TOOL,
9
11
  ANTENNA_LENGTH_CALCULATOR_TOOL,
10
12
  GPS_COORDINATES_CONVERTER_TOOL,
13
+ DRONE_POWER_ANALYZER_TOOL,
14
+ GSD_FLIGHT_PLANNER_TOOL,
11
15
  ];
12
16
 
13
17
  export {
14
18
  DRONE_FLIGHT_TIME_TOOL,
15
19
  ANTENNA_LENGTH_CALCULATOR_TOOL,
16
20
  GPS_COORDINATES_CONVERTER_TOOL,
21
+ DRONE_POWER_ANALYZER_TOOL,
22
+ GSD_FLIGHT_PLANNER_TOOL,
17
23
  };
18
24
 
package/src/types.ts CHANGED
@@ -5,7 +5,7 @@ export type { SEOSection };
5
5
 
6
6
  export type KnownLocale =
7
7
  | 'ar' | 'da' | 'de' | 'en' | 'es' | 'fi'
8
- | 'fr' | 'it' | 'ja' | 'ko' | 'nb' | 'nl'
8
+ | 'fr' | 'id' | 'it' | 'ja' | 'ko' | 'nb' | 'nl'
9
9
  | 'pl' | 'pt' | 'ru' | 'sv' | 'tr' | 'zh';
10
10
 
11
11
  export interface FAQItem {
@@ -29,9 +29,7 @@ export interface ToolLocaleContent<TUI extends Record<string, string> = Record<s
29
29
  description: string;
30
30
  ui: TUI;
31
31
  seo: SEOSection[];
32
- faqTitle?: string;
33
32
  faq: FAQItem[];
34
- bibliographyTitle?: string;
35
33
  bibliography: BibliographyEntry[];
36
34
  howTo: HowToStep[];
37
35
  schemas: WithContext<Thing>[];