@jjlmoya/utils-drones 1.16.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 (34) hide show
  1. package/package.json +1 -1
  2. package/src/entries.ts +4 -1
  3. package/src/index.ts +1 -0
  4. package/src/tests/locale_completeness.test.ts +2 -2
  5. package/src/tests/tool_validation.test.ts +2 -2
  6. package/src/tool/drone-power-analyzer/i18n/de.ts +1 -1
  7. package/src/tool/drone-power-analyzer/i18n/es.ts +1 -1
  8. package/src/tool/drone-power-analyzer/i18n/fr.ts +1 -1
  9. package/src/tool/gsd-flight-planner/InputsColumn.astro +108 -0
  10. package/src/tool/gsd-flight-planner/ResultsColumn.astro +90 -0
  11. package/src/tool/gsd-flight-planner/bibliography.astro +6 -0
  12. package/src/tool/gsd-flight-planner/bibliography.ts +7 -0
  13. package/src/tool/gsd-flight-planner/component.astro +22 -0
  14. package/src/tool/gsd-flight-planner/entry.ts +29 -0
  15. package/src/tool/gsd-flight-planner/gsd-flight-planner.css +515 -0
  16. package/src/tool/gsd-flight-planner/i18n/de.ts +214 -0
  17. package/src/tool/gsd-flight-planner/i18n/en.ts +214 -0
  18. package/src/tool/gsd-flight-planner/i18n/es.ts +221 -0
  19. package/src/tool/gsd-flight-planner/i18n/fr.ts +214 -0
  20. package/src/tool/gsd-flight-planner/i18n/id.ts +214 -0
  21. package/src/tool/gsd-flight-planner/i18n/it.ts +214 -0
  22. package/src/tool/gsd-flight-planner/i18n/ja.ts +214 -0
  23. package/src/tool/gsd-flight-planner/i18n/ko.ts +214 -0
  24. package/src/tool/gsd-flight-planner/i18n/nl.ts +214 -0
  25. package/src/tool/gsd-flight-planner/i18n/pl.ts +214 -0
  26. package/src/tool/gsd-flight-planner/i18n/pt.ts +214 -0
  27. package/src/tool/gsd-flight-planner/i18n/ru.ts +214 -0
  28. package/src/tool/gsd-flight-planner/i18n/sv.ts +214 -0
  29. package/src/tool/gsd-flight-planner/i18n/tr.ts +214 -0
  30. package/src/tool/gsd-flight-planner/i18n/zh.ts +214 -0
  31. package/src/tool/gsd-flight-planner/index.ts +8 -0
  32. package/src/tool/gsd-flight-planner/logic.js +326 -0
  33. package/src/tool/gsd-flight-planner/seo.astro +15 -0
  34. package/src/tools.ts +3 -0
@@ -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
@@ -3,6 +3,7 @@ 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
5
  import { DRONE_POWER_ANALYZER_TOOL } from './tool/drone-power-analyzer/index';
6
+ import { GSD_FLIGHT_PLANNER_TOOL } from './tool/gsd-flight-planner/index';
6
7
  import type { ToolDefinition } from './types';
7
8
 
8
9
  export const ALL_TOOLS: ToolDefinition[] = [
@@ -10,6 +11,7 @@ export const ALL_TOOLS: ToolDefinition[] = [
10
11
  ANTENNA_LENGTH_CALCULATOR_TOOL,
11
12
  GPS_COORDINATES_CONVERTER_TOOL,
12
13
  DRONE_POWER_ANALYZER_TOOL,
14
+ GSD_FLIGHT_PLANNER_TOOL,
13
15
  ];
14
16
 
15
17
  export {
@@ -17,5 +19,6 @@ export {
17
19
  ANTENNA_LENGTH_CALCULATOR_TOOL,
18
20
  GPS_COORDINATES_CONVERTER_TOOL,
19
21
  DRONE_POWER_ANALYZER_TOOL,
22
+ GSD_FLIGHT_PLANNER_TOOL,
20
23
  };
21
24