@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.
- package/package.json +1 -1
- package/src/category/i18n/en.ts +1 -1
- package/src/category/i18n/fr.ts +1 -1
- package/src/category/index.ts +2 -0
- package/src/components/PreviewNavSidebar.astro +116 -116
- package/src/components/PreviewToolbar.astro +143 -143
- package/src/data.ts +1 -1
- package/src/entries.ts +4 -0
- package/src/env.d.ts +1 -1
- package/src/index.ts +1 -1
- package/src/layouts/PreviewLayout.astro +118 -118
- package/src/pages/[locale].astro +251 -251
- package/src/pages/index.astro +4 -4
- package/src/tests/faq_count.test.ts +1 -1
- package/src/tests/locale_completeness.test.ts +1 -2
- package/src/tests/mocks/astro_mock.js +1 -1
- package/src/tests/no_h1_in_components.test.ts +1 -1
- package/src/tests/tool_validation.test.ts +17 -17
- package/src/tool/antenna-length-calculator/bibliography.astro +6 -6
- package/src/tool/antenna-length-calculator/component.astro +329 -329
- package/src/tool/antenna-length-calculator/seo.astro +15 -15
- package/src/tool/drone-battery-c-rating-calculator/i18n/de.ts +192 -192
- package/src/tool/drone-battery-c-rating-calculator/i18n/en.ts +192 -192
- package/src/tool/drone-battery-c-rating-calculator/i18n/es.ts +192 -192
- package/src/tool/drone-battery-c-rating-calculator/i18n/id.ts +192 -192
- package/src/tool/drone-battery-c-rating-calculator/i18n/ja.ts +192 -192
- package/src/tool/drone-battery-c-rating-calculator/i18n/ko.ts +192 -192
- package/src/tool/drone-battery-c-rating-calculator/i18n/nl.ts +192 -192
- package/src/tool/drone-battery-c-rating-calculator/i18n/pl.ts +192 -192
- package/src/tool/drone-battery-c-rating-calculator/i18n/ru.ts +192 -192
- package/src/tool/drone-battery-c-rating-calculator/i18n/sv.ts +192 -192
- package/src/tool/drone-battery-c-rating-calculator/i18n/tr.ts +192 -192
- package/src/tool/drone-battery-c-rating-calculator/i18n/zh.ts +192 -192
- package/src/tool/drone-flight-time/component.astro +187 -187
- package/src/tool/drone-flight-time/seo.astro +15 -15
- package/src/tool/drone-motor-propeller-calculator/bibliography.astro +6 -0
- package/src/tool/drone-motor-propeller-calculator/bibliography.ts +12 -0
- package/src/tool/drone-motor-propeller-calculator/component.astro +109 -0
- package/src/tool/drone-motor-propeller-calculator/controller.ts +174 -0
- package/src/tool/drone-motor-propeller-calculator/dom-views.ts +78 -0
- package/src/tool/drone-motor-propeller-calculator/drone-motor-propeller-calculator.css +562 -0
- package/src/tool/drone-motor-propeller-calculator/entry.ts +27 -0
- package/src/tool/drone-motor-propeller-calculator/evaluator.ts +14 -0
- package/src/tool/drone-motor-propeller-calculator/i18n/de.ts +202 -0
- package/src/tool/drone-motor-propeller-calculator/i18n/en.ts +202 -0
- package/src/tool/drone-motor-propeller-calculator/i18n/es.ts +202 -0
- package/src/tool/drone-motor-propeller-calculator/i18n/fr.ts +202 -0
- package/src/tool/drone-motor-propeller-calculator/i18n/id.ts +192 -0
- package/src/tool/drone-motor-propeller-calculator/i18n/it.ts +192 -0
- package/src/tool/drone-motor-propeller-calculator/i18n/ja.ts +192 -0
- package/src/tool/drone-motor-propeller-calculator/i18n/ko.ts +192 -0
- package/src/tool/drone-motor-propeller-calculator/i18n/nl.ts +192 -0
- package/src/tool/drone-motor-propeller-calculator/i18n/pl.ts +192 -0
- package/src/tool/drone-motor-propeller-calculator/i18n/pt.ts +192 -0
- package/src/tool/drone-motor-propeller-calculator/i18n/ru.ts +192 -0
- package/src/tool/drone-motor-propeller-calculator/i18n/sv.ts +192 -0
- package/src/tool/drone-motor-propeller-calculator/i18n/tr.ts +192 -0
- package/src/tool/drone-motor-propeller-calculator/i18n/zh.ts +192 -0
- package/src/tool/drone-motor-propeller-calculator/index.ts +11 -0
- package/src/tool/drone-motor-propeller-calculator/logic.test.ts +56 -0
- package/src/tool/drone-motor-propeller-calculator/logic.ts +108 -0
- package/src/tool/drone-motor-propeller-calculator/seo.astro +15 -0
- package/src/tool/drone-motor-propeller-calculator/storage.ts +26 -0
- package/src/tool/drone-motor-propeller-calculator/ui.ts +55 -0
- package/src/tool/gps-coordinates-converter/component.astro +275 -275
- package/src/tool/gps-coordinates-converter/seo.astro +15 -15
- package/src/tools.ts +3 -0
|
@@ -1,275 +1,275 @@
|
|
|
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
|
-
|
|
7
|
-
const { ui } = Astro.props;
|
|
8
|
-
---
|
|
9
|
-
|
|
10
|
-
<div class="gps-converter-ui"
|
|
11
|
-
data-ui-copied={ui.copied}
|
|
12
|
-
data-ui-no-history={ui.noHistory}
|
|
13
|
-
data-ui-load={ui.load}
|
|
14
|
-
data-ui-delete={ui.delete}
|
|
15
|
-
>
|
|
16
|
-
<div class="tech-mega-card animate-in fade-in zoom-in duration-700">
|
|
17
|
-
<div class="card-grid">
|
|
18
|
-
<aside class="config-sidebar">
|
|
19
|
-
<GpsInputs ui={ui} />
|
|
20
|
-
<div class="divider"></div>
|
|
21
|
-
<GpsHistory ui={ui} />
|
|
22
|
-
</aside>
|
|
23
|
-
|
|
24
|
-
<main class="main-display">
|
|
25
|
-
<GpsResults ui={ui} />
|
|
26
|
-
<div class="divider"></div>
|
|
27
|
-
<GpsMap ui={ui} />
|
|
28
|
-
</main>
|
|
29
|
-
</div>
|
|
30
|
-
</div>
|
|
31
|
-
</div>
|
|
32
|
-
|
|
33
|
-
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" integrity="sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY=" crossorigin=""/>
|
|
34
|
-
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js" integrity="sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo=" crossorigin="" is:inline></script>
|
|
35
|
-
|
|
36
|
-
<script is:inline>
|
|
37
|
-
function getHemisphere(val, isLat) {
|
|
38
|
-
if (val >= 0) return isLat ? 'N' : 'E';
|
|
39
|
-
return isLat ? 'S' : 'W';
|
|
40
|
-
}
|
|
41
|
-
function decimalToGMS(d, isLat) {
|
|
42
|
-
const h = getHemisphere(d, isLat);
|
|
43
|
-
const absD = Math.abs(d);
|
|
44
|
-
const g = Math.floor(absD);
|
|
45
|
-
const m = Math.floor((absD - g) * 60);
|
|
46
|
-
const s = ((absD - g) * 60 - m) * 60;
|
|
47
|
-
return {g, m, s, h, mDec: (absD - g) * 60};
|
|
48
|
-
}
|
|
49
|
-
function formatGMS(obj) {
|
|
50
|
-
return `${obj.g}° ${obj.m}' ${obj.s.toFixed(2)}" ${obj.h}`;
|
|
51
|
-
}
|
|
52
|
-
function formatDM(obj) {
|
|
53
|
-
return `${obj.g}° ${obj.mDec.toFixed(3)}' ${obj.h}`;
|
|
54
|
-
}
|
|
55
|
-
function id(name) {
|
|
56
|
-
return document.getElementById(name);
|
|
57
|
-
}
|
|
58
|
-
const state={el:{}, ui:{}, mode:'DD', map:null, L:window.L};
|
|
59
|
-
function initElements() {
|
|
60
|
-
state.el = {
|
|
61
|
-
latDD:id('latDD'),lngDD:id('lngDD'),latG:id('latG'),latM:id('latM'),
|
|
62
|
-
latS:id('latS'),latH:id('latH'),lngG:id('lngG'),lngM:id('lngM'),
|
|
63
|
-
lngS:id('lngS'),lngH:id('lngH'),valDD:id('valDD'),valGMS:id('valGMS'),
|
|
64
|
-
valDM:id('valDM'),valMaps:id('valMaps'),historyList:id('historyList'),
|
|
65
|
-
modeDD:id('modeDD'),modeGMS:id('modeGMS'),ddInputs:id('dd-inputs'),
|
|
66
|
-
gmsInputs:id('gms-inputs'),btnMyLocation:id('btnMyLocation')
|
|
67
|
-
};
|
|
68
|
-
}
|
|
69
|
-
function getUIValue(attr, def) {
|
|
70
|
-
const ct = document.querySelector('.gps-converter-ui');
|
|
71
|
-
return ct?.dataset[attr] || def;
|
|
72
|
-
}
|
|
73
|
-
function initUI() {
|
|
74
|
-
state.ui = {
|
|
75
|
-
copied: getUIValue('uiCopied', 'Copied'),
|
|
76
|
-
noHistory: getUIValue('uiNoHistory', 'No history'),
|
|
77
|
-
load: getUIValue('uiLoad', 'Load'),
|
|
78
|
-
delete: getUIValue('uiDelete', 'Delete')
|
|
79
|
-
};
|
|
80
|
-
}
|
|
81
|
-
function initState() {
|
|
82
|
-
initElements();
|
|
83
|
-
initUI();
|
|
84
|
-
}
|
|
85
|
-
function setLatGMS(gms) {
|
|
86
|
-
if (state.el.latG) state.el.latG.value = gms.g.toString();
|
|
87
|
-
if (state.el.latM) state.el.latM.value = gms.m.toString();
|
|
88
|
-
if (state.el.latS) state.el.latS.value = gms.s.toFixed(2);
|
|
89
|
-
if (state.el.latH) state.el.latH.value = gms.h;
|
|
90
|
-
}
|
|
91
|
-
function setLngGMS(gms) {
|
|
92
|
-
if (state.el.lngG) state.el.lngG.value = gms.g.toString();
|
|
93
|
-
if (state.el.lngM) state.el.lngM.value = gms.m.toString();
|
|
94
|
-
if (state.el.lngS) state.el.lngS.value = gms.s.toFixed(2);
|
|
95
|
-
if (state.el.lngH) state.el.lngH.value = gms.h;
|
|
96
|
-
}
|
|
97
|
-
function updateGMSDisplay(lat, lng) {
|
|
98
|
-
const lGMS = decimalToGMS(lat, true);
|
|
99
|
-
const nGMS = decimalToGMS(lng, false);
|
|
100
|
-
setLatGMS(lGMS);
|
|
101
|
-
setLngGMS(nGMS);
|
|
102
|
-
}
|
|
103
|
-
function updateResultDisplay(lat, lng) {
|
|
104
|
-
const lGMS = decimalToGMS(lat, true);
|
|
105
|
-
const nGMS = decimalToGMS(lng, false);
|
|
106
|
-
if (state.el.valDD) {
|
|
107
|
-
state.el.valDD.textContent = `${lat.toFixed(6)}, ${lng.toFixed(6)}`;
|
|
108
|
-
}
|
|
109
|
-
if (state.el.valGMS) {
|
|
110
|
-
state.el.valGMS.textContent = `${formatGMS(lGMS)}, ${formatGMS(nGMS)}`;
|
|
111
|
-
}
|
|
112
|
-
if (state.el.valDM) {
|
|
113
|
-
state.el.valDM.textContent = `${formatDM(lGMS)}, ${formatDM(nGMS)}`;
|
|
114
|
-
}
|
|
115
|
-
if (state.el.valMaps) {
|
|
116
|
-
state.el.valMaps.textContent = `${lat.toFixed(6)},${lng.toFixed(6)}`;
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
function setDDInputs(lat, lng) {
|
|
120
|
-
if (state.el.latDD) state.el.latDD.value = lat.toFixed(6);
|
|
121
|
-
if (state.el.lngDD) state.el.lngDD.value = lng.toFixed(6);
|
|
122
|
-
}
|
|
123
|
-
function updateFromDD(lat, lng, upd=true) {
|
|
124
|
-
if (isNaN(lat) || isNaN(lng)) return;
|
|
125
|
-
if (upd) setDDInputs(lat, lng);
|
|
126
|
-
if (!upd && state.mode !== 'DD') return;
|
|
127
|
-
updateGMSDisplay(lat, lng);
|
|
128
|
-
updateResultDisplay(lat, lng);
|
|
129
|
-
saveHistory(lat, lng);
|
|
130
|
-
}
|
|
131
|
-
function createHistoryItem(item) {
|
|
132
|
-
return `<div class="history-item" data-id="${item.id}"><div class="hist-info" data-lat="${item.lat}" data-lng="${item.lng}">
|
|
133
|
-
<span class="hist-coords">${item.lat}, ${item.lng}</span><span class="hist-time">${item.time}</span></div><div class="hist-actions">
|
|
134
|
-
<button class="action-btn load-btn" data-lat="${item.lat}" data-lng="${item.lng}"><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></button>
|
|
135
|
-
<button class="action-btn delete-btn" data-id="${item.id}"><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></button></div></div>`;
|
|
136
|
-
}
|
|
137
|
-
function setupHistoryListeners() {
|
|
138
|
-
document.querySelectorAll('.load-btn').forEach(btn => {
|
|
139
|
-
btn.addEventListener('click', (evt) => {
|
|
140
|
-
evt.stopPropagation();
|
|
141
|
-
const lat = btn.getAttribute('data-lat');
|
|
142
|
-
const lng = btn.getAttribute('data-lng');
|
|
143
|
-
if (lat && lng) updateFromDD(parseFloat(lat), parseFloat(lng));
|
|
144
|
-
});
|
|
145
|
-
});
|
|
146
|
-
document.querySelectorAll('.delete-btn').forEach(btn => {
|
|
147
|
-
btn.addEventListener('click', (evt) => {
|
|
148
|
-
evt.stopPropagation();
|
|
149
|
-
const itemId = btn.getAttribute('data-id');
|
|
150
|
-
if (itemId) {
|
|
151
|
-
let history = JSON.parse(localStorage.getItem('gps_history') || '[]');
|
|
152
|
-
history = history.filter(it => it.id !== parseInt(itemId));
|
|
153
|
-
localStorage.setItem('gps_history', JSON.stringify(history));
|
|
154
|
-
renderHistory(history);
|
|
155
|
-
}
|
|
156
|
-
});
|
|
157
|
-
});
|
|
158
|
-
document.querySelectorAll('.hist-info').forEach(info => {
|
|
159
|
-
info.addEventListener('click', () => {
|
|
160
|
-
const lat = info.getAttribute('data-lat');
|
|
161
|
-
const lng = info.getAttribute('data-lng');
|
|
162
|
-
if (lat && lng) updateFromDD(parseFloat(lat), parseFloat(lng));
|
|
163
|
-
});
|
|
164
|
-
});
|
|
165
|
-
}
|
|
166
|
-
function renderHistory(history) {
|
|
167
|
-
if (!state.el.historyList) return;
|
|
168
|
-
if (history.length === 0) {
|
|
169
|
-
state.el.historyList.innerHTML = `<div class="no-history">${state.ui.noHistory}</div>`;
|
|
170
|
-
} else {
|
|
171
|
-
state.el.historyList.innerHTML = history.map(createHistoryItem).join('');
|
|
172
|
-
}
|
|
173
|
-
setupHistoryListeners();
|
|
174
|
-
}
|
|
175
|
-
function saveHistory(lat, lng) {
|
|
176
|
-
let history = JSON.parse(localStorage.getItem('gps_history') || '[]');
|
|
177
|
-
const entry = {
|
|
178
|
-
id: Date.now(),
|
|
179
|
-
lat: lat.toFixed(5),
|
|
180
|
-
lng: lng.toFixed(5),
|
|
181
|
-
time: new Date().toLocaleTimeString()
|
|
182
|
-
};
|
|
183
|
-
if (history.length > 0 && history[0].lat === entry.lat && history[0].lng === entry.lng) return;
|
|
184
|
-
history.unshift(entry);
|
|
185
|
-
history = history.slice(0, 5);
|
|
186
|
-
localStorage.setItem('gps_history', JSON.stringify(history));
|
|
187
|
-
renderHistory(history);
|
|
188
|
-
}
|
|
189
|
-
function loadHistory() {
|
|
190
|
-
const history = JSON.parse(localStorage.getItem('gps_history') || '[]');
|
|
191
|
-
renderHistory(history);
|
|
192
|
-
}
|
|
193
|
-
function initMap() {
|
|
194
|
-
const mapEl = id('gpsMap');
|
|
195
|
-
if (!mapEl || !state.L) return;
|
|
196
|
-
state.map = state.L.map('gpsMap').setView([40.4168, -3.7038], 13);
|
|
197
|
-
state.L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
|
198
|
-
attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors',
|
|
199
|
-
maxZoom: 19
|
|
200
|
-
}).addTo(state.map);
|
|
201
|
-
state.map.on('click', (evt) => updateFromDD(evt.latlng.lat, evt.latlng.lng));
|
|
202
|
-
}
|
|
203
|
-
function setupModeListeners() {
|
|
204
|
-
state.el.modeDD?.addEventListener('click', () => {
|
|
205
|
-
state.mode = 'DD';
|
|
206
|
-
state.el.modeDD?.classList.add('active');
|
|
207
|
-
state.el.modeGMS?.classList.remove('active');
|
|
208
|
-
if (state.el.ddInputs) state.el.ddInputs.style.display = 'grid';
|
|
209
|
-
if (state.el.gmsInputs) state.el.gmsInputs.style.display = 'none';
|
|
210
|
-
});
|
|
211
|
-
state.el.modeGMS?.addEventListener('click', () => {
|
|
212
|
-
state.mode = 'GMS';
|
|
213
|
-
state.el.modeGMS?.classList.add('active');
|
|
214
|
-
state.el.modeDD?.classList.remove('active');
|
|
215
|
-
if (state.el.ddInputs) state.el.ddInputs.style.display = 'none';
|
|
216
|
-
if (state.el.gmsInputs) state.el.gmsInputs.style.display = 'flex';
|
|
217
|
-
});
|
|
218
|
-
}
|
|
219
|
-
function setupInputListeners() {
|
|
220
|
-
state.el.btnMyLocation?.addEventListener('click', () => {
|
|
221
|
-
if ('geolocation' in navigator) {
|
|
222
|
-
navigator.geolocation.getCurrentPosition(pos => {
|
|
223
|
-
updateFromDD(pos.coords.latitude, pos.coords.longitude);
|
|
224
|
-
});
|
|
225
|
-
}
|
|
226
|
-
});
|
|
227
|
-
[state.el.latDD, state.el.lngDD].forEach(el => {
|
|
228
|
-
el?.addEventListener('input', () => {
|
|
229
|
-
if (state.mode === 'DD' && state.el.latDD && state.el.lngDD) {
|
|
230
|
-
updateFromDD(parseFloat(state.el.latDD.value), parseFloat(state.el.lngDD.value), false);
|
|
231
|
-
}
|
|
232
|
-
});
|
|
233
|
-
});
|
|
234
|
-
}
|
|
235
|
-
function setupCopyListeners() {
|
|
236
|
-
document.querySelectorAll('.copy-btn').forEach(btn => {
|
|
237
|
-
btn.addEventListener('click', () => {
|
|
238
|
-
const targetId = btn.getAttribute('data-copy');
|
|
239
|
-
const text = targetId ? id(targetId)?.textContent : null;
|
|
240
|
-
if (text) {
|
|
241
|
-
navigator.clipboard.writeText(text);
|
|
242
|
-
const original = btn.innerHTML;
|
|
243
|
-
btn.innerHTML = state.ui.copied;
|
|
244
|
-
setTimeout(() => { btn.innerHTML = original; }, 2000);
|
|
245
|
-
}
|
|
246
|
-
});
|
|
247
|
-
});
|
|
248
|
-
id('clearHistory')?.addEventListener('click', () => {
|
|
249
|
-
localStorage.removeItem('gps_history');
|
|
250
|
-
loadHistory();
|
|
251
|
-
});
|
|
252
|
-
}
|
|
253
|
-
function setupAll() {
|
|
254
|
-
setupModeListeners();
|
|
255
|
-
setupInputListeners();
|
|
256
|
-
setupCopyListeners();
|
|
257
|
-
setupHistoryListeners();
|
|
258
|
-
}
|
|
259
|
-
function checkLeaflet() {
|
|
260
|
-
if (window.L) {
|
|
261
|
-
state.L = window.L;
|
|
262
|
-
initMap();
|
|
263
|
-
setupAll();
|
|
264
|
-
loadHistory();
|
|
265
|
-
updateFromDD(40.4168, -3.7038);
|
|
266
|
-
} else {
|
|
267
|
-
setTimeout(checkLeaflet, 100);
|
|
268
|
-
}
|
|
269
|
-
}
|
|
270
|
-
initState();
|
|
271
|
-
checkLeaflet();
|
|
272
|
-
</script>
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
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
|
+
|
|
7
|
+
const { ui } = Astro.props;
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
<div class="gps-converter-ui"
|
|
11
|
+
data-ui-copied={ui.copied}
|
|
12
|
+
data-ui-no-history={ui.noHistory}
|
|
13
|
+
data-ui-load={ui.load}
|
|
14
|
+
data-ui-delete={ui.delete}
|
|
15
|
+
>
|
|
16
|
+
<div class="tech-mega-card animate-in fade-in zoom-in duration-700">
|
|
17
|
+
<div class="card-grid">
|
|
18
|
+
<aside class="config-sidebar">
|
|
19
|
+
<GpsInputs ui={ui} />
|
|
20
|
+
<div class="divider"></div>
|
|
21
|
+
<GpsHistory ui={ui} />
|
|
22
|
+
</aside>
|
|
23
|
+
|
|
24
|
+
<main class="main-display">
|
|
25
|
+
<GpsResults ui={ui} />
|
|
26
|
+
<div class="divider"></div>
|
|
27
|
+
<GpsMap ui={ui} />
|
|
28
|
+
</main>
|
|
29
|
+
</div>
|
|
30
|
+
</div>
|
|
31
|
+
</div>
|
|
32
|
+
|
|
33
|
+
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" integrity="sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY=" crossorigin=""/>
|
|
34
|
+
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js" integrity="sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo=" crossorigin="" is:inline></script>
|
|
35
|
+
|
|
36
|
+
<script is:inline>
|
|
37
|
+
function getHemisphere(val, isLat) {
|
|
38
|
+
if (val >= 0) return isLat ? 'N' : 'E';
|
|
39
|
+
return isLat ? 'S' : 'W';
|
|
40
|
+
}
|
|
41
|
+
function decimalToGMS(d, isLat) {
|
|
42
|
+
const h = getHemisphere(d, isLat);
|
|
43
|
+
const absD = Math.abs(d);
|
|
44
|
+
const g = Math.floor(absD);
|
|
45
|
+
const m = Math.floor((absD - g) * 60);
|
|
46
|
+
const s = ((absD - g) * 60 - m) * 60;
|
|
47
|
+
return {g, m, s, h, mDec: (absD - g) * 60};
|
|
48
|
+
}
|
|
49
|
+
function formatGMS(obj) {
|
|
50
|
+
return `${obj.g}° ${obj.m}' ${obj.s.toFixed(2)}" ${obj.h}`;
|
|
51
|
+
}
|
|
52
|
+
function formatDM(obj) {
|
|
53
|
+
return `${obj.g}° ${obj.mDec.toFixed(3)}' ${obj.h}`;
|
|
54
|
+
}
|
|
55
|
+
function id(name) {
|
|
56
|
+
return document.getElementById(name);
|
|
57
|
+
}
|
|
58
|
+
const state={el:{}, ui:{}, mode:'DD', map:null, L:window.L};
|
|
59
|
+
function initElements() {
|
|
60
|
+
state.el = {
|
|
61
|
+
latDD:id('latDD'),lngDD:id('lngDD'),latG:id('latG'),latM:id('latM'),
|
|
62
|
+
latS:id('latS'),latH:id('latH'),lngG:id('lngG'),lngM:id('lngM'),
|
|
63
|
+
lngS:id('lngS'),lngH:id('lngH'),valDD:id('valDD'),valGMS:id('valGMS'),
|
|
64
|
+
valDM:id('valDM'),valMaps:id('valMaps'),historyList:id('historyList'),
|
|
65
|
+
modeDD:id('modeDD'),modeGMS:id('modeGMS'),ddInputs:id('dd-inputs'),
|
|
66
|
+
gmsInputs:id('gms-inputs'),btnMyLocation:id('btnMyLocation')
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
function getUIValue(attr, def) {
|
|
70
|
+
const ct = document.querySelector('.gps-converter-ui');
|
|
71
|
+
return ct?.dataset[attr] || def;
|
|
72
|
+
}
|
|
73
|
+
function initUI() {
|
|
74
|
+
state.ui = {
|
|
75
|
+
copied: getUIValue('uiCopied', 'Copied'),
|
|
76
|
+
noHistory: getUIValue('uiNoHistory', 'No history'),
|
|
77
|
+
load: getUIValue('uiLoad', 'Load'),
|
|
78
|
+
delete: getUIValue('uiDelete', 'Delete')
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
function initState() {
|
|
82
|
+
initElements();
|
|
83
|
+
initUI();
|
|
84
|
+
}
|
|
85
|
+
function setLatGMS(gms) {
|
|
86
|
+
if (state.el.latG) state.el.latG.value = gms.g.toString();
|
|
87
|
+
if (state.el.latM) state.el.latM.value = gms.m.toString();
|
|
88
|
+
if (state.el.latS) state.el.latS.value = gms.s.toFixed(2);
|
|
89
|
+
if (state.el.latH) state.el.latH.value = gms.h;
|
|
90
|
+
}
|
|
91
|
+
function setLngGMS(gms) {
|
|
92
|
+
if (state.el.lngG) state.el.lngG.value = gms.g.toString();
|
|
93
|
+
if (state.el.lngM) state.el.lngM.value = gms.m.toString();
|
|
94
|
+
if (state.el.lngS) state.el.lngS.value = gms.s.toFixed(2);
|
|
95
|
+
if (state.el.lngH) state.el.lngH.value = gms.h;
|
|
96
|
+
}
|
|
97
|
+
function updateGMSDisplay(lat, lng) {
|
|
98
|
+
const lGMS = decimalToGMS(lat, true);
|
|
99
|
+
const nGMS = decimalToGMS(lng, false);
|
|
100
|
+
setLatGMS(lGMS);
|
|
101
|
+
setLngGMS(nGMS);
|
|
102
|
+
}
|
|
103
|
+
function updateResultDisplay(lat, lng) {
|
|
104
|
+
const lGMS = decimalToGMS(lat, true);
|
|
105
|
+
const nGMS = decimalToGMS(lng, false);
|
|
106
|
+
if (state.el.valDD) {
|
|
107
|
+
state.el.valDD.textContent = `${lat.toFixed(6)}, ${lng.toFixed(6)}`;
|
|
108
|
+
}
|
|
109
|
+
if (state.el.valGMS) {
|
|
110
|
+
state.el.valGMS.textContent = `${formatGMS(lGMS)}, ${formatGMS(nGMS)}`;
|
|
111
|
+
}
|
|
112
|
+
if (state.el.valDM) {
|
|
113
|
+
state.el.valDM.textContent = `${formatDM(lGMS)}, ${formatDM(nGMS)}`;
|
|
114
|
+
}
|
|
115
|
+
if (state.el.valMaps) {
|
|
116
|
+
state.el.valMaps.textContent = `${lat.toFixed(6)},${lng.toFixed(6)}`;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
function setDDInputs(lat, lng) {
|
|
120
|
+
if (state.el.latDD) state.el.latDD.value = lat.toFixed(6);
|
|
121
|
+
if (state.el.lngDD) state.el.lngDD.value = lng.toFixed(6);
|
|
122
|
+
}
|
|
123
|
+
function updateFromDD(lat, lng, upd=true) {
|
|
124
|
+
if (isNaN(lat) || isNaN(lng)) return;
|
|
125
|
+
if (upd) setDDInputs(lat, lng);
|
|
126
|
+
if (!upd && state.mode !== 'DD') return;
|
|
127
|
+
updateGMSDisplay(lat, lng);
|
|
128
|
+
updateResultDisplay(lat, lng);
|
|
129
|
+
saveHistory(lat, lng);
|
|
130
|
+
}
|
|
131
|
+
function createHistoryItem(item) {
|
|
132
|
+
return `<div class="history-item" data-id="${item.id}"><div class="hist-info" data-lat="${item.lat}" data-lng="${item.lng}">
|
|
133
|
+
<span class="hist-coords">${item.lat}, ${item.lng}</span><span class="hist-time">${item.time}</span></div><div class="hist-actions">
|
|
134
|
+
<button class="action-btn load-btn" data-lat="${item.lat}" data-lng="${item.lng}"><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></button>
|
|
135
|
+
<button class="action-btn delete-btn" data-id="${item.id}"><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></button></div></div>`;
|
|
136
|
+
}
|
|
137
|
+
function setupHistoryListeners() {
|
|
138
|
+
document.querySelectorAll('.load-btn').forEach(btn => {
|
|
139
|
+
btn.addEventListener('click', (evt) => {
|
|
140
|
+
evt.stopPropagation();
|
|
141
|
+
const lat = btn.getAttribute('data-lat');
|
|
142
|
+
const lng = btn.getAttribute('data-lng');
|
|
143
|
+
if (lat && lng) updateFromDD(parseFloat(lat), parseFloat(lng));
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
document.querySelectorAll('.delete-btn').forEach(btn => {
|
|
147
|
+
btn.addEventListener('click', (evt) => {
|
|
148
|
+
evt.stopPropagation();
|
|
149
|
+
const itemId = btn.getAttribute('data-id');
|
|
150
|
+
if (itemId) {
|
|
151
|
+
let history = JSON.parse(localStorage.getItem('gps_history') || '[]');
|
|
152
|
+
history = history.filter(it => it.id !== parseInt(itemId));
|
|
153
|
+
localStorage.setItem('gps_history', JSON.stringify(history));
|
|
154
|
+
renderHistory(history);
|
|
155
|
+
}
|
|
156
|
+
});
|
|
157
|
+
});
|
|
158
|
+
document.querySelectorAll('.hist-info').forEach(info => {
|
|
159
|
+
info.addEventListener('click', () => {
|
|
160
|
+
const lat = info.getAttribute('data-lat');
|
|
161
|
+
const lng = info.getAttribute('data-lng');
|
|
162
|
+
if (lat && lng) updateFromDD(parseFloat(lat), parseFloat(lng));
|
|
163
|
+
});
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
function renderHistory(history) {
|
|
167
|
+
if (!state.el.historyList) return;
|
|
168
|
+
if (history.length === 0) {
|
|
169
|
+
state.el.historyList.innerHTML = `<div class="no-history">${state.ui.noHistory}</div>`;
|
|
170
|
+
} else {
|
|
171
|
+
state.el.historyList.innerHTML = history.map(createHistoryItem).join('');
|
|
172
|
+
}
|
|
173
|
+
setupHistoryListeners();
|
|
174
|
+
}
|
|
175
|
+
function saveHistory(lat, lng) {
|
|
176
|
+
let history = JSON.parse(localStorage.getItem('gps_history') || '[]');
|
|
177
|
+
const entry = {
|
|
178
|
+
id: Date.now(),
|
|
179
|
+
lat: lat.toFixed(5),
|
|
180
|
+
lng: lng.toFixed(5),
|
|
181
|
+
time: new Date().toLocaleTimeString()
|
|
182
|
+
};
|
|
183
|
+
if (history.length > 0 && history[0].lat === entry.lat && history[0].lng === entry.lng) return;
|
|
184
|
+
history.unshift(entry);
|
|
185
|
+
history = history.slice(0, 5);
|
|
186
|
+
localStorage.setItem('gps_history', JSON.stringify(history));
|
|
187
|
+
renderHistory(history);
|
|
188
|
+
}
|
|
189
|
+
function loadHistory() {
|
|
190
|
+
const history = JSON.parse(localStorage.getItem('gps_history') || '[]');
|
|
191
|
+
renderHistory(history);
|
|
192
|
+
}
|
|
193
|
+
function initMap() {
|
|
194
|
+
const mapEl = id('gpsMap');
|
|
195
|
+
if (!mapEl || !state.L) return;
|
|
196
|
+
state.map = state.L.map('gpsMap').setView([40.4168, -3.7038], 13);
|
|
197
|
+
state.L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
|
198
|
+
attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors',
|
|
199
|
+
maxZoom: 19
|
|
200
|
+
}).addTo(state.map);
|
|
201
|
+
state.map.on('click', (evt) => updateFromDD(evt.latlng.lat, evt.latlng.lng));
|
|
202
|
+
}
|
|
203
|
+
function setupModeListeners() {
|
|
204
|
+
state.el.modeDD?.addEventListener('click', () => {
|
|
205
|
+
state.mode = 'DD';
|
|
206
|
+
state.el.modeDD?.classList.add('active');
|
|
207
|
+
state.el.modeGMS?.classList.remove('active');
|
|
208
|
+
if (state.el.ddInputs) state.el.ddInputs.style.display = 'grid';
|
|
209
|
+
if (state.el.gmsInputs) state.el.gmsInputs.style.display = 'none';
|
|
210
|
+
});
|
|
211
|
+
state.el.modeGMS?.addEventListener('click', () => {
|
|
212
|
+
state.mode = 'GMS';
|
|
213
|
+
state.el.modeGMS?.classList.add('active');
|
|
214
|
+
state.el.modeDD?.classList.remove('active');
|
|
215
|
+
if (state.el.ddInputs) state.el.ddInputs.style.display = 'none';
|
|
216
|
+
if (state.el.gmsInputs) state.el.gmsInputs.style.display = 'flex';
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
function setupInputListeners() {
|
|
220
|
+
state.el.btnMyLocation?.addEventListener('click', () => {
|
|
221
|
+
if ('geolocation' in navigator) {
|
|
222
|
+
navigator.geolocation.getCurrentPosition(pos => {
|
|
223
|
+
updateFromDD(pos.coords.latitude, pos.coords.longitude);
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
});
|
|
227
|
+
[state.el.latDD, state.el.lngDD].forEach(el => {
|
|
228
|
+
el?.addEventListener('input', () => {
|
|
229
|
+
if (state.mode === 'DD' && state.el.latDD && state.el.lngDD) {
|
|
230
|
+
updateFromDD(parseFloat(state.el.latDD.value), parseFloat(state.el.lngDD.value), false);
|
|
231
|
+
}
|
|
232
|
+
});
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
function setupCopyListeners() {
|
|
236
|
+
document.querySelectorAll('.copy-btn').forEach(btn => {
|
|
237
|
+
btn.addEventListener('click', () => {
|
|
238
|
+
const targetId = btn.getAttribute('data-copy');
|
|
239
|
+
const text = targetId ? id(targetId)?.textContent : null;
|
|
240
|
+
if (text) {
|
|
241
|
+
navigator.clipboard.writeText(text);
|
|
242
|
+
const original = btn.innerHTML;
|
|
243
|
+
btn.innerHTML = state.ui.copied;
|
|
244
|
+
setTimeout(() => { btn.innerHTML = original; }, 2000);
|
|
245
|
+
}
|
|
246
|
+
});
|
|
247
|
+
});
|
|
248
|
+
id('clearHistory')?.addEventListener('click', () => {
|
|
249
|
+
localStorage.removeItem('gps_history');
|
|
250
|
+
loadHistory();
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
function setupAll() {
|
|
254
|
+
setupModeListeners();
|
|
255
|
+
setupInputListeners();
|
|
256
|
+
setupCopyListeners();
|
|
257
|
+
setupHistoryListeners();
|
|
258
|
+
}
|
|
259
|
+
function checkLeaflet() {
|
|
260
|
+
if (window.L) {
|
|
261
|
+
state.L = window.L;
|
|
262
|
+
initMap();
|
|
263
|
+
setupAll();
|
|
264
|
+
loadHistory();
|
|
265
|
+
updateFromDD(40.4168, -3.7038);
|
|
266
|
+
} else {
|
|
267
|
+
setTimeout(checkLeaflet, 100);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
initState();
|
|
271
|
+
checkLeaflet();
|
|
272
|
+
</script>
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
|
|
@@ -1,15 +1,15 @@
|
|
|
1
|
-
---
|
|
2
|
-
import { SEORenderer } from '@jjlmoya/utils-shared';
|
|
3
|
-
import { gpsCoordinatesConverter } 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 gpsCoordinatesConverter.i18n[locale]?.();
|
|
12
|
-
if (!content) return null;
|
|
13
|
-
---
|
|
14
|
-
|
|
15
|
-
{content.seo?.length > 0 && <SEORenderer content={{ locale, sections: content.seo }} />}
|
|
1
|
+
---
|
|
2
|
+
import { SEORenderer } from '@jjlmoya/utils-shared';
|
|
3
|
+
import { gpsCoordinatesConverter } 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 gpsCoordinatesConverter.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
|
@@ -6,6 +6,7 @@ import { DRONE_POWER_ANALYZER_TOOL } from './tool/drone-power-analyzer/index';
|
|
|
6
6
|
import { GSD_FLIGHT_PLANNER_TOOL } from './tool/gsd-flight-planner/index';
|
|
7
7
|
import { DRONE_BATTERY_C_RATING_CALCULATOR_TOOL } from './tool/drone-battery-c-rating-calculator/index';
|
|
8
8
|
import { FPV_DRONE_THRUST_TO_WEIGHT_RATIO_TOOL } from './tool/fpv-drone-thrust-to-weight-ratio/index';
|
|
9
|
+
import { DRONE_MOTOR_PROPELLER_CALCULATOR_TOOL } from './tool/drone-motor-propeller-calculator/index';
|
|
9
10
|
import type { ToolDefinition } from './types';
|
|
10
11
|
|
|
11
12
|
export const ALL_TOOLS: ToolDefinition[] = [
|
|
@@ -16,6 +17,7 @@ export const ALL_TOOLS: ToolDefinition[] = [
|
|
|
16
17
|
GSD_FLIGHT_PLANNER_TOOL,
|
|
17
18
|
DRONE_BATTERY_C_RATING_CALCULATOR_TOOL,
|
|
18
19
|
FPV_DRONE_THRUST_TO_WEIGHT_RATIO_TOOL,
|
|
20
|
+
DRONE_MOTOR_PROPELLER_CALCULATOR_TOOL,
|
|
19
21
|
];
|
|
20
22
|
|
|
21
23
|
export {
|
|
@@ -26,4 +28,5 @@ export {
|
|
|
26
28
|
GSD_FLIGHT_PLANNER_TOOL,
|
|
27
29
|
DRONE_BATTERY_C_RATING_CALCULATOR_TOOL,
|
|
28
30
|
FPV_DRONE_THRUST_TO_WEIGHT_RATIO_TOOL,
|
|
31
|
+
DRONE_MOTOR_PROPELLER_CALCULATOR_TOOL,
|
|
29
32
|
};
|