@jjlmoya/utils-drones 1.30.0 → 1.32.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 (67) hide show
  1. package/package.json +1 -1
  2. package/src/category/i18n/en.ts +1 -1
  3. package/src/category/i18n/fr.ts +1 -1
  4. package/src/category/index.ts +2 -0
  5. package/src/components/PreviewNavSidebar.astro +116 -116
  6. package/src/components/PreviewToolbar.astro +143 -143
  7. package/src/data.ts +1 -1
  8. package/src/entries.ts +4 -0
  9. package/src/env.d.ts +1 -1
  10. package/src/index.ts +1 -1
  11. package/src/layouts/PreviewLayout.astro +118 -118
  12. package/src/pages/[locale].astro +251 -251
  13. package/src/pages/index.astro +4 -4
  14. package/src/tests/faq_count.test.ts +1 -1
  15. package/src/tests/locale_completeness.test.ts +1 -2
  16. package/src/tests/mocks/astro_mock.js +1 -1
  17. package/src/tests/no_h1_in_components.test.ts +1 -1
  18. package/src/tests/tool_validation.test.ts +17 -17
  19. package/src/tool/antenna-length-calculator/bibliography.astro +6 -6
  20. package/src/tool/antenna-length-calculator/component.astro +329 -329
  21. package/src/tool/antenna-length-calculator/seo.astro +15 -15
  22. package/src/tool/drone-battery-c-rating-calculator/i18n/de.ts +192 -192
  23. package/src/tool/drone-battery-c-rating-calculator/i18n/en.ts +192 -192
  24. package/src/tool/drone-battery-c-rating-calculator/i18n/es.ts +192 -192
  25. package/src/tool/drone-battery-c-rating-calculator/i18n/id.ts +192 -192
  26. package/src/tool/drone-battery-c-rating-calculator/i18n/ja.ts +192 -192
  27. package/src/tool/drone-battery-c-rating-calculator/i18n/ko.ts +192 -192
  28. package/src/tool/drone-battery-c-rating-calculator/i18n/nl.ts +192 -192
  29. package/src/tool/drone-battery-c-rating-calculator/i18n/pl.ts +192 -192
  30. package/src/tool/drone-battery-c-rating-calculator/i18n/ru.ts +192 -192
  31. package/src/tool/drone-battery-c-rating-calculator/i18n/sv.ts +192 -192
  32. package/src/tool/drone-battery-c-rating-calculator/i18n/tr.ts +192 -192
  33. package/src/tool/drone-battery-c-rating-calculator/i18n/zh.ts +192 -192
  34. package/src/tool/drone-flight-time/component.astro +187 -187
  35. package/src/tool/drone-flight-time/seo.astro +15 -15
  36. package/src/tool/drone-motor-propeller-calculator/bibliography.astro +6 -0
  37. package/src/tool/drone-motor-propeller-calculator/bibliography.ts +12 -0
  38. package/src/tool/drone-motor-propeller-calculator/component.astro +109 -0
  39. package/src/tool/drone-motor-propeller-calculator/controller.ts +174 -0
  40. package/src/tool/drone-motor-propeller-calculator/dom-views.ts +78 -0
  41. package/src/tool/drone-motor-propeller-calculator/drone-motor-propeller-calculator.css +562 -0
  42. package/src/tool/drone-motor-propeller-calculator/entry.ts +27 -0
  43. package/src/tool/drone-motor-propeller-calculator/evaluator.ts +14 -0
  44. package/src/tool/drone-motor-propeller-calculator/i18n/de.ts +202 -0
  45. package/src/tool/drone-motor-propeller-calculator/i18n/en.ts +202 -0
  46. package/src/tool/drone-motor-propeller-calculator/i18n/es.ts +202 -0
  47. package/src/tool/drone-motor-propeller-calculator/i18n/fr.ts +202 -0
  48. package/src/tool/drone-motor-propeller-calculator/i18n/id.ts +192 -0
  49. package/src/tool/drone-motor-propeller-calculator/i18n/it.ts +192 -0
  50. package/src/tool/drone-motor-propeller-calculator/i18n/ja.ts +192 -0
  51. package/src/tool/drone-motor-propeller-calculator/i18n/ko.ts +192 -0
  52. package/src/tool/drone-motor-propeller-calculator/i18n/nl.ts +192 -0
  53. package/src/tool/drone-motor-propeller-calculator/i18n/pl.ts +192 -0
  54. package/src/tool/drone-motor-propeller-calculator/i18n/pt.ts +192 -0
  55. package/src/tool/drone-motor-propeller-calculator/i18n/ru.ts +192 -0
  56. package/src/tool/drone-motor-propeller-calculator/i18n/sv.ts +192 -0
  57. package/src/tool/drone-motor-propeller-calculator/i18n/tr.ts +192 -0
  58. package/src/tool/drone-motor-propeller-calculator/i18n/zh.ts +192 -0
  59. package/src/tool/drone-motor-propeller-calculator/index.ts +11 -0
  60. package/src/tool/drone-motor-propeller-calculator/logic.test.ts +56 -0
  61. package/src/tool/drone-motor-propeller-calculator/logic.ts +108 -0
  62. package/src/tool/drone-motor-propeller-calculator/seo.astro +15 -0
  63. package/src/tool/drone-motor-propeller-calculator/storage.ts +26 -0
  64. package/src/tool/drone-motor-propeller-calculator/ui.ts +55 -0
  65. package/src/tool/gps-coordinates-converter/component.astro +275 -275
  66. package/src/tool/gps-coordinates-converter/seo.astro +15 -15
  67. package/src/tools.ts +3 -0
@@ -1,329 +1,329 @@
1
- ---
2
- const { ui } = Astro.props;
3
- import { Icon } from "astro-icon/components";
4
-
5
- const presets = [
6
- { label: ui.presetFpv, val: 5800 },
7
- { label: ui.presetWifi, val: 2400 },
8
- { label: ui.presetElrs, val: 915 },
9
- { label: ui.presetLrs, val: 868 },
10
- { label: ui.presetUhf, val: 433 },
11
- { label: ui.presetMbus, val: 169 },
12
- ];
13
-
14
- const materials = [
15
- { label: ui.materialBareCopper, vf: 0.95 },
16
- { label: ui.materialPvcInsulated, vf: 0.92 },
17
- { label: ui.materialSolidRod, vf: 0.97 },
18
- { label: ui.materialCoaxial, vf: 0.66 },
19
- ];
20
- ---
21
-
22
- <div class="antenna-calculator-ui">
23
- <div class="tech-mega-card animate-in fade-in zoom-in duration-1000">
24
- <div class="card-grid">
25
- <aside class="config-sidebar">
26
- <section>
27
- <div class="section-title">
28
- <Icon name="mdi:sine-wave" />
29
- <h2>{ui.signalParameters}</h2>
30
- </div>
31
-
32
- <div class="input-group">
33
- <div class="input-with-unit">
34
- <input type="number" id="freqInput" value="868" step="1" min="1" max="10000" aria-label={ui.signalParameters} />
35
- <span class="unit">MHz</span>
36
- </div>
37
- </div>
38
-
39
- <div class="preset-grid mt-6">
40
- {presets.map(p => (
41
- <button class="preset-btn" data-val={p.val}>
42
- {p.label}
43
- </button>
44
- ))}
45
- </div>
46
- </section>
47
-
48
- <div class="divider"></div>
49
-
50
- <section>
51
- <div class="section-title">
52
- <Icon name="mdi:antenna" />
53
- <h2>{ui.antennaType}</h2>
54
- </div>
55
- <div class="type-selector">
56
- <div class="radio-card">
57
- <input type="radio" name="waveType" value="half" checked id="waveHalf" />
58
- <label for="waveHalf" class="radio-content">
59
- <Icon name="mdi:arrow-left-right" />
60
- <span>{ui.dipole}</span>
61
- </label>
62
- </div>
63
- <div class="radio-card">
64
- <input type="radio" name="waveType" value="quarter" id="waveQuarter" />
65
- <label for="waveQuarter" class="radio-content">
66
- <Icon name="mdi:arrow-up" />
67
- <span>{ui.whip}</span>
68
- </label>
69
- </div>
70
- </div>
71
- </section>
72
-
73
- <section>
74
- <div class="section-title">
75
- <Icon name="mdi:layers-triple" />
76
- <h2>{ui.conductorMedium}</h2>
77
- </div>
78
- <div class="input-group">
79
- <select id="vfInput" class="custom-select-premium" aria-label={ui.conductorMedium}>
80
- {materials.map(m => (
81
- <option value={m.vf}>{m.label}</option>
82
- ))}
83
- </select>
84
- </div>
85
- </section>
86
- </aside>
87
-
88
- <main class="main-display">
89
- <div class="results-hero">
90
- <div class="mega-result gold">
91
- <span class="result-label">{ui.totalLength}</span>
92
- <div class="result-value">
93
- <span class="number" id="totalLength">0</span>
94
- <span class="unit">mm</span>
95
- </div>
96
- </div>
97
- <div class="mega-result glass" id="branchCard">
98
- <span class="result-label">{ui.branchLength}</span>
99
- <div class="result-value">
100
- <span class="number" id="branchLength">0</span>
101
- <span class="unit">mm</span>
102
- </div>
103
- </div>
104
- </div>
105
-
106
- <div class="visualizer-stage">
107
- <svg id="antenna-svg" viewBox="0 0 400 300" preserveAspectRatio="xMidYMid meet">
108
- <defs>
109
- <linearGradient id="wireGrad" x1="0%" y1="0%" x2="100%" y2="0%">
110
- <stop offset="0%" style="stop-color:#f59e0b;stop-opacity:1" />
111
- <stop offset="100%" style="stop-color:#fbbf24;stop-opacity:1" />
112
- </linearGradient>
113
- </defs>
114
-
115
- <g id="dipoleLayer">
116
- <line x1="205" y1="150" x2="350" y2="150" class="antenna-wire active" id="rightArm" />
117
- <line x1="195" y1="150" x2="50" y2="150" class="antenna-wire active" id="leftArm" />
118
- <circle cx="200" cy="150" r="4" class="feed-core" />
119
- <path d="M195,150 Q200,160 205,150" fill="none" stroke="#ef4444" stroke-width="2" class="feed-point-gap" />
120
-
121
- <line x1="50" y1="170" x2="50" y2="180" class="measuring-line" id="leftTick" />
122
- <line x1="350" y1="170" x2="350" y2="180" class="measuring-line" id="rightTick" />
123
- <line x1="50" y1="175" x2="350" y2="175" class="measuring-line" id="measureLine" />
124
- </g>
125
-
126
- <g id="whipLayer" style="display: none;">
127
- <line x1="200" y1="250" x2="200" y2="50" class="antenna-wire active" id="verticalWhip" />
128
- <rect x="100" y="250" width="200" height="6" rx="3" class="ground-plate" />
129
- <circle cx="200" cy="250" r="6" class="feed-core" />
130
-
131
- <line x1="220" y1="50" x2="230" y2="50" class="measuring-line" id="topTick" />
132
- <line x1="220" y1="250" x2="230" y2="250" class="measuring-line" id="bottomTick" />
133
- <line x1="225" y1="50" x2="225" y2="250" class="measuring-line" id="verticalMeasure" />
134
- </g>
135
-
136
- <text x="200" y="280" class="svg-label-text" id="svgInfoText" text-anchor="middle">{ui.dynamicScheme}</text>
137
- </svg>
138
- </div>
139
-
140
- <div class="harmonics-container">
141
- <div class="section-title">
142
- <Icon name="mdi:chart-timeline-variant" />
143
- <h2>{ui.secondaryResonance}</h2>
144
- </div>
145
- <div class="harmonics-list" id="harmonicsList" data-label={ui.harmonicLabel}>
146
- </div>
147
- </div>
148
-
149
- <div class="expert-reference-grid">
150
- <div class="ref-card">
151
- <Icon name="mdi:swap-vertical" />
152
- <div>
153
- <h4>{ui.swrIdeal}</h4>
154
- <p>&lt; 1.5:1 (ROE)</p>
155
- </div>
156
- </div>
157
- <div class="ref-card">
158
- <Icon name="mdi:omega" />
159
- <div>
160
- <h4>{ui.impedance}</h4>
161
- <p>50 Ω System</p>
162
- </div>
163
- </div>
164
- </div>
165
-
166
- <div class="security-warning">
167
- <Icon name="mdi:alert-decagram" />
168
- <p><strong>{ui.criticalNotice}:</strong> {ui.criticalNoticeText}</p>
169
- </div>
170
- </main>
171
- </div>
172
- </div>
173
- </div>
174
-
175
- <script>
176
- const freqInput = document.getElementById('freqInput');
177
- const vfInput = document.getElementById('vfInput');
178
- const waveRadios = document.getElementsByName('waveType');
179
-
180
- const elements = {
181
- total: document.getElementById('totalLength'),
182
- branch: document.getElementById('branchLength'),
183
- branchCard: document.getElementById('branchCard'),
184
- dipoleLayer: document.getElementById('dipoleLayer'),
185
- whipLayer: document.getElementById('whipLayer'),
186
- harmonics: document.getElementById('harmonicsList'),
187
- leftArm: document.getElementById('leftArm') || null,
188
- rightArm: document.getElementById('rightArm') || null,
189
- verticalWhip: document.getElementById('verticalWhip') || null,
190
- measureLine: document.getElementById('measureLine') || null,
191
- leftTick: document.getElementById('leftTick') || null,
192
- rightTick: document.getElementById('rightTick') || null,
193
- topTick: document.getElementById('topTick') || null,
194
- bottomTick: document.getElementById('bottomTick') || null,
195
- verticalMeasure: document.getElementById('verticalMeasure') || null
196
- };
197
-
198
- function getWaveType() {
199
- let waveType = 'half';
200
- waveRadios.forEach(r => { if(r.checked) waveType = r.value; });
201
- return waveType;
202
- }
203
-
204
- function updateLayerVisibility(waveType) {
205
- if (waveType === 'half') {
206
- if (elements.branchCard) elements.branchCard.style.display = 'flex';
207
- if (elements.dipoleLayer) elements.dipoleLayer.style.display = 'block';
208
- if (elements.whipLayer) elements.whipLayer.style.display = 'none';
209
- } else {
210
- if (elements.branchCard) elements.branchCard.style.display = 'none';
211
- if (elements.dipoleLayer) elements.dipoleLayer.style.display = 'none';
212
- if (elements.whipLayer) elements.whipLayer.style.display = 'block';
213
- }
214
- }
215
-
216
- function validateInputs() {
217
- if (!freqInput || !(freqInput as HTMLInputElement).value || !vfInput) return null;
218
- const freq = parseFloat((freqInput as HTMLInputElement).value);
219
- const vf = parseFloat((vfInput as HTMLSelectElement).value);
220
- return freq > 0 ? { freq, vf } : null;
221
- }
222
-
223
- function calculateLengths(freq, vf, waveType) {
224
- const lambda = 300000 / freq;
225
- const totalPhysicalLambda = lambda * vf;
226
- const totalLen = waveType === 'half' ? totalPhysicalLambda / 2 : totalPhysicalLambda / 4;
227
- const branchLen = waveType === 'half' ? totalLen / 2 : totalLen;
228
- return { totalLen, branchLen };
229
- }
230
-
231
- function displayLengths(totalLen, branchLen) {
232
- if (elements.total) elements.total.textContent = Math.round(totalLen).toString();
233
- if (elements.branch) elements.branch.textContent = Math.round(branchLen).toString();
234
- }
235
-
236
- function calculate() {
237
- const inputs = validateInputs();
238
- if (!inputs) return;
239
- const waveType = getWaveType();
240
- const { totalLen, branchLen } = calculateLengths(inputs.freq, inputs.vf, waveType);
241
- updateLayerVisibility(waveType);
242
- updateVisuals(waveType, waveType === 'half' ? branchLen : totalLen);
243
- displayLengths(totalLen, branchLen);
244
- updateHarmonics(inputs.freq);
245
- }
246
-
247
- function setArmAttributes(x, isLeft) {
248
- const arm = isLeft ? elements.leftArm : elements.rightArm;
249
- const startX = isLeft ? '195' : '205';
250
- arm?.setAttribute('x1', startX);
251
- arm?.setAttribute('x2', x.toString());
252
- const tick = isLeft ? elements.leftTick : elements.rightTick;
253
- tick?.setAttribute('x1', x.toString());
254
- tick?.setAttribute('x2', x.toString());
255
- }
256
-
257
- function updateDipoleVisuals(visualLen) {
258
- const x1 = 200 - visualLen;
259
- const x2 = 200 + visualLen;
260
- setArmAttributes(x1, true);
261
- setArmAttributes(x2, false);
262
- elements.measureLine?.setAttribute('x1', x1.toString());
263
- elements.measureLine?.setAttribute('x2', x2.toString());
264
- }
265
-
266
- function updateWhipVisuals(visualLen) {
267
- const y2 = 250 - visualLen;
268
- elements.verticalWhip?.setAttribute('y2', y2.toString());
269
- elements.verticalMeasure?.setAttribute('y1', y2.toString());
270
- elements.topTick?.setAttribute('y1', y2.toString());
271
- elements.topTick?.setAttribute('y2', y2.toString());
272
- }
273
-
274
- function updateVisuals(type, len) {
275
- const visualLen = Math.min(Math.max(len / 4, 30), 160);
276
- if (type === 'half') {
277
- updateDipoleVisuals(visualLen);
278
- } else {
279
- updateWhipVisuals(visualLen);
280
- }
281
- }
282
-
283
- function updateHarmonics(baseFreq) {
284
- if (!elements.harmonics) return;
285
-
286
- const harmonicLabel = elements.harmonics.dataset.label || "Armónica";
287
-
288
- const orders = [
289
- { label: `3ª ${harmonicLabel}`, val: baseFreq * 3 },
290
- { label: `5ª ${harmonicLabel}`, val: baseFreq * 5 },
291
- { label: `7ª ${harmonicLabel}`, val: baseFreq * 7 }
292
- ];
293
-
294
- elements.harmonics.innerHTML = orders.map(o => `
295
- <div class="harmonic-card animate-in fade-in slide-in-from-bottom-2">
296
- <span class="h-order">${o.label}</span>
297
- <span class="h-freq">${Math.round(o.val)} MHz</span>
298
- </div>
299
- `).join('');
300
- }
301
-
302
- freqInput.addEventListener('input', calculate);
303
-
304
- vfInput.addEventListener('change', calculate);
305
- waveRadios.forEach(r => {
306
- r.addEventListener('change', () => {
307
- calculate();
308
- document.querySelectorAll('.radio-card').forEach(card => card.classList.remove('active'));
309
- const checkedRadio = document.querySelector('.radio-card input:checked');
310
- if (checkedRadio?.parentElement?.classList) {
311
- checkedRadio.parentElement.classList.add('active');
312
- }
313
- });
314
- });
315
-
316
- document.querySelectorAll('.preset-btn').forEach(btn => {
317
- btn.addEventListener('click', () => {
318
- const val = btn.getAttribute('data-val');
319
- if (val) {
320
- freqInput.value = val;
321
- calculate();
322
- }
323
- });
324
- });
325
-
326
- calculate();
327
- </script>
328
-
329
-
1
+ ---
2
+ const { ui } = Astro.props;
3
+ import { Icon } from "astro-icon/components";
4
+
5
+ const presets = [
6
+ { label: ui.presetFpv, val: 5800 },
7
+ { label: ui.presetWifi, val: 2400 },
8
+ { label: ui.presetElrs, val: 915 },
9
+ { label: ui.presetLrs, val: 868 },
10
+ { label: ui.presetUhf, val: 433 },
11
+ { label: ui.presetMbus, val: 169 },
12
+ ];
13
+
14
+ const materials = [
15
+ { label: ui.materialBareCopper, vf: 0.95 },
16
+ { label: ui.materialPvcInsulated, vf: 0.92 },
17
+ { label: ui.materialSolidRod, vf: 0.97 },
18
+ { label: ui.materialCoaxial, vf: 0.66 },
19
+ ];
20
+ ---
21
+
22
+ <div class="antenna-calculator-ui">
23
+ <div class="tech-mega-card animate-in fade-in zoom-in duration-1000">
24
+ <div class="card-grid">
25
+ <aside class="config-sidebar">
26
+ <section>
27
+ <div class="section-title">
28
+ <Icon name="mdi:sine-wave" />
29
+ <h2>{ui.signalParameters}</h2>
30
+ </div>
31
+
32
+ <div class="input-group">
33
+ <div class="input-with-unit">
34
+ <input type="number" id="freqInput" value="868" step="1" min="1" max="10000" aria-label={ui.signalParameters} />
35
+ <span class="unit">MHz</span>
36
+ </div>
37
+ </div>
38
+
39
+ <div class="preset-grid mt-6">
40
+ {presets.map(p => (
41
+ <button class="preset-btn" data-val={p.val}>
42
+ {p.label}
43
+ </button>
44
+ ))}
45
+ </div>
46
+ </section>
47
+
48
+ <div class="divider"></div>
49
+
50
+ <section>
51
+ <div class="section-title">
52
+ <Icon name="mdi:antenna" />
53
+ <h2>{ui.antennaType}</h2>
54
+ </div>
55
+ <div class="type-selector">
56
+ <div class="radio-card">
57
+ <input type="radio" name="waveType" value="half" checked id="waveHalf" />
58
+ <label for="waveHalf" class="radio-content">
59
+ <Icon name="mdi:arrow-left-right" />
60
+ <span>{ui.dipole}</span>
61
+ </label>
62
+ </div>
63
+ <div class="radio-card">
64
+ <input type="radio" name="waveType" value="quarter" id="waveQuarter" />
65
+ <label for="waveQuarter" class="radio-content">
66
+ <Icon name="mdi:arrow-up" />
67
+ <span>{ui.whip}</span>
68
+ </label>
69
+ </div>
70
+ </div>
71
+ </section>
72
+
73
+ <section>
74
+ <div class="section-title">
75
+ <Icon name="mdi:layers-triple" />
76
+ <h2>{ui.conductorMedium}</h2>
77
+ </div>
78
+ <div class="input-group">
79
+ <select id="vfInput" class="custom-select-premium" aria-label={ui.conductorMedium}>
80
+ {materials.map(m => (
81
+ <option value={m.vf}>{m.label}</option>
82
+ ))}
83
+ </select>
84
+ </div>
85
+ </section>
86
+ </aside>
87
+
88
+ <main class="main-display">
89
+ <div class="results-hero">
90
+ <div class="mega-result gold">
91
+ <span class="result-label">{ui.totalLength}</span>
92
+ <div class="result-value">
93
+ <span class="number" id="totalLength">0</span>
94
+ <span class="unit">mm</span>
95
+ </div>
96
+ </div>
97
+ <div class="mega-result glass" id="branchCard">
98
+ <span class="result-label">{ui.branchLength}</span>
99
+ <div class="result-value">
100
+ <span class="number" id="branchLength">0</span>
101
+ <span class="unit">mm</span>
102
+ </div>
103
+ </div>
104
+ </div>
105
+
106
+ <div class="visualizer-stage">
107
+ <svg id="antenna-svg" viewBox="0 0 400 300" preserveAspectRatio="xMidYMid meet">
108
+ <defs>
109
+ <linearGradient id="wireGrad" x1="0%" y1="0%" x2="100%" y2="0%">
110
+ <stop offset="0%" style="stop-color:#f59e0b;stop-opacity:1" />
111
+ <stop offset="100%" style="stop-color:#fbbf24;stop-opacity:1" />
112
+ </linearGradient>
113
+ </defs>
114
+
115
+ <g id="dipoleLayer">
116
+ <line x1="205" y1="150" x2="350" y2="150" class="antenna-wire active" id="rightArm" />
117
+ <line x1="195" y1="150" x2="50" y2="150" class="antenna-wire active" id="leftArm" />
118
+ <circle cx="200" cy="150" r="4" class="feed-core" />
119
+ <path d="M195,150 Q200,160 205,150" fill="none" stroke="#ef4444" stroke-width="2" class="feed-point-gap" />
120
+
121
+ <line x1="50" y1="170" x2="50" y2="180" class="measuring-line" id="leftTick" />
122
+ <line x1="350" y1="170" x2="350" y2="180" class="measuring-line" id="rightTick" />
123
+ <line x1="50" y1="175" x2="350" y2="175" class="measuring-line" id="measureLine" />
124
+ </g>
125
+
126
+ <g id="whipLayer" style="display: none;">
127
+ <line x1="200" y1="250" x2="200" y2="50" class="antenna-wire active" id="verticalWhip" />
128
+ <rect x="100" y="250" width="200" height="6" rx="3" class="ground-plate" />
129
+ <circle cx="200" cy="250" r="6" class="feed-core" />
130
+
131
+ <line x1="220" y1="50" x2="230" y2="50" class="measuring-line" id="topTick" />
132
+ <line x1="220" y1="250" x2="230" y2="250" class="measuring-line" id="bottomTick" />
133
+ <line x1="225" y1="50" x2="225" y2="250" class="measuring-line" id="verticalMeasure" />
134
+ </g>
135
+
136
+ <text x="200" y="280" class="svg-label-text" id="svgInfoText" text-anchor="middle">{ui.dynamicScheme}</text>
137
+ </svg>
138
+ </div>
139
+
140
+ <div class="harmonics-container">
141
+ <div class="section-title">
142
+ <Icon name="mdi:chart-timeline-variant" />
143
+ <h2>{ui.secondaryResonance}</h2>
144
+ </div>
145
+ <div class="harmonics-list" id="harmonicsList" data-label={ui.harmonicLabel}>
146
+ </div>
147
+ </div>
148
+
149
+ <div class="expert-reference-grid">
150
+ <div class="ref-card">
151
+ <Icon name="mdi:swap-vertical" />
152
+ <div>
153
+ <h4>{ui.swrIdeal}</h4>
154
+ <p>&lt; 1.5:1 (ROE)</p>
155
+ </div>
156
+ </div>
157
+ <div class="ref-card">
158
+ <Icon name="mdi:omega" />
159
+ <div>
160
+ <h4>{ui.impedance}</h4>
161
+ <p>50 Ω System</p>
162
+ </div>
163
+ </div>
164
+ </div>
165
+
166
+ <div class="security-warning">
167
+ <Icon name="mdi:alert-decagram" />
168
+ <p><strong>{ui.criticalNotice}:</strong> {ui.criticalNoticeText}</p>
169
+ </div>
170
+ </main>
171
+ </div>
172
+ </div>
173
+ </div>
174
+
175
+ <script>
176
+ const freqInput = document.getElementById('freqInput');
177
+ const vfInput = document.getElementById('vfInput');
178
+ const waveRadios = document.getElementsByName('waveType');
179
+
180
+ const elements = {
181
+ total: document.getElementById('totalLength'),
182
+ branch: document.getElementById('branchLength'),
183
+ branchCard: document.getElementById('branchCard'),
184
+ dipoleLayer: document.getElementById('dipoleLayer'),
185
+ whipLayer: document.getElementById('whipLayer'),
186
+ harmonics: document.getElementById('harmonicsList'),
187
+ leftArm: document.getElementById('leftArm') || null,
188
+ rightArm: document.getElementById('rightArm') || null,
189
+ verticalWhip: document.getElementById('verticalWhip') || null,
190
+ measureLine: document.getElementById('measureLine') || null,
191
+ leftTick: document.getElementById('leftTick') || null,
192
+ rightTick: document.getElementById('rightTick') || null,
193
+ topTick: document.getElementById('topTick') || null,
194
+ bottomTick: document.getElementById('bottomTick') || null,
195
+ verticalMeasure: document.getElementById('verticalMeasure') || null
196
+ };
197
+
198
+ function getWaveType() {
199
+ let waveType = 'half';
200
+ waveRadios.forEach(r => { if(r.checked) waveType = r.value; });
201
+ return waveType;
202
+ }
203
+
204
+ function updateLayerVisibility(waveType) {
205
+ if (waveType === 'half') {
206
+ if (elements.branchCard) elements.branchCard.style.display = 'flex';
207
+ if (elements.dipoleLayer) elements.dipoleLayer.style.display = 'block';
208
+ if (elements.whipLayer) elements.whipLayer.style.display = 'none';
209
+ } else {
210
+ if (elements.branchCard) elements.branchCard.style.display = 'none';
211
+ if (elements.dipoleLayer) elements.dipoleLayer.style.display = 'none';
212
+ if (elements.whipLayer) elements.whipLayer.style.display = 'block';
213
+ }
214
+ }
215
+
216
+ function validateInputs() {
217
+ if (!freqInput || !(freqInput as HTMLInputElement).value || !vfInput) return null;
218
+ const freq = parseFloat((freqInput as HTMLInputElement).value);
219
+ const vf = parseFloat((vfInput as HTMLSelectElement).value);
220
+ return freq > 0 ? { freq, vf } : null;
221
+ }
222
+
223
+ function calculateLengths(freq, vf, waveType) {
224
+ const lambda = 300000 / freq;
225
+ const totalPhysicalLambda = lambda * vf;
226
+ const totalLen = waveType === 'half' ? totalPhysicalLambda / 2 : totalPhysicalLambda / 4;
227
+ const branchLen = waveType === 'half' ? totalLen / 2 : totalLen;
228
+ return { totalLen, branchLen };
229
+ }
230
+
231
+ function displayLengths(totalLen, branchLen) {
232
+ if (elements.total) elements.total.textContent = Math.round(totalLen).toString();
233
+ if (elements.branch) elements.branch.textContent = Math.round(branchLen).toString();
234
+ }
235
+
236
+ function calculate() {
237
+ const inputs = validateInputs();
238
+ if (!inputs) return;
239
+ const waveType = getWaveType();
240
+ const { totalLen, branchLen } = calculateLengths(inputs.freq, inputs.vf, waveType);
241
+ updateLayerVisibility(waveType);
242
+ updateVisuals(waveType, waveType === 'half' ? branchLen : totalLen);
243
+ displayLengths(totalLen, branchLen);
244
+ updateHarmonics(inputs.freq);
245
+ }
246
+
247
+ function setArmAttributes(x, isLeft) {
248
+ const arm = isLeft ? elements.leftArm : elements.rightArm;
249
+ const startX = isLeft ? '195' : '205';
250
+ arm?.setAttribute('x1', startX);
251
+ arm?.setAttribute('x2', x.toString());
252
+ const tick = isLeft ? elements.leftTick : elements.rightTick;
253
+ tick?.setAttribute('x1', x.toString());
254
+ tick?.setAttribute('x2', x.toString());
255
+ }
256
+
257
+ function updateDipoleVisuals(visualLen) {
258
+ const x1 = 200 - visualLen;
259
+ const x2 = 200 + visualLen;
260
+ setArmAttributes(x1, true);
261
+ setArmAttributes(x2, false);
262
+ elements.measureLine?.setAttribute('x1', x1.toString());
263
+ elements.measureLine?.setAttribute('x2', x2.toString());
264
+ }
265
+
266
+ function updateWhipVisuals(visualLen) {
267
+ const y2 = 250 - visualLen;
268
+ elements.verticalWhip?.setAttribute('y2', y2.toString());
269
+ elements.verticalMeasure?.setAttribute('y1', y2.toString());
270
+ elements.topTick?.setAttribute('y1', y2.toString());
271
+ elements.topTick?.setAttribute('y2', y2.toString());
272
+ }
273
+
274
+ function updateVisuals(type, len) {
275
+ const visualLen = Math.min(Math.max(len / 4, 30), 160);
276
+ if (type === 'half') {
277
+ updateDipoleVisuals(visualLen);
278
+ } else {
279
+ updateWhipVisuals(visualLen);
280
+ }
281
+ }
282
+
283
+ function updateHarmonics(baseFreq) {
284
+ if (!elements.harmonics) return;
285
+
286
+ const harmonicLabel = elements.harmonics.dataset.label || "Armónica";
287
+
288
+ const orders = [
289
+ { label: `3ª ${harmonicLabel}`, val: baseFreq * 3 },
290
+ { label: `5ª ${harmonicLabel}`, val: baseFreq * 5 },
291
+ { label: `7ª ${harmonicLabel}`, val: baseFreq * 7 }
292
+ ];
293
+
294
+ elements.harmonics.innerHTML = orders.map(o => `
295
+ <div class="harmonic-card animate-in fade-in slide-in-from-bottom-2">
296
+ <span class="h-order">${o.label}</span>
297
+ <span class="h-freq">${Math.round(o.val)} MHz</span>
298
+ </div>
299
+ `).join('');
300
+ }
301
+
302
+ freqInput.addEventListener('input', calculate);
303
+
304
+ vfInput.addEventListener('change', calculate);
305
+ waveRadios.forEach(r => {
306
+ r.addEventListener('change', () => {
307
+ calculate();
308
+ document.querySelectorAll('.radio-card').forEach(card => card.classList.remove('active'));
309
+ const checkedRadio = document.querySelector('.radio-card input:checked');
310
+ if (checkedRadio?.parentElement?.classList) {
311
+ checkedRadio.parentElement.classList.add('active');
312
+ }
313
+ });
314
+ });
315
+
316
+ document.querySelectorAll('.preset-btn').forEach(btn => {
317
+ btn.addEventListener('click', () => {
318
+ const val = btn.getAttribute('data-val');
319
+ if (val) {
320
+ freqInput.value = val;
321
+ calculate();
322
+ }
323
+ });
324
+ });
325
+
326
+ calculate();
327
+ </script>
328
+
329
+