@jjlmoya/utils-drones 1.1.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 +61 -0
- package/src/category/i18n/en.ts +107 -0
- package/src/category/i18n/es.ts +106 -0
- package/src/category/i18n/fr.ts +107 -0
- package/src/category/index.ts +15 -0
- package/src/category/seo.astro +92 -0
- package/src/components/PreviewNavSidebar.astro +116 -0
- package/src/components/PreviewToolbar.astro +143 -0
- package/src/data.ts +11 -0
- package/src/env.d.ts +5 -0
- package/src/index.ts +22 -0
- package/src/layouts/PreviewLayout.astro +117 -0
- package/src/pages/[locale]/[slug].astro +146 -0
- package/src/pages/[locale].astro +251 -0
- package/src/pages/index.astro +4 -0
- package/src/tests/faq_count.test.ts +19 -0
- package/src/tests/locale_completeness.test.ts +42 -0
- package/src/tests/mocks/astro_mock.js +2 -0
- package/src/tests/no_h1_in_components.test.ts +48 -0
- package/src/tests/seo_length.test.ts +22 -0
- package/src/tests/seo_section_types.test.ts +75 -0
- package/src/tests/tool_validation.test.ts +17 -0
- package/src/tool/antenna-length-calculator/AntennaLengthCalculator.css +684 -0
- package/src/tool/antenna-length-calculator/bibliography.astro +14 -0
- package/src/tool/antenna-length-calculator/component.astro +360 -0
- package/src/tool/antenna-length-calculator/i18n/en.ts +204 -0
- package/src/tool/antenna-length-calculator/i18n/es.ts +204 -0
- package/src/tool/antenna-length-calculator/i18n/fr.ts +204 -0
- package/src/tool/antenna-length-calculator/index.ts +27 -0
- package/src/tool/antenna-length-calculator/seo.astro +39 -0
- package/src/tool/drone-flight-time/FlightTimeCalculator.css +363 -0
- package/src/tool/drone-flight-time/bibliography.astro +14 -0
- package/src/tool/drone-flight-time/component.astro +262 -0
- package/src/tool/drone-flight-time/components/AutonomyChart.astro +13 -0
- package/src/tool/drone-flight-time/components/BatterySpecs.astro +46 -0
- package/src/tool/drone-flight-time/components/ConsumptionStats.astro +33 -0
- package/src/tool/drone-flight-time/components/FlightDashboard.astro +33 -0
- package/src/tool/drone-flight-time/i18n/en.ts +200 -0
- package/src/tool/drone-flight-time/i18n/es.ts +200 -0
- package/src/tool/drone-flight-time/i18n/fr.ts +200 -0
- package/src/tool/drone-flight-time/index.ts +27 -0
- package/src/tool/drone-flight-time/seo.astro +31 -0
- package/src/tool/gps-coordinates-converter/GpsCoordinatesConverter.css +310 -0
- package/src/tool/gps-coordinates-converter/bibliography.astro +14 -0
- package/src/tool/gps-coordinates-converter/component.astro +355 -0
- package/src/tool/gps-coordinates-converter/components/GpsHistory.astro +36 -0
- package/src/tool/gps-coordinates-converter/components/GpsInputs.astro +92 -0
- package/src/tool/gps-coordinates-converter/components/GpsMap.astro +18 -0
- package/src/tool/gps-coordinates-converter/components/GpsResults.astro +50 -0
- package/src/tool/gps-coordinates-converter/i18n/en.ts +201 -0
- package/src/tool/gps-coordinates-converter/i18n/es.ts +201 -0
- package/src/tool/gps-coordinates-converter/i18n/fr.ts +201 -0
- package/src/tool/gps-coordinates-converter/index.ts +27 -0
- package/src/tool/gps-coordinates-converter/seo.astro +39 -0
- package/src/tools.ts +16 -0
- package/src/types.ts +72 -0
|
@@ -0,0 +1,355 @@
|
|
|
1
|
+
---
|
|
2
|
+
import GpsInputs from "./components/GpsInputs.astro";
|
|
3
|
+
import GpsResults from "./components/GpsResults.astro";
|
|
4
|
+
import GpsMap from "./components/GpsMap.astro";
|
|
5
|
+
import GpsHistory from "./components/GpsHistory.astro";
|
|
6
|
+
import "./GpsCoordinatesConverter.css";
|
|
7
|
+
|
|
8
|
+
const { ui } = Astro.props;
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
<div class="gps-converter-ui"
|
|
12
|
+
data-ui-copied={ui.copied}
|
|
13
|
+
data-ui-no-history={ui.noHistory}
|
|
14
|
+
data-ui-load={ui.load}
|
|
15
|
+
data-ui-delete={ui.delete}
|
|
16
|
+
>
|
|
17
|
+
<div class="tech-mega-card animate-in fade-in zoom-in duration-700">
|
|
18
|
+
<div class="card-grid">
|
|
19
|
+
<aside class="config-sidebar">
|
|
20
|
+
<GpsInputs ui={ui} />
|
|
21
|
+
<div class="divider"></div>
|
|
22
|
+
<GpsHistory ui={ui} />
|
|
23
|
+
</aside>
|
|
24
|
+
|
|
25
|
+
<main class="main-display">
|
|
26
|
+
<GpsResults ui={ui} />
|
|
27
|
+
<div class="divider"></div>
|
|
28
|
+
<GpsMap ui={ui} />
|
|
29
|
+
</main>
|
|
30
|
+
</div>
|
|
31
|
+
</div>
|
|
32
|
+
</div>
|
|
33
|
+
|
|
34
|
+
<script is:inline lang="ts">
|
|
35
|
+
interface GMSObject {
|
|
36
|
+
g: number;
|
|
37
|
+
m: number;
|
|
38
|
+
s: number;
|
|
39
|
+
h: string;
|
|
40
|
+
mDec: number;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
interface HistoryItem {
|
|
44
|
+
id: number;
|
|
45
|
+
lat: string;
|
|
46
|
+
lng: string;
|
|
47
|
+
time: string;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
let mode: string = 'DD';
|
|
51
|
+
|
|
52
|
+
const elements: Record<string, HTMLElement | null> = {
|
|
53
|
+
latDD: document.getElementById('latDD') as HTMLInputElement,
|
|
54
|
+
lngDD: document.getElementById('lngDD') as HTMLInputElement,
|
|
55
|
+
latG: document.getElementById('latG') as HTMLInputElement,
|
|
56
|
+
latM: document.getElementById('latM') as HTMLInputElement,
|
|
57
|
+
latS: document.getElementById('latS') as HTMLInputElement,
|
|
58
|
+
latH: document.getElementById('latH') as HTMLSelectElement,
|
|
59
|
+
lngG: document.getElementById('lngG') as HTMLInputElement,
|
|
60
|
+
lngM: document.getElementById('lngM') as HTMLInputElement,
|
|
61
|
+
lngS: document.getElementById('lngS') as HTMLInputElement,
|
|
62
|
+
lngH: document.getElementById('lngH') as HTMLSelectElement,
|
|
63
|
+
valDD: document.getElementById('valDD'),
|
|
64
|
+
valGMS: document.getElementById('valGMS'),
|
|
65
|
+
valDM: document.getElementById('valDM'),
|
|
66
|
+
valMaps: document.getElementById('valMaps'),
|
|
67
|
+
historyList: document.getElementById('historyList'),
|
|
68
|
+
modeDD: document.getElementById('modeDD'),
|
|
69
|
+
modeGMS: document.getElementById('modeGMS'),
|
|
70
|
+
ddInputs: document.getElementById('dd-inputs'),
|
|
71
|
+
gmsInputs: document.getElementById('gms-inputs'),
|
|
72
|
+
btnMyLocation: document.getElementById('btnMyLocation'),
|
|
73
|
+
};
|
|
74
|
+
const container: HTMLElement | null = document.querySelector('.gps-converter-ui');
|
|
75
|
+
const uiLocal = {
|
|
76
|
+
copied: container?.dataset.uiCopied || 'Copied',
|
|
77
|
+
noHistory: container?.dataset.uiNoHistory || 'No history',
|
|
78
|
+
load: container?.dataset.uiLoad || 'Load',
|
|
79
|
+
delete: container?.dataset.uiDelete || 'Delete'
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
function init(): void {
|
|
83
|
+
initMap();
|
|
84
|
+
setupListeners();
|
|
85
|
+
loadHistory();
|
|
86
|
+
updateFromDD(40.4168, -3.7038);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function initMap(): void {
|
|
90
|
+
const mapContainer = document.getElementById('gpsMap');
|
|
91
|
+
if (!mapContainer) return;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function parseCoordValue(val: string): number {
|
|
95
|
+
return parseFloat(val.replace(',', '.'));
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function setupModeListeners(): void {
|
|
99
|
+
elements.modeDD?.addEventListener('click', () => switchMode('DD'));
|
|
100
|
+
elements.modeGMS?.addEventListener('click', () => switchMode('GMS'));
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function setupLocationListener(): void {
|
|
104
|
+
elements.btnMyLocation?.addEventListener('click', () => {
|
|
105
|
+
if ("geolocation" in navigator) {
|
|
106
|
+
navigator.geolocation.getCurrentPosition((position) => {
|
|
107
|
+
updateFromDD(position.coords.latitude, position.coords.longitude);
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function setupDDInputListeners(): void {
|
|
114
|
+
[elements.latDD, elements.lngDD].forEach(el => {
|
|
115
|
+
el?.addEventListener('input', () => {
|
|
116
|
+
if (mode === 'DD') {
|
|
117
|
+
const lat = parseCoordValue(elements.latDD.value);
|
|
118
|
+
const lng = parseCoordValue(elements.lngDD.value);
|
|
119
|
+
updateFromDD(lat, lng, false);
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function setupGMSInputListeners(): void {
|
|
126
|
+
const gmsElements = [elements.latG, elements.latM, elements.latS, elements.latH,
|
|
127
|
+
elements.lngG, elements.lngM, elements.lngS, elements.lngH];
|
|
128
|
+
gmsElements.forEach(el => {
|
|
129
|
+
el?.addEventListener('input', () => {
|
|
130
|
+
if (mode === 'GMS') {
|
|
131
|
+
const lat = gmsToDecimal(
|
|
132
|
+
parseFloat(elements.latG.value) || 0,
|
|
133
|
+
parseFloat(elements.latM.value) || 0,
|
|
134
|
+
parseFloat(elements.latS.value) || 0,
|
|
135
|
+
elements.latH.value
|
|
136
|
+
);
|
|
137
|
+
const lng = gmsToDecimal(
|
|
138
|
+
parseFloat(elements.lngG.value) || 0,
|
|
139
|
+
parseFloat(elements.lngM.value) || 0,
|
|
140
|
+
parseFloat(elements.lngS.value) || 0,
|
|
141
|
+
elements.lngH.value
|
|
142
|
+
);
|
|
143
|
+
updateFromDD(lat, lng, false);
|
|
144
|
+
}
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function setupCopyListeners(): void {
|
|
150
|
+
document.querySelectorAll('.copy-btn').forEach(btn => {
|
|
151
|
+
btn.addEventListener('click', () => {
|
|
152
|
+
const targetId = btn.getAttribute('data-copy');
|
|
153
|
+
const text = document.getElementById(targetId!)?.textContent;
|
|
154
|
+
if (text) {
|
|
155
|
+
navigator.clipboard.writeText(text);
|
|
156
|
+
const original = btn.innerHTML;
|
|
157
|
+
btn.innerHTML = uiLocal.copied;
|
|
158
|
+
setTimeout(() => btn.innerHTML = original, 2000);
|
|
159
|
+
}
|
|
160
|
+
});
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function setupClearHistoryListener(): void {
|
|
165
|
+
document.getElementById('clearHistory')?.addEventListener('click', () => {
|
|
166
|
+
localStorage.removeItem('gps_history');
|
|
167
|
+
loadHistory();
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function setupListeners(): void {
|
|
172
|
+
setupModeListeners();
|
|
173
|
+
setupLocationListener();
|
|
174
|
+
setupDDInputListeners();
|
|
175
|
+
setupGMSInputListeners();
|
|
176
|
+
setupCopyListeners();
|
|
177
|
+
setupClearHistoryListener();
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function setSwitchModeDisplay(isDD: boolean): void {
|
|
181
|
+
if (isDD) {
|
|
182
|
+
if (elements.ddInputs) elements.ddInputs.style.display = 'grid';
|
|
183
|
+
if (elements.gmsInputs) elements.gmsInputs.style.display = 'none';
|
|
184
|
+
} else {
|
|
185
|
+
if (elements.ddInputs) elements.ddInputs.style.display = 'none';
|
|
186
|
+
if (elements.gmsInputs) elements.gmsInputs.style.display = 'flex';
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function setSwitchModeActive(isDD: boolean): void {
|
|
191
|
+
if (isDD) {
|
|
192
|
+
elements.modeDD?.classList.add('active');
|
|
193
|
+
elements.modeGMS?.classList.remove('active');
|
|
194
|
+
} else {
|
|
195
|
+
elements.modeDD?.classList.remove('active');
|
|
196
|
+
elements.modeGMS?.classList.add('active');
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function switchMode(newMode: string): void {
|
|
201
|
+
mode = newMode;
|
|
202
|
+
const isDD = mode === 'DD';
|
|
203
|
+
setSwitchModeActive(isDD);
|
|
204
|
+
setSwitchModeDisplay(isDD);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function updateGMSInputs(lat: number, lng: number): void {
|
|
208
|
+
const latGMS = decimalToGMS(lat, true);
|
|
209
|
+
const lngGMS = decimalToGMS(lng, false);
|
|
210
|
+
elements.latG.value = latGMS.g.toString();
|
|
211
|
+
elements.latM.value = latGMS.m.toString();
|
|
212
|
+
elements.latS.value = latGMS.s.toFixed(2);
|
|
213
|
+
elements.latH.value = latGMS.h;
|
|
214
|
+
elements.lngG.value = lngGMS.g.toString();
|
|
215
|
+
elements.lngM.value = lngGMS.m.toString();
|
|
216
|
+
elements.lngS.value = lngGMS.s.toFixed(2);
|
|
217
|
+
elements.lngH.value = lngGMS.h;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function updateResultsDisplay(lat: number, lng: number): void {
|
|
221
|
+
const latGMS = decimalToGMS(lat, true);
|
|
222
|
+
const lngGMS = decimalToGMS(lng, false);
|
|
223
|
+
if (elements.valDD) elements.valDD.textContent = `${lat.toFixed(6)}, ${lng.toFixed(6)}`;
|
|
224
|
+
if (elements.valGMS) elements.valGMS.textContent = `${formatGMS(latGMS)}, ${formatGMS(lngGMS)}`;
|
|
225
|
+
if (elements.valDM) elements.valDM.textContent = `${formatDM(latGMS)}, ${formatDM(lngGMS)}`;
|
|
226
|
+
if (elements.valMaps) elements.valMaps.textContent = `${lat.toFixed(6)},${lng.toFixed(6)}`;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function updateFromDD(lat: number, lng: number, updateInputs: boolean = true): void {
|
|
230
|
+
if (isNaN(lat) || isNaN(lng)) return;
|
|
231
|
+
|
|
232
|
+
if (updateInputs) {
|
|
233
|
+
elements.latDD.value = lat.toFixed(6);
|
|
234
|
+
elements.lngDD.value = lng.toFixed(6);
|
|
235
|
+
updateGMSInputs(lat, lng);
|
|
236
|
+
} else if (mode === 'DD') {
|
|
237
|
+
updateGMSInputs(lat, lng);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
updateResultsDisplay(lat, lng);
|
|
241
|
+
saveToHistory(lat, lng);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function getHemisphere(value: number, isLat: boolean): string {
|
|
245
|
+
if (value >= 0) return isLat ? 'N' : 'E';
|
|
246
|
+
return isLat ? 'S' : 'W';
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function decimalToGMS(d: number, isLat: boolean): GMSObject {
|
|
250
|
+
const h = getHemisphere(d, isLat);
|
|
251
|
+
const absD = Math.abs(d);
|
|
252
|
+
const g = Math.floor(absD);
|
|
253
|
+
const mDecimal = (absD - g) * 60;
|
|
254
|
+
const m = Math.floor(mDecimal);
|
|
255
|
+
const s = (mDecimal - m) * 60;
|
|
256
|
+
return { g, m, s, h, mDec: mDecimal };
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function gmsToDecimal(g: number, m: number, s: number, h: string): number {
|
|
260
|
+
let d = g + (m / 60) + (s / 3600);
|
|
261
|
+
if (h === 'S' || h === 'W') d = -d;
|
|
262
|
+
return d;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function formatGMS(obj: GMSObject): string {
|
|
266
|
+
return `${obj.g}° ${obj.m}' ${obj.s.toFixed(2)}" ${obj.h}`;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function formatDM(obj: GMSObject): string {
|
|
270
|
+
return `${obj.g}° ${obj.mDec.toFixed(3)}' ${obj.h}`;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function saveToHistory(lat: number, lng: number): void {
|
|
274
|
+
let history: HistoryItem[] = JSON.parse(localStorage.getItem('gps_history') || '[]');
|
|
275
|
+
const entry: HistoryItem = {
|
|
276
|
+
id: Date.now(),
|
|
277
|
+
lat: lat.toFixed(5),
|
|
278
|
+
lng: lng.toFixed(5),
|
|
279
|
+
time: new Date().toLocaleTimeString()
|
|
280
|
+
};
|
|
281
|
+
|
|
282
|
+
if (history.length > 0 && history[0].lat === entry.lat && history[0].lng === entry.lng) return;
|
|
283
|
+
|
|
284
|
+
history.unshift(entry);
|
|
285
|
+
history = history.slice(0, 5);
|
|
286
|
+
localStorage.setItem('gps_history', JSON.stringify(history));
|
|
287
|
+
renderHistory(history);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function loadHistory(): void {
|
|
291
|
+
const history: HistoryItem[] = JSON.parse(localStorage.getItem('gps_history') || '[]');
|
|
292
|
+
renderHistory(history);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function renderHistoryHTML(history: HistoryItem[]): string {
|
|
296
|
+
return history.map(item => `
|
|
297
|
+
<div class="history-item" data-id="${item.id}">
|
|
298
|
+
<div class="hist-info" data-lat="${item.lat}" data-lng="${item.lng}">
|
|
299
|
+
<span class="hist-coords">${item.lat}, ${item.lng}</span>
|
|
300
|
+
<span class="hist-time">${item.time}</span>
|
|
301
|
+
</div>
|
|
302
|
+
<div class="hist-actions">
|
|
303
|
+
<button class="action-btn load-btn" title="${uiLocal.load}" data-lat="${item.lat}" data-lng="${item.lng}">
|
|
304
|
+
<svg viewBox="0 0 24 24" width="14" height="14" stroke="currentColor" stroke-width="2" fill="none"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4M7 10l5 5 5-5M12 15V3"/></svg>
|
|
305
|
+
</button>
|
|
306
|
+
<button class="action-btn delete-btn" title="${uiLocal.delete}" data-id="${item.id}">
|
|
307
|
+
<svg viewBox="0 0 24 24" width="14" height="14" stroke="currentColor" stroke-width="2" fill="none"><path d="M3 6h18M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/></svg>
|
|
308
|
+
</button>
|
|
309
|
+
</div>
|
|
310
|
+
</div>
|
|
311
|
+
`).join('');
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function setupHistoryEventListeners(): void {
|
|
315
|
+
document.querySelectorAll('.load-btn').forEach(btn => {
|
|
316
|
+
btn.addEventListener('click', (e) => {
|
|
317
|
+
e.stopPropagation();
|
|
318
|
+
const lat = parseFloat(btn.getAttribute('data-lat')!);
|
|
319
|
+
const lng = parseFloat(btn.getAttribute('data-lng')!);
|
|
320
|
+
updateFromDD(lat, lng);
|
|
321
|
+
});
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
document.querySelectorAll('.delete-btn').forEach(btn => {
|
|
325
|
+
btn.addEventListener('click', (e) => {
|
|
326
|
+
e.stopPropagation();
|
|
327
|
+
const id = parseInt(btn.getAttribute('data-id')!);
|
|
328
|
+
let history: HistoryItem[] = JSON.parse(localStorage.getItem('gps_history') || '[]');
|
|
329
|
+
history = history.filter((item) => item.id !== id);
|
|
330
|
+
localStorage.setItem('gps_history', JSON.stringify(history));
|
|
331
|
+
renderHistory(history);
|
|
332
|
+
});
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
document.querySelectorAll('.hist-info').forEach(info => {
|
|
336
|
+
info.addEventListener('click', () => {
|
|
337
|
+
const lat = parseFloat(info.getAttribute('data-lat')!);
|
|
338
|
+
const lng = parseFloat(info.getAttribute('data-lng')!);
|
|
339
|
+
updateFromDD(lat, lng);
|
|
340
|
+
});
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
function renderHistory(history: HistoryItem[]) {
|
|
345
|
+
if (!elements.historyList) return;
|
|
346
|
+
if (history.length === 0) {
|
|
347
|
+
elements.historyList.innerHTML = `<div class="no-history">${uiLocal.noHistory}</div>`;
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
350
|
+
elements.historyList.innerHTML = renderHistoryHTML(history);
|
|
351
|
+
setupHistoryEventListeners();
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
init();
|
|
355
|
+
</script>
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
---
|
|
2
|
+
const { ui } = Astro.props;
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
<div class="tech-section">
|
|
6
|
+
<div class="header-row">
|
|
7
|
+
<h3 class="section-title">{ui.recentHistory}</h3>
|
|
8
|
+
<button id="clearHistory" class="text-btn">{ui.clear}</button>
|
|
9
|
+
</div>
|
|
10
|
+
<div id="historyList" class="history-list">
|
|
11
|
+
<div class="no-history">{ui.noHistory}</div>
|
|
12
|
+
</div>
|
|
13
|
+
</div>
|
|
14
|
+
|
|
15
|
+
<style>
|
|
16
|
+
.header-row {
|
|
17
|
+
display: flex;
|
|
18
|
+
justify-content: space-between;
|
|
19
|
+
align-items: center;
|
|
20
|
+
}
|
|
21
|
+
.text-btn {
|
|
22
|
+
background: transparent;
|
|
23
|
+
border: none;
|
|
24
|
+
color: #94a3b8;
|
|
25
|
+
font-size: 0.7rem;
|
|
26
|
+
font-weight: 700;
|
|
27
|
+
cursor: pointer;
|
|
28
|
+
text-transform: uppercase;
|
|
29
|
+
}
|
|
30
|
+
.text-btn:hover { color: #ef4444; }
|
|
31
|
+
.no-history {
|
|
32
|
+
font-size: 0.8rem;
|
|
33
|
+
color: #94a3b8;
|
|
34
|
+
font-style: italic;
|
|
35
|
+
}
|
|
36
|
+
</style>
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
---
|
|
2
|
+
const { ui } = Astro.props;
|
|
3
|
+
import { Icon } from "astro-icon/components";
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
<div class="tech-section">
|
|
7
|
+
<div class="row-header">
|
|
8
|
+
<div class="mode-switch">
|
|
9
|
+
<button id="modeDD" class="mode-btn active" data-mode="DD">{ui.decimalDD}</button>
|
|
10
|
+
<button id="modeGMS" class="mode-btn" data-mode="GMS">{ui.degreesGMS}</button>
|
|
11
|
+
</div>
|
|
12
|
+
<button id="btnMyLocation" class="loc-btn" title={ui.useLocation}>
|
|
13
|
+
<Icon name="mdi:crosshairs-gps" size={18} />
|
|
14
|
+
</button>
|
|
15
|
+
</div>
|
|
16
|
+
|
|
17
|
+
<div id="dd-inputs" class="compact-inputs">
|
|
18
|
+
<div class="input-block">
|
|
19
|
+
<label class="field-label">{ui.lat}</label>
|
|
20
|
+
<input type="text" id="latDD" class="tech-input" placeholder="40.4168" />
|
|
21
|
+
</div>
|
|
22
|
+
<div class="input-block">
|
|
23
|
+
<label class="field-label">{ui.lng}</label>
|
|
24
|
+
<input type="text" id="lngDD" class="tech-input" placeholder="-3.7038" />
|
|
25
|
+
</div>
|
|
26
|
+
</div>
|
|
27
|
+
|
|
28
|
+
<div id="gms-inputs" class="tech-section" style="display: none;">
|
|
29
|
+
<div class="gms-group">
|
|
30
|
+
<label class="field-label">{ui.latGMS}</label>
|
|
31
|
+
<div class="compact-inputs">
|
|
32
|
+
<input type="number" id="latG" class="tech-input" placeholder="G" />
|
|
33
|
+
<input type="number" id="latM" class="tech-input" placeholder="M" />
|
|
34
|
+
<input type="number" id="latS" class="tech-input" placeholder="S" step="any" />
|
|
35
|
+
<select id="latH" class="tech-input">
|
|
36
|
+
<option value="N">N</option>
|
|
37
|
+
<option value="S">S</option>
|
|
38
|
+
</select>
|
|
39
|
+
</div>
|
|
40
|
+
</div>
|
|
41
|
+
<div class="gms-group">
|
|
42
|
+
<label class="field-label">{ui.lngGMS}</label>
|
|
43
|
+
<div class="compact-inputs">
|
|
44
|
+
<input type="number" id="lngG" class="tech-input" placeholder="G" />
|
|
45
|
+
<input type="number" id="lngM" class="tech-input" placeholder="M" />
|
|
46
|
+
<input type="number" id="lngS" class="tech-input" placeholder="S" step="any" />
|
|
47
|
+
<select id="lngH" class="tech-input">
|
|
48
|
+
<option value="E">E</option>
|
|
49
|
+
<option value="W">W</option>
|
|
50
|
+
</select>
|
|
51
|
+
</div>
|
|
52
|
+
</div>
|
|
53
|
+
</div>
|
|
54
|
+
</div>
|
|
55
|
+
|
|
56
|
+
<style>
|
|
57
|
+
.row-header {
|
|
58
|
+
display: flex;
|
|
59
|
+
justify-content: space-between;
|
|
60
|
+
align-items: center;
|
|
61
|
+
gap: 1rem;
|
|
62
|
+
}
|
|
63
|
+
.loc-btn {
|
|
64
|
+
background: #0ea5e910;
|
|
65
|
+
border: 1px solid #0ea5e930;
|
|
66
|
+
color: #0ea5e9;
|
|
67
|
+
padding: 0.6rem;
|
|
68
|
+
border-radius: 12px;
|
|
69
|
+
cursor: pointer;
|
|
70
|
+
display: flex;
|
|
71
|
+
align-items: center;
|
|
72
|
+
justify-content: center;
|
|
73
|
+
transition: all 0.2s;
|
|
74
|
+
}
|
|
75
|
+
.loc-btn:hover {
|
|
76
|
+
background: #0ea5e920;
|
|
77
|
+
transform: scale(1.05);
|
|
78
|
+
}
|
|
79
|
+
.gms-group {
|
|
80
|
+
display: flex;
|
|
81
|
+
flex-direction: column;
|
|
82
|
+
gap: 0.5rem;
|
|
83
|
+
}
|
|
84
|
+
.compact-inputs {
|
|
85
|
+
display: grid;
|
|
86
|
+
grid-template-columns: repeat(4, 1fr);
|
|
87
|
+
gap: 0.5rem;
|
|
88
|
+
}
|
|
89
|
+
#dd-inputs.compact-inputs {
|
|
90
|
+
grid-template-columns: 1fr 1fr;
|
|
91
|
+
}
|
|
92
|
+
</style>
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
---
|
|
2
|
+
---
|
|
3
|
+
|
|
4
|
+
<div class="tech-section">
|
|
5
|
+
<h3 class="section-title">Visualización en Mapas</h3>
|
|
6
|
+
<div id="gpsMap" class="map-container"></div>
|
|
7
|
+
<small class="hint">Haz clic en el mapa para capturar coordenadas directamente.</small>
|
|
8
|
+
</div>
|
|
9
|
+
|
|
10
|
+
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
|
|
11
|
+
|
|
12
|
+
<style>
|
|
13
|
+
.hint {
|
|
14
|
+
font-size: 0.75rem;
|
|
15
|
+
color: #94a3b8;
|
|
16
|
+
margin-top: -0.5rem;
|
|
17
|
+
}
|
|
18
|
+
</style>
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
---
|
|
2
|
+
const { ui } = Astro.props;
|
|
3
|
+
import { Icon } from "astro-icon/components";
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
<div class="results-area">
|
|
7
|
+
<div class="result-box">
|
|
8
|
+
<div class="res-header">
|
|
9
|
+
<span class="res-label">{ui.decimalDD}</span>
|
|
10
|
+
<button class="copy-btn" data-copy="valDD">
|
|
11
|
+
<Icon name="mdi:content-copy" size={14} />
|
|
12
|
+
{ui.copy}
|
|
13
|
+
</button>
|
|
14
|
+
</div>
|
|
15
|
+
<div id="valDD" class="val-display">0.0000, 0.0000</div>
|
|
16
|
+
</div>
|
|
17
|
+
|
|
18
|
+
<div class="result-box">
|
|
19
|
+
<div class="res-header">
|
|
20
|
+
<span class="res-label">{ui.gmsTraditional}</span>
|
|
21
|
+
<button class="copy-btn" data-copy="valGMS">
|
|
22
|
+
<Icon name="mdi:content-copy" size={14} />
|
|
23
|
+
{ui.copy}
|
|
24
|
+
</button>
|
|
25
|
+
</div>
|
|
26
|
+
<div id="valGMS" class="val-display">0° 0' 0" N, 0° 0' 0" E</div>
|
|
27
|
+
</div>
|
|
28
|
+
|
|
29
|
+
<div class="result-box">
|
|
30
|
+
<div class="res-header">
|
|
31
|
+
<span class="res-label">{ui.nauticalDM}</span>
|
|
32
|
+
<button class="copy-btn" data-copy="valDM">
|
|
33
|
+
<Icon name="mdi:content-copy" size={14} />
|
|
34
|
+
{ui.copy}
|
|
35
|
+
</button>
|
|
36
|
+
</div>
|
|
37
|
+
<div id="valDM" class="val-display">0° 0.000' N, 0° 0.000' E</div>
|
|
38
|
+
</div>
|
|
39
|
+
|
|
40
|
+
<div class="result-box">
|
|
41
|
+
<div class="res-header">
|
|
42
|
+
<span class="res-label">{ui.googleMapsFormat}</span>
|
|
43
|
+
<button class="copy-btn" data-copy="valMaps">
|
|
44
|
+
<Icon name="mdi:google-maps" size={14} />
|
|
45
|
+
{ui.copy}
|
|
46
|
+
</button>
|
|
47
|
+
</div>
|
|
48
|
+
<div id="valMaps" class="val-display">0.0, 0.0</div>
|
|
49
|
+
</div>
|
|
50
|
+
</div>
|