@jjlmoya/utils-drones 1.5.0 → 1.6.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.
@@ -30,246 +30,149 @@ const { ui } = Astro.props;
30
30
  </div>
31
31
  </div>
32
32
 
33
- <script is:inline>
34
- let mode = 'DD';
35
- let mapInstance = null;
36
- let L = null;
37
-
38
- const elements = {
39
- latDD: document.getElementById('latDD'),
40
- lngDD: document.getElementById('lngDD'),
41
- latG: document.getElementById('latG'),
42
- latM: document.getElementById('latM'),
43
- latS: document.getElementById('latS'),
44
- latH: document.getElementById('latH'),
45
- lngG: document.getElementById('lngG'),
46
- lngM: document.getElementById('lngM'),
47
- lngS: document.getElementById('lngS'),
48
- lngH: document.getElementById('lngH'),
49
- valDD: document.getElementById('valDD'),
50
- valGMS: document.getElementById('valGMS'),
51
- valDM: document.getElementById('valDM'),
52
- valMaps: document.getElementById('valMaps'),
53
- historyList: document.getElementById('historyList'),
54
- modeDD: document.getElementById('modeDD'),
55
- modeGMS: document.getElementById('modeGMS'),
56
- ddInputs: document.getElementById('dd-inputs'),
57
- gmsInputs: document.getElementById('gms-inputs'),
58
- btnMyLocation: document.getElementById('btnMyLocation'),
59
- };
60
- const container = document.querySelector('.gps-converter-ui');
61
- const uiLocal = {
62
- copied: container?.dataset.uiCopied || 'Copied',
63
- noHistory: container?.dataset.uiNoHistory || 'No history',
64
- load: container?.dataset.uiLoad || 'Load',
65
- delete: container?.dataset.uiDelete || 'Delete'
66
- };
67
-
68
- function init() {
69
- import('leaflet').then(module => {
70
- L = module.default;
71
- initMap();
72
- setupListeners();
73
- loadHistory();
74
- updateFromDD(40.4168, -3.7038);
75
- });
76
- }
77
-
78
- function initMap() {
79
- const mapContainer = document.getElementById('gpsMap');
80
- if (!mapContainer) return;
81
-
82
- mapInstance = L.map('gpsMap').setView([40.4168, -3.7038], 13);
83
- L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
84
- attribution: '© OpenStreetMap contributors',
85
- maxZoom: 19
86
- }).addTo(mapInstance);
87
-
88
- mapInstance.on('click', (e) => {
89
- updateFromDD(e.latlng.lat, e.latlng.lng);
90
- });
91
- }
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>
92
35
 
93
- function parseCoordValue(val) {
94
- return parseFloat(val.replace(',', '.'));
95
- }
96
-
97
- function setupModeListeners() {
98
- elements.modeDD?.addEventListener('click', () => switchMode('DD'));
99
- elements.modeGMS?.addEventListener('click', () => switchMode('GMS'));
36
+ <script is:inline>
37
+ function getHemisphere(val, isLat) {
38
+ if (val >= 0) return isLat ? 'N' : 'E';
39
+ return isLat ? 'S' : 'W';
100
40
  }
101
-
102
- function setupLocationListener() {
103
- elements.btnMyLocation?.addEventListener('click', () => {
104
- if ("geolocation" in navigator) {
105
- navigator.geolocation.getCurrentPosition((position) => {
106
- updateFromDD(position.coords.latitude, position.coords.longitude);
107
- });
108
- }
109
- });
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};
110
48
  }
111
-
112
- function setupDDInputListeners() {
113
- [elements.latDD, elements.lngDD].forEach(el => {
114
- el?.addEventListener('input', () => {
115
- if (mode === 'DD') {
116
- const lat = parseCoordValue(elements.latDD.value);
117
- const lng = parseCoordValue(elements.lngDD.value);
118
- updateFromDD(lat, lng, false);
119
- }
120
- });
121
- });
49
+ function formatGMS(obj) {
50
+ return `${obj.g}° ${obj.m}' ${obj.s.toFixed(2)}" ${obj.h}`;
122
51
  }
123
-
124
- function setupGMSInputListeners() {
125
- const gmsElements = [elements.latG, elements.latM, elements.latS, elements.latH,
126
- elements.lngG, elements.lngM, elements.lngS, elements.lngH];
127
- gmsElements.forEach(el => {
128
- el?.addEventListener('input', () => {
129
- if (mode === 'GMS') {
130
- const lat = gmsToDecimal(
131
- parseFloat(elements.latG.value) || 0,
132
- parseFloat(elements.latM.value) || 0,
133
- parseFloat(elements.latS.value) || 0,
134
- elements.latH.value
135
- );
136
- const lng = gmsToDecimal(
137
- parseFloat(elements.lngG.value) || 0,
138
- parseFloat(elements.lngM.value) || 0,
139
- parseFloat(elements.lngS.value) || 0,
140
- elements.lngH.value
141
- );
142
- updateFromDD(lat, lng, false);
143
- }
144
- });
145
- });
52
+ function formatDM(obj) {
53
+ return `${obj.g}° ${obj.mDec.toFixed(3)}' ${obj.h}`;
146
54
  }
147
-
148
- function setupCopyListeners() {
149
- document.querySelectorAll('.copy-btn').forEach(btn => {
150
- btn.addEventListener('click', () => {
151
- const targetId = btn.getAttribute('data-copy');
152
- const text = document.getElementById(targetId)?.textContent;
153
- if (text) {
154
- navigator.clipboard.writeText(text);
155
- const original = btn.innerHTML;
156
- btn.innerHTML = uiLocal.copied;
157
- setTimeout(() => btn.innerHTML = original, 2000);
158
- }
159
- });
160
- });
55
+ function id(name) {
56
+ return document.getElementById(name);
161
57
  }
162
-
163
- function setupClearHistoryListener() {
164
- document.getElementById('clearHistory')?.addEventListener('click', () => {
165
- localStorage.removeItem('gps_history');
166
- loadHistory();
167
- });
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
+ };
168
68
  }
169
-
170
- function setupListeners() {
171
- setupModeListeners();
172
- setupLocationListener();
173
- setupDDInputListeners();
174
- setupGMSInputListeners();
175
- setupCopyListeners();
176
- setupClearHistoryListener();
69
+ function getUIValue(attr, def) {
70
+ const ct = document.querySelector('.gps-converter-ui');
71
+ return ct?.dataset[attr] || def;
177
72
  }
178
-
179
- function setSwitchModeDisplay(isDD) {
180
- if (isDD) {
181
- if (elements.ddInputs) elements.ddInputs.style.display = 'grid';
182
- if (elements.gmsInputs) elements.gmsInputs.style.display = 'none';
183
- } else {
184
- if (elements.ddInputs) elements.ddInputs.style.display = 'none';
185
- if (elements.gmsInputs) elements.gmsInputs.style.display = 'flex';
186
- }
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
+ };
187
80
  }
188
-
189
- function setSwitchModeActive(isDD) {
190
- if (isDD) {
191
- elements.modeDD?.classList.add('active');
192
- elements.modeGMS?.classList.remove('active');
193
- } else {
194
- elements.modeDD?.classList.remove('active');
195
- elements.modeGMS?.classList.add('active');
196
- }
81
+ function initState() {
82
+ initElements();
83
+ initUI();
197
84
  }
198
-
199
- function switchMode(newMode) {
200
- mode = newMode;
201
- const isDD = mode === 'DD';
202
- setSwitchModeActive(isDD);
203
- setSwitchModeDisplay(isDD);
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;
204
90
  }
205
-
206
- function updateGMSInputs(lat, lng) {
207
- const latGMS = decimalToGMS(lat, true);
208
- const lngGMS = decimalToGMS(lng, false);
209
- elements.latG.value = latGMS.g.toString();
210
- elements.latM.value = latGMS.m.toString();
211
- elements.latS.value = latGMS.s.toFixed(2);
212
- elements.latH.value = latGMS.h;
213
- elements.lngG.value = lngGMS.g.toString();
214
- elements.lngM.value = lngGMS.m.toString();
215
- elements.lngS.value = lngGMS.s.toFixed(2);
216
- elements.lngH.value = lngGMS.h;
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;
217
96
  }
218
-
219
- function updateResultsDisplay(lat, lng) {
220
- const latGMS = decimalToGMS(lat, true);
221
- const lngGMS = decimalToGMS(lng, false);
222
- if (elements.valDD) elements.valDD.textContent = `${lat.toFixed(6)}, ${lng.toFixed(6)}`;
223
- if (elements.valGMS) elements.valGMS.textContent = `${formatGMS(latGMS)}, ${formatGMS(lngGMS)}`;
224
- if (elements.valDM) elements.valDM.textContent = `${formatDM(latGMS)}, ${formatDM(lngGMS)}`;
225
- if (elements.valMaps) elements.valMaps.textContent = `${lat.toFixed(6)},${lng.toFixed(6)}`;
97
+ function updateGMSDisplay(lat, lng) {
98
+ const lGMS = decimalToGMS(lat, true);
99
+ const nGMS = decimalToGMS(lng, false);
100
+ setLatGMS(lGMS);
101
+ setLngGMS(nGMS);
226
102
  }
227
-
228
- function updateFromDD(lat, lng, updateInputs = true) {
229
- if (isNaN(lat) || isNaN(lng)) return;
230
-
231
- if (updateInputs) {
232
- elements.latDD.value = lat.toFixed(6);
233
- elements.lngDD.value = lng.toFixed(6);
234
- updateGMSInputs(lat, lng);
235
- } else if (mode === 'DD') {
236
- updateGMSInputs(lat, lng);
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)}`;
237
117
  }
238
-
239
- updateResultsDisplay(lat, lng);
240
- saveToHistory(lat, lng);
241
118
  }
242
-
243
- function getHemisphere(value, isLat) {
244
- if (value >= 0) return isLat ? 'N' : 'E';
245
- return isLat ? 'S' : 'W';
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);
246
122
  }
247
-
248
- function decimalToGMS(d, isLat) {
249
- const h = getHemisphere(d, isLat);
250
- const absD = Math.abs(d);
251
- const g = Math.floor(absD);
252
- const mDecimal = (absD - g) * 60;
253
- const m = Math.floor(mDecimal);
254
- const s = (mDecimal - m) * 60;
255
- return { g, m, s, h, mDec: mDecimal };
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);
256
130
  }
257
-
258
- function gmsToDecimal(g, m, s, h) {
259
- let d = g + (m / 60) + (s / 3600);
260
- if (h === 'S' || h === 'W') d = -d;
261
- return d;
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>`;
262
136
  }
263
-
264
- function formatGMS(obj) {
265
- return `${obj.g}° ${obj.m}' ${obj.s.toFixed(2)}" ${obj.h}`;
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
+ });
266
165
  }
267
-
268
- function formatDM(obj) {
269
- return `${obj.g}° ${obj.mDec.toFixed(3)}' ${obj.h}`;
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();
270
174
  }
271
-
272
- function saveToHistory(lat, lng) {
175
+ function saveHistory(lat, lng) {
273
176
  let history = JSON.parse(localStorage.getItem('gps_history') || '[]');
274
177
  const entry = {
275
178
  id: Date.now(),
@@ -277,382 +180,403 @@ const { ui } = Astro.props;
277
180
  lng: lng.toFixed(5),
278
181
  time: new Date().toLocaleTimeString()
279
182
  };
280
-
281
183
  if (history.length > 0 && history[0].lat === entry.lat && history[0].lng === entry.lng) return;
282
-
283
184
  history.unshift(entry);
284
185
  history = history.slice(0, 5);
285
186
  localStorage.setItem('gps_history', JSON.stringify(history));
286
187
  renderHistory(history);
287
188
  }
288
-
289
189
  function loadHistory() {
290
190
  const history = JSON.parse(localStorage.getItem('gps_history') || '[]');
291
191
  renderHistory(history);
292
192
  }
293
-
294
- function renderHistoryHTML(history) {
295
- return history.map(item => `
296
- <div class="history-item" data-id="${item.id}">
297
- <div class="hist-info" data-lat="${item.lat}" data-lng="${item.lng}">
298
- <span class="hist-coords">${item.lat}, ${item.lng}</span>
299
- <span class="hist-time">${item.time}</span>
300
- </div>
301
- <div class="hist-actions">
302
- <button class="action-btn load-btn" title="${uiLocal.load}" data-lat="${item.lat}" data-lng="${item.lng}">
303
- <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>
304
- </button>
305
- <button class="action-btn delete-btn" title="${uiLocal.delete}" data-id="${item.id}">
306
- <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>
307
- </button>
308
- </div>
309
- </div>
310
- `).join('');
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: '&copy; <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));
311
202
  }
312
-
313
- function setupHistoryEventListeners() {
314
- document.querySelectorAll('.load-btn').forEach(btn => {
315
- btn.addEventListener('click', (e) => {
316
- e.stopPropagation();
317
- const lat = parseFloat(btn.getAttribute('data-lat')!);
318
- const lng = parseFloat(btn.getAttribute('data-lng')!);
319
- updateFromDD(lat, lng);
320
- });
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';
321
210
  });
322
-
323
- document.querySelectorAll('.delete-btn').forEach(btn => {
324
- btn.addEventListener('click', (e) => {
325
- e.stopPropagation();
326
- const id = parseInt(btn.getAttribute('data-id')!);
327
- let history = JSON.parse(localStorage.getItem('gps_history') || '[]');
328
- history = history.filter((item) => item.id !== id);
329
- localStorage.setItem('gps_history', JSON.stringify(history));
330
- renderHistory(history);
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
+ }
331
232
  });
332
233
  });
333
-
334
- document.querySelectorAll('.hist-info').forEach(info => {
335
- info.addEventListener('click', () => {
336
- const lat = parseFloat(info.getAttribute('data-lat')!);
337
- const lng = parseFloat(info.getAttribute('data-lng')!);
338
- updateFromDD(lat, lng);
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
+ }
339
246
  });
340
247
  });
248
+ id('clearHistory')?.addEventListener('click', () => {
249
+ localStorage.removeItem('gps_history');
250
+ loadHistory();
251
+ });
341
252
  }
342
-
343
- function renderHistory(history) {
344
- if (!elements.historyList) return;
345
- if (history.length === 0) {
346
- elements.historyList.innerHTML = `<div class="no-history">${uiLocal.noHistory}</div>`;
347
- return;
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);
348
268
  }
349
- elements.historyList.innerHTML = renderHistoryHTML(history);
350
- setupHistoryEventListeners();
351
269
  }
352
-
353
- init();
270
+ initState();
271
+ checkLeaflet();
354
272
  </script>
355
273
 
356
274
  <style>
357
275
  .gps-converter-ui {
358
- --gps-accent: #0ea5e9;
359
- --gps-accent-glow: rgba(14, 165, 233, 0.15);
360
- --gps-bg: #fff;
361
- --gps-bg-muted: #f5f5f5;
362
- --gps-bg-surface: #eee;
363
- --gps-bg-overlay: #f0f0f0;
364
- --gps-text: #1a1a1a;
365
- --gps-text-muted: #4a4a4a;
366
- --gps-text-dim: #707070;
367
- --gps-border: #d0d0d0;
368
- --gps-border-light: #e8e8e8;
369
- --gps-shadow: 0 30px 60px rgba(0, 0, 0, 0.08);
370
- --gps-danger: #ef4444;
371
- --gps-danger-bg: rgba(239, 68, 68, 0.1);
372
-
373
- width: 100%;
374
- max-width: 1100px;
375
- margin: 0 auto;
276
+ --gps-accent: #0ea5e9;
277
+ --gps-accent-rgb: 14, 165, 233;
278
+ --gps-accent-glow: rgba(14, 165, 233, 0.15);
279
+ --gps-bg: #fff;
280
+ --gps-bg-muted: #f8fafc;
281
+ --gps-bg-surface: #f1f5f9;
282
+ --gps-bg-overlay: #fdfdfd;
283
+ --gps-text: #1e293b;
284
+ --gps-text-muted: #64748b;
285
+ --gps-text-dim: #94a3b8;
286
+ --gps-border: #e2e8f0;
287
+ --gps-border-light: #f1f5f9;
288
+ --gps-shadow: 0 40px 100px -20px rgba(0, 0, 0, 0.08);
289
+ --gps-danger: #ef4444;
290
+ --gps-danger-bg: rgba(239, 68, 68, 0.1);
291
+
292
+ width: 100%;
293
+ max-width: 1200px;
294
+ margin: 2rem auto;
295
+ padding: 0 1.5rem;
376
296
  }
377
297
 
378
298
  :global([data-theme="dark"]) .gps-converter-ui,
379
299
  :global(.theme-dark) .gps-converter-ui {
380
- --gps-bg: #1a1a1a;
381
- --gps-bg-muted: #2d2d2d;
382
- --gps-bg-surface: #3a3a3a;
383
- --gps-bg-overlay: #292929;
384
- --gps-text: #f5f5f5;
385
- --gps-text-muted: #b0b0b0;
386
- --gps-text-dim: #888;
387
- --gps-border: #4a4a4a;
388
- --gps-border-light: #525252;
389
- --gps-shadow: 0 30px 60px rgba(0, 0, 0, 0.08);
300
+ --gps-bg: #0f172a;
301
+ --gps-bg-muted: #1e293b;
302
+ --gps-bg-surface: #334155;
303
+ --gps-bg-overlay: #1e293b;
304
+ --gps-text: #f1f5f9;
305
+ --gps-text-muted: #94a3b8;
306
+ --gps-text-dim: #64748b;
307
+ --gps-border: #334155;
308
+ --gps-border-light: #1e293b;
309
+ --gps-shadow: 0 40px 100px -20px rgba(0, 0, 0, 0.4);
390
310
  }
391
311
 
392
312
  .gps-converter-ui .tech-mega-card {
393
- background: var(--gps-bg);
394
- backdrop-filter: blur(20px);
395
- border: 1px solid var(--gps-border);
396
- border-radius: 32px;
397
- box-shadow: var(--gps-shadow);
398
- overflow: hidden;
313
+ background: var(--gps-bg);
314
+ backdrop-filter: blur(24px) saturate(180%);
315
+ -webkit-backdrop-filter: blur(24px) saturate(180%);
316
+ border: 1px solid var(--gps-border);
317
+ border-radius: 40px;
318
+ box-shadow: var(--gps-shadow);
319
+ position: relative;
320
+ overflow: hidden;
321
+ }
322
+
323
+ .gps-converter-ui .tech-mega-card::before {
324
+ content: '';
325
+ position: absolute;
326
+ top: 0;
327
+ left: 0;
328
+ right: 0;
329
+ height: 6px;
330
+ background: linear-gradient(90deg, #0ea5e9, #6366f1, #0ea5e9);
331
+ background-size: 200% 100%;
332
+ animation: gradient-shift-gps 4s linear infinite;
333
+ z-index: 10;
334
+ }
335
+
336
+ @keyframes gradient-shift-gps {
337
+ 0% { background-position: 0% 50%; }
338
+ 100% { background-position: 200% 50%; }
399
339
  }
400
340
 
401
341
  .gps-converter-ui .card-grid {
402
- display: grid;
403
- grid-template-columns: 420px 1fr;
342
+ display: grid;
343
+ grid-template-columns: 400px 1fr;
344
+ min-height: 700px;
404
345
  }
405
346
 
406
347
  .gps-converter-ui .config-sidebar {
407
- padding: 2rem;
408
- background: var(--gps-bg-overlay);
409
- display: flex;
410
- flex-direction: column;
411
- gap: 2rem;
412
- border-right: 1px solid var(--gps-border-light);
413
- }
414
-
415
- :global([data-theme="dark"]) .gps-converter-ui .config-sidebar,
416
- :global(.theme-dark) .gps-converter-ui .config-sidebar {
417
- background: var(--gps-bg-surface);
418
- border-right-color: var(--gps-border-light);
348
+ padding: 3.5rem 3rem;
349
+ background: var(--gps-bg-overlay);
350
+ border-right: 1px solid var(--gps-border-light);
351
+ display: flex;
352
+ flex-direction: column;
353
+ gap: 3rem;
419
354
  }
420
355
 
421
356
  .gps-converter-ui .main-display {
422
- padding: 2rem;
423
- display: flex;
424
- flex-direction: column;
425
- gap: 2rem;
426
- background: var(--gps-bg);
427
- }
428
-
429
- .gps-converter-ui .tech-section {
430
- display: flex;
431
- flex-direction: column;
432
- gap: 1.5rem;
433
- }
434
-
435
- .gps-converter-ui .section-title {
436
- font-size: 0.9rem;
437
- font-weight: 800;
438
- color: var(--gps-text);
439
- text-transform: uppercase;
440
- letter-spacing: 0.05em;
441
- margin: 0;
357
+ padding: 3.5rem 3rem;
358
+ display: flex;
359
+ flex-direction: column;
360
+ gap: 3rem;
361
+ background: var(--gps-bg);
442
362
  }
443
363
 
444
364
  .gps-converter-ui .divider {
445
- height: 1px;
446
- width: 100%;
447
- background: linear-gradient(90deg, transparent, var(--gps-border-light), transparent);
448
- }
449
-
450
- .gps-converter-ui .compact-inputs {
451
- display: grid;
452
- grid-template-columns: 1fr 1fr;
453
- gap: 1rem;
454
- }
455
-
456
- .gps-converter-ui .input-block {
457
- display: flex;
458
- flex-direction: column;
459
- gap: 0.5rem;
460
- }
461
-
462
- .gps-converter-ui .field-label {
463
- font-size: 0.75rem;
464
- font-weight: 700;
465
- color: var(--gps-text-muted);
466
- text-transform: uppercase;
467
- }
468
-
469
- .gps-converter-ui .tech-input {
470
- width: 100%;
471
- padding: 0.75rem;
472
- background: var(--gps-bg-surface);
473
- border: 1px solid var(--gps-border);
474
- border-radius: 12px;
475
- font-weight: 600;
476
- color: var(--gps-text);
477
- }
478
-
479
- .gps-converter-ui .mode-switch {
480
- display: flex;
481
- background: var(--gps-bg-surface);
482
- padding: 0.3rem;
483
- border-radius: 14px;
484
- }
485
-
486
- .gps-converter-ui .mode-btn {
487
- flex: 1;
488
- padding: 0.6rem;
489
- border-radius: 10px;
490
- border: none;
491
- background: transparent;
492
- font-size: 0.8rem;
493
- font-weight: 700;
494
- color: var(--gps-text-muted);
495
- cursor: pointer;
496
- transition: all 0.2s;
497
- }
498
-
499
- .gps-converter-ui .mode-btn.active {
500
- background: var(--gps-bg);
501
- color: var(--gps-accent);
502
- box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05);
365
+ height: 1px;
366
+ width: 100%;
367
+ background: linear-gradient(90deg, transparent, var(--gps-border-light), transparent);
368
+ }
369
+
370
+ .gps-converter-ui :global(.tech-section) {
371
+ display: flex;
372
+ flex-direction: column;
373
+ gap: 1.5rem;
374
+ }
375
+
376
+ .gps-converter-ui :global(.section-title) {
377
+ font-size: 0.75rem;
378
+ font-weight: 800;
379
+ color: var(--gps-text-muted);
380
+ text-transform: uppercase;
381
+ letter-spacing: 0.15em;
382
+ }
383
+
384
+ .gps-converter-ui :global(.field-label) {
385
+ font-size: 0.7rem;
386
+ font-weight: 800;
387
+ color: var(--gps-text-dim);
388
+ text-transform: uppercase;
389
+ letter-spacing: 0.05em;
390
+ margin-bottom: 0.5rem;
391
+ display: block;
392
+ }
393
+
394
+ .gps-converter-ui :global(.tech-input) {
395
+ width: 100%;
396
+ padding: 1rem 1.25rem;
397
+ background: var(--gps-bg-surface);
398
+ border: 1px solid var(--gps-border);
399
+ border-radius: 16px;
400
+ font-weight: 700;
401
+ color: var(--gps-text);
402
+ font-size: 1rem;
403
+ transition: all 0.3s ease;
404
+ }
405
+
406
+ .gps-converter-ui :global(.tech-input:focus) {
407
+ outline: none;
408
+ border-color: var(--gps-accent);
409
+ box-shadow: 0 0 0 4px var(--gps-accent-glow);
410
+ background: var(--gps-bg);
411
+ }
412
+
413
+ .gps-converter-ui :global(.mode-switch) {
414
+ display: flex;
415
+ background: var(--gps-bg-surface);
416
+ padding: 0.4rem;
417
+ border-radius: 18px;
418
+ gap: 0.25rem;
419
+ }
420
+
421
+ .gps-converter-ui :global(.mode-btn) {
422
+ flex: 1;
423
+ padding: 0.75rem;
424
+ border-radius: 14px;
425
+ border: none;
426
+ background: transparent;
427
+ font-size: 0.75rem;
428
+ font-weight: 800;
429
+ color: var(--gps-text-muted);
430
+ cursor: pointer;
431
+ transition: all 0.3s ease;
432
+ }
433
+
434
+ .gps-converter-ui :global(.mode-btn.active) {
435
+ background: var(--gps-bg);
436
+ color: var(--gps-accent);
437
+ box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.1);
438
+ }
439
+
440
+ .gps-converter-ui :global(.map-container) {
441
+ max-width: 100%;
442
+ height: 400px;
443
+ border-radius: 32px;
444
+ overflow: hidden;
445
+ background: var(--gps-bg-surface);
446
+ border: 1px solid var(--gps-border-light);
447
+ }
448
+
449
+ .gps-converter-ui :global(.results-area) {
450
+ display: grid;
451
+ grid-template-columns: repeat(2, 1fr);
452
+ gap: 2rem;
453
+ }
454
+
455
+ .gps-converter-ui :global(.result-box) {
456
+ background: var(--gps-bg-overlay);
457
+ border: 1px solid var(--gps-border-light);
458
+ border-radius: 28px;
459
+ padding: 2.5rem;
460
+ display: flex;
461
+ flex-direction: column;
462
+ gap: 1rem;
463
+ transition: all 0.3s ease;
464
+ }
465
+
466
+ .gps-converter-ui :global(.result-box:hover) {
467
+ transform: translateY(-5px);
468
+ box-shadow: 0 20px 40px rgba(0, 0, 0, 0.04);
469
+ border-color: var(--gps-accent);
470
+ }
471
+
472
+ .gps-converter-ui :global(.res-header) {
473
+ display: flex;
474
+ justify-content: space-between;
475
+ align-items: center;
476
+ }
477
+
478
+ .gps-converter-ui :global(.copy-btn) {
479
+ background: var(--gps-bg-surface);
480
+ border: 1px solid var(--gps-border);
481
+ color: var(--gps-text-muted);
482
+ padding: 0.5rem 1rem;
483
+ border-radius: 12px;
484
+ font-size: 0.7rem;
485
+ font-weight: 800;
486
+ cursor: pointer;
487
+ display: flex;
488
+ align-items: center;
489
+ gap: 0.5rem;
490
+ transition: all 0.3s ease;
491
+ }
492
+
493
+ .gps-converter-ui :global(.copy-btn:hover) {
494
+ background: var(--gps-accent);
495
+ color: white;
496
+ border-color: var(--gps-accent);
497
+ }
498
+
499
+ .gps-converter-ui :global(.val-display) {
500
+ font-size: 1.5rem;
501
+ font-weight: 900;
502
+ color: var(--gps-text);
503
+ word-break: break-all;
504
+ letter-spacing: -0.01em;
505
+ }
506
+
507
+ .gps-converter-ui :global(.history-item) {
508
+ padding: 1.25rem 1.5rem;
509
+ background: var(--gps-bg-surface);
510
+ border-radius: 20px;
511
+ display: flex;
512
+ justify-content: space-between;
513
+ align-items: center;
514
+ transition: all 0.3s ease;
515
+ border: 1px solid transparent;
516
+ }
517
+
518
+ .gps-converter-ui :global(.history-item:hover) {
519
+ background: var(--gps-bg);
520
+ border-color: var(--gps-accent);
521
+ transform: translateX(5px);
522
+ }
523
+
524
+ .gps-converter-ui :global(.hist-info) {
525
+ display: flex;
526
+ flex-direction: column;
527
+ cursor: pointer;
528
+ flex: 1;
503
529
  }
504
530
 
505
- .gps-converter-ui .map-container {
506
- width: 100%;
507
- height: 350px;
508
- border-radius: 20px;
509
- overflow: hidden;
510
- background: var(--gps-bg-surface);
511
- border: 1px solid var(--gps-border-light);
531
+ .gps-converter-ui :global(.hist-coords) {
532
+ font-size: 0.9rem;
533
+ font-weight: 800;
534
+ color: var(--gps-text);
535
+ }
536
+
537
+ .gps-converter-ui :global(.hist-time) {
538
+ font-size: 0.7rem;
539
+ color: var(--gps-text-dim);
540
+ font-weight: 700;
541
+ text-transform: uppercase;
542
+ }
543
+
544
+ .gps-converter-ui :global(.action-btn) {
545
+ background: var(--gps-bg-overlay);
546
+ border: 1px solid var(--gps-border);
547
+ padding: 0.6rem;
548
+ border-radius: 12px;
549
+ color: var(--gps-text-dim);
550
+ cursor: pointer;
551
+ display: flex;
552
+ align-items: center;
553
+ justify-content: center;
554
+ transition: all 0.2s ease;
555
+ }
556
+
557
+ .gps-converter-ui :global(.no-history) {
558
+ text-align: center;
559
+ padding: 3rem;
560
+ color: var(--gps-text-dim);
561
+ font-style: italic;
562
+ font-size: 0.9rem;
512
563
  }
513
564
 
514
- .gps-converter-ui .results-area {
515
- display: grid;
516
- grid-template-columns: 1fr 1fr;
517
- gap: 1.5rem;
518
- }
519
-
520
- .gps-converter-ui .result-box {
521
- background: var(--gps-bg-overlay);
522
- border: 1px solid var(--gps-border-light);
523
- border-radius: 20px;
524
- padding: 1.5rem;
525
- display: flex;
526
- flex-direction: column;
527
- gap: 0.75rem;
528
- }
529
-
530
- .gps-converter-ui .res-header {
531
- display: flex;
532
- justify-content: space-between;
533
- align-items: center;
534
- }
535
-
536
- .gps-converter-ui .res-label {
537
- font-size: 0.7rem;
538
- font-weight: 700;
539
- color: var(--gps-text-dim);
540
- text-transform: uppercase;
541
- }
542
-
543
- .gps-converter-ui .copy-btn {
544
- background: transparent;
545
- border: 1px solid var(--gps-border);
546
- color: var(--gps-text-muted);
547
- padding: 0.3rem 0.6rem;
548
- border-radius: 8px;
549
- font-size: 0.7rem;
550
- font-weight: 700;
551
- cursor: pointer;
552
- display: flex;
553
- align-items: center;
554
- gap: 0.4rem;
555
- transition: all 0.2s;
556
- }
557
-
558
- .gps-converter-ui .copy-btn:hover {
559
- background: var(--gps-accent);
560
- color: white;
561
- border-color: var(--gps-accent);
562
- }
563
-
564
- .gps-converter-ui .val-display {
565
- font-size: 1.35rem;
566
- font-weight: 800;
567
- color: var(--gps-text);
568
- }
569
-
570
- .gps-converter-ui .history-list {
571
- display: flex;
572
- flex-direction: column;
573
- gap: 0.5rem;
574
- }
575
-
576
- .gps-converter-ui .history-item {
577
- padding: 0.75rem 1rem;
578
- background: var(--gps-bg-surface);
579
- border-radius: 12px;
580
- display: flex;
581
- justify-content: space-between;
582
- align-items: center;
583
- transition: all 0.2s;
584
- }
585
-
586
- .gps-converter-ui .history-item:hover {
587
- background: var(--gps-border);
588
- }
589
-
590
- .gps-converter-ui .hist-info {
591
- display: flex;
592
- flex-direction: column;
593
- gap: 0.1rem;
594
- cursor: pointer;
595
- flex: 1;
596
- }
597
-
598
- .gps-converter-ui .hist-coords {
599
- font-size: 0.85rem;
600
- font-weight: 700;
601
- color: var(--gps-text);
602
- }
603
-
604
- .gps-converter-ui .hist-time {
605
- font-size: 0.65rem;
606
- color: var(--gps-text-dim);
607
- text-transform: uppercase;
608
- font-weight: 600;
609
- }
610
-
611
- .gps-converter-ui .hist-actions {
612
- display: flex;
613
- gap: 0.5rem;
614
- }
615
-
616
- .gps-converter-ui .action-btn {
617
- background: transparent;
618
- border: none;
619
- padding: 0.4rem;
620
- border-radius: 8px;
621
- color: var(--gps-text-dim);
622
- cursor: pointer;
623
- display: flex;
624
- align-items: center;
625
- justify-content: center;
626
- transition: all 0.2s;
627
- }
628
-
629
- .gps-converter-ui .load-btn:hover {
630
- color: var(--gps-accent);
631
- background: var(--gps-accent-glow);
632
- }
633
-
634
- .gps-converter-ui .delete-btn:hover {
635
- color: var(--gps-danger);
636
- background: var(--gps-danger-bg);
565
+ @media (max-width: 1200px) {
566
+ .gps-converter-ui .card-grid {
567
+ grid-template-columns: 1fr;
568
+ }
569
+ .gps-converter-ui .config-sidebar {
570
+ border-right: none;
571
+ border-bottom: 1px solid var(--gps-border-light);
572
+ }
637
573
  }
638
574
 
639
- @media (max-width: 992px) {
640
- .gps-converter-ui .card-grid {
641
- grid-template-columns: 1fr;
642
- }
643
-
644
- .gps-converter-ui .config-sidebar {
645
- border-right: none;
646
- border-bottom: 1px solid var(--gps-border-light);
647
- }
648
-
649
- :global([data-theme="dark"]) .gps-converter-ui .config-sidebar,
650
- :global(.theme-dark) .gps-converter-ui .config-sidebar {
651
- border-bottom-color: var(--gps-border-light);
652
- }
653
-
654
- .gps-converter-ui .results-area {
655
- grid-template-columns: 1fr;
656
- }
575
+ @media (max-width: 600px) {
576
+ .gps-converter-ui { padding: 0.5rem; }
577
+ .gps-converter-ui .config-sidebar,
578
+ .gps-converter-ui .main-display { padding: 2rem 1.5rem; }
579
+ .gps-converter-ui :global(.results-area) { grid-template-columns: 1fr; }
657
580
  }
658
581
  </style>
582
+