1/** 2 * CARTA NATAL - Módulo completo 3 * - Búsqueda de ciudades (Nominatim) 4 * - Timezone automático 5 * - Coordenadas en formato sexagesimal (grados, minutos, segundos, hemisferio) 6 * - Huso horario editable 7 * - Guardar/cargar cartas en localStorage 8 * - Exportar/Importar JSON 9 * - Imprimir y PDF 10 * Reutiliza el modal cf-modal-carta de carta-fase.js 11 */ 12 13var cnBuscando = false; 14var cnDebounceTimer = null; 15var cnUltimaCartaData = null; 16 17// ============================================================================ 18// INICIALIZACIÃN 19// ============================================================================ 20 21document.addEventListener('DOMContentLoaded', function() { 22 var input = document.getElementById('cn-ciudad'); 23 if (!input) return; 24 25 input.addEventListener('input', function() { 26 clearTimeout(cnDebounceTimer); 27 var query = this.value.trim(); 28 if (query.length < 3) { 29 document.getElementById('cn-resultados').style.display = 'none'; 30 return; 31 } 32 cnDebounceTimer = setTimeout(function() { cnBuscarCiudad(query); }, 400); 33 }); 34 35 document.addEventListener('click', function(e) { 36 if (!e.target.closest('#cn-ciudad') && !e.target.closest('#cn-resultados')) { 37 document.getElementById('cn-resultados').style.display = 'none'; 38 } 39 }); 40 41 var fechaInput = document.getElementById('cn-fecha'); 42 if (fechaInput) { 43 fechaInput.addEventListener('change', function() { 44 var lat = document.getElementById('cn-lat').value; 45 var lon = document.getElementById('cn-lon').value; 46 if (lat && lon) cnObtenerTimezone(parseFloat(lat), parseFloat(lon)); 47 }); 48 } 49 50 // Validación: solo números en campos sexagesimales 51 document.querySelectorAll('.cn-sex-input, .cn-huso-input').forEach(function(inp) { 52 inp.addEventListener('keydown', function(e) { 53 // Permitir: backspace, delete, tab, escape, enter, flechas, home, end 54 if ([8,9,13,27,46,35,36,37,38,39,40].indexOf(e.keyCode) !== -1) return; 55 // Permitir Ctrl+A, Ctrl+C, Ctrl+V, Ctrl+X 56 if ((e.ctrlKey || e.metaKey) && [65,67,86,88].indexOf(e.keyCode) !== -1) return; 57 // Bloquear si no es número 58 if ((e.keyCode < 48 || e.keyCode > 57) && (e.keyCode < 96 || e.keyCode > 105)) { 59 e.preventDefault(); 60 } 61 }); 62 // Validar rango al perder foco 63 inp.addEventListener('blur', function() { 64 cnValidarCampoSex(this); 65 }); 66 }); 67}); 68 69// ============================================================================ 70// VALIDACIÃN SEXAGESIMAL 71// ============================================================================ 72 73function cnValidarCampoSex(el) { 74 var val = parseInt(el.value); 75 if (isNaN(val) || val < 0) { el.value = ''; return; } 76 var min = parseInt(el.min) || 0; 77 var max = parseInt(el.max) || 999; 78 if (val < min) el.value = min; 79 if (val > max) el.value = max; 80} 81 82function cnSexADecimal(grados, minutos, segundos) { 83 var g = parseInt(grados) || 0; 84 var m = parseInt(minutos) || 0; 85 var s = parseInt(segundos) || 0; 86 return g + (m / 60) + (s / 3600); 87} 88 89function cnDecimalASex(decimal) { 90 var abs = Math.abs(decimal); 91 var g = Math.floor(abs); 92 var resto = (abs - g) * 60; 93 var m = Math.floor(resto); 94 var s = Math.round((resto - m) * 60); 95 if (s === 60) { m++; s = 0; } 96 if (m === 60) { g++; m = 0; } 97 return { grados: g, minutos: m, segundos: s }; 98} 99 100function cnSexagesimalEditado() { 101 var latG = document.getElementById('cn-lat-grados').value; 102 var latM = document.getElementById('cn-lat-min').value; 103 var latS = document.getElementById('cn-lat-seg').value; 104 var latH = document.getElementById('cn-lat-hem').value; 105 106 var lonG = document.getElementById('cn-lon-grados').value; 107 var lonM = document.getElementById('cn-lon-min').value; 108 var lonS = document.getElementById('cn-lon-seg').value; 109 var lonH = document.getElementById('cn-lon-hem').value; 110 111 if (latG !== '' && lonG !== '') { 112 var latDec = cnSexADecimal(latG, latM, latS); 113 if (latH === 'S') latDec = -latDec; 114 115 var lonDec = cnSexADecimal(lonG, lonM, lonS); 116 if (lonH === 'W') lonDec = -lonDec; 117 118 document.getElementById('cn-lat').value = latDec.toFixed(4); 119 document.getElementById('cn-lon').value = lonDec.toFixed(4); 120 121 // Mostrar resultado 122 var res = document.getElementById('cn-coords-resultado'); 123 res.textContent = 'â Lat: ' + Math.abs(latDec).toFixed(4) + '° ' + latH + 124 ' | Lon: ' + Math.abs(lonDec).toFixed(4) + '° ' + lonH + 125 ' (decimal: ' + latDec.toFixed(4) + ', ' + lonDec.toFixed(4) + ')'; 126 res.style.display = 'block'; 127 } 128} 129 130function cnAplicarHuso() { 131 var signo = document.getElementById('cn-tz-signo').value; 132 var horas = parseInt(document.getElementById('cn-tz-horas').value) || 0; 133 var minutos = parseInt(document.getElementById('cn-tz-minutos').value) || 0; 134 135 var offset = horas + (minutos / 60); 136 if (signo === '-') offset = -offset; 137 138 document.getElementById('cn-tz-offset').value = offset; 139 140 // Actualizar display 141 var res = document.getElementById('cn-coords-resultado'); 142 var textoActual = res.textContent || ''; 143 var husoTexto = 'Huso: UTC' + (offset >= 0 ? '+' : '') + cnFormatHuso(offset); 144 145 if (textoActual.indexOf('Lat:') >= 0) { 146 // Ya hay coordenadas, agregar huso 147 if (textoActual.indexOf('Huso:') >= 0) { 148 textoActual = textoActual.replace(/\| Huso:.*$/, '| ' + hu
148soTexto); 149 } else { 150 textoActual += ' | ' + husoTexto; 151 } 152 res.textContent = textoActual; 153 } else { 154 res.textContent = husoTexto; 155 } 156 res.style.display = 'block'; 157 158 // Actualizar info box 159 document.getElementById('cn-tz-texto').textContent = '| UTC' + (offset >= 0 ? '+' : '') + cnFormatHuso(offset); 160 document.getElementById('cn-ubicacion-info').style.display = 'block'; 161} 162 163function cnFormatHuso(h) { 164 var abs = Math.abs(h); 165 var hh = Math.floor(abs); 166 var mm = Math.round((abs - hh) * 60); 167 if (mm === 0) return String(h >= 0 ? hh : -hh); 168 return (h < 0 ? '-' : '') + hh + ':' + (mm < 10 ? '0' : '') + mm; 169} 170 171// Llenar campos sexagesimales desde decimal (cuando se selecciona ciudad) 172function cnLlenarSexDesdeDecimal(latDec, lonDec) { 173 var lat = cnDecimalASex(latDec); 174 var lon = cnDecimalASex(lonDec); 175 176 document.getElementById('cn-lat-grados').value = lat.grados; 177 document.getElementById('cn-lat-min').value = lat.minutos; 178 document.getElementById('cn-lat-seg').value = lat.segundos; 179 document.getElementById('cn-lat-hem').value = latDec < 0 ? 'S' : 'N'; 180 181 document.getElementById('cn-lon-grados').value = lon.grados; 182 document.getElementById('cn-lon-min').value = lon.minutos; 183 document.getElementById('cn-lon-seg').value = lon.segundos; 184 document.getElementById('cn-lon-hem').value = lonDec < 0 ? 'W' : 'E'; 185} 186 187// Llenar huso desde decimal 188function cnLlenarHusoDesdeDecimal(offset) { 189 document.getElementById('cn-tz-signo').value = offset < 0 ? '-' : '+'; 190 var abs = Math.abs(offset); 191 document.getElementById('cn-tz-horas').value = Math.floor(abs); 192 document.getElementById('cn-tz-minutos').value = Math.round((abs - Math.floor(abs)) * 60); 193} 194 195 196// ============================================================================ 197// TOGGLE COORDENADAS 198// ============================================================================ 199 200function cnToggleCoords() { 201 var div = document.getElementById('cn-coords-manual'); 202 var btn = document.getElementById('cn-btn-toggle-coords'); 203 if (div.style.display === 'none' || div.style.display === '') { 204 div.style.display = 'block'; 205 btn.classList.add('active'); 206 btn.innerHTML = 'âï¸ Ocultar Coordenadas y Huso Horario'; 207 } else { 208 div.style.display = 'none'; 209 btn.classList.remove('active'); 210 btn.innerHTML = 'âï¸ Ingresar / Corregir Coordenadas y Huso Horario'; 211 } 212} 213 214 215// ============================================================================ 216// BÃSQUEDA DE CIUDADES 217// ============================================================================ 218 219async function cnBuscarCiudad(query) { 220 if (cnBuscando) return; 221 cnBuscando = true; 222 var resultadosDiv = document.getElementById('cn-resultados'); 223 resultadosDiv.innerHTML = '<div style="padding:0.8rem;text-align:center;color:#888;">ð Buscando...</div>'; 224 resultadosDiv.style.display = 'block'; 225 try { 226 var url = 'https://nominatim.openstreetmap.org/search?q=' + encodeURIComponent(query) + 227 '&format=json&limit=8&accept-language=es&featuretype=city&addressdetails=1'; 228 var response = await fetch(url, { headers: { 'User-Agent': 'LunarApp/1.0 (urania.ar)' } }); 229 var data = await response.json(); 230 if (data.length === 0) { 231 resultadosDiv.innerHTML = '<div style="padding:0.8rem;text-align:center;color:#888;">No se encontraron ciudades</div>'; 232 cnBuscando = false; return; 233 } 234 var html = ''; 235 data.forEach(function(lugar) { 236 var nombre = lugar.display_name.split(',').slice(0, 3).join(','); 237 var lat = parseFloat(lugar.lat).toFixed(4); 238 var lon = parseFloat(lugar.lon).toFixed(4); 239 html += '<div class="cn-ciudad-item" onclick="cnSeleccionarCiudad(\'' + 240 nombre.replace(/'/g, "\\'") + '\',' + lugar.lat + ',' + lugar.lon + ')">' + 241 '<div style="color:#d4af37;font-size:0.9rem;">' + nombre + '</div>' +
242 '<div style="color:#777;font-size:0.75rem;">Lat: ' + lat + '° | Lon: ' + lon + '°</div></div>'; 243 }); 244 resultadosDiv.innerHTML = html; 245 } catch(e) { 246 resultadosDiv.innerHTML = '<div style="padding:0.8rem;text-align:center;color:#ff6347;">Error en la búsqueda</div>'; 247 } 248 cnBuscando = false; 249} 250 251async function cnSeleccionarCiudad(nombre, lat, lon) { 252 document.getElementById('cn-ciudad').value = nombre; 253 document.getElementById('cn-resultados').style.display = 'none'; 254 255 var latF = parseFloat(lat); 256 var lonF = parseFloat(lon); 257 258 document.getElementById('cn-lat').value = latF.toFixed(4); 259 document.getElementById('cn-lon').value = lonF.toFixed(4); 260 261 // Llenar campos sexagesimales 262 cnLlenarSexDesdeDecimal(latF, lonF); 263 264 await cnObtenerTimezone(latF, lonF); 265} 266 267async function cnObtenerTimezone(lat, lon) { 268 var fecha = document.getElementById('cn-fecha').value || ''; 269 var hora = document.getElementById('cn-hora').value || '12:00'; 270 var ciudad = document.getElementById('cn-ciudad').value; 271 try { 272 var url = '/api/timezone?lat=' + lat + '&lon=' + lon; 273 if (fecha) url += '&fecha=' + fecha + '&hora=' + hora; 274 var response = await fetch(url); 275 var data = await response.json(); 276 if (data.exito) { 277 document.getElementById('cn-tz-offset').value = data.offset_hours; 278 cnLlenarHusoDesdeDecimal(data.offset_hours); 279 document.getElementById('cn-ubicacion-texto').textContent = ciudad.split(',')[0]; 280 document.getElementById('cn-tz-texto').textContent = '| ' + cnFormatOffset(data.offset_hours) + ' (' + data.timezone_id + ')'; 281 document.getElementById('cn-ubicacion-info').style.display = 'block'; 282 } 283 } catch(e) { 284 var offset = Math.round(lon / 15); 285 document.getElementById('cn-tz-offset').value = offset; 286 cnLlenarHusoDesdeDecimal(offset); 287 document.getElementById('cn-ubicacion-texto').textContent = ciudad.split(',')[0]; 288 document.getElementById('cn-tz-texto').textContent = '| UTC' + (offset >= 0 ? '+' : '') + offset + ' (estimado)'; 289 document.getElementById('cn-ubicacion-info').style.display = 'block'; 290 } 291} 292 293function cnFormatOffset(h) { 294 if (h % 1 === 0) return 'UTC' + (h >= 0 ? '+' : '') + h; 295 var hh = Math.floor(Math.abs(h)); 296 var mm = Math.round((Math.abs(h) - hh) * 60); 297 return 'UTC' + (h >= 0 ? '+' : '-') + hh + ':' + (mm < 10 ? '0' : '') + mm; 298} 299 300 301// ============================================================================ 302// CALCULAR CARTA NATAL 303// ============================================================================ 304 305async function cnCalcularCarta() { 306 var fecha = document.getElementById('cn-fecha').value; 307 var hora = document.getElementById('cn-hora').value || '12:00'; 308 var lat = document.getElementById('cn-lat').value; 309 var lon = document.getElementById('cn-lon').value; 310 var tzOffset = document.getElementById('cn-tz-offset').value; 311 var ciudad = document.getElementById('cn-ciudad').value; 312 var nombre = document.getElementById('cn-nombre') ? document.getElementById('cn-nombre').value : ''; 313 314 if (!fecha) { alert('Ingresá la fecha de nacimiento'); document.getElementById('cn-fecha').focus(); return; } 315 if (!lat || !lon) { 316 alert('Ingresá un lugar de nacimiento.\nPodés buscar una ciudad o cargar las coordenadas manualmente con el botón "Ingresar / Corregir Coordenadas".'); 317 return; 318 } 319 if (tzOffset === '' || tzOffset === undefined || tzOffset === null) { 320 alert('Falta el huso horario.\nSeleccioná una ciudad (se detecta automáticamente) o ingresalo manualmente.'); 321 // Abrir panel de coordenadas si está cerrado 322 var div = document.getElementById('cn-coords-manual'); 323 if (div.style.display === 'none' || div.style.display === '') cnToggleCoords(); 324 return; 325 } 326 327 if (typeof cfUserLat !== 'undefined') { 328 cfUserLat = parseFloat(lat); cfUserLon = parseFloat(lon); 329 cfUserCiudad = ciudad ? ciudad.split(',')[0] : 'Manual'; 330 } 331 332 var fechaDisplay = fecha.split('-').reverse().join('/'); 333 var titulo = (nombre ? nombre + ' â ' : '') + 'Carta Natal â ' + fechaDisplay + ' ' + hora + ' â ' + (ciudad ? ciudad.split(',')[0] : 'Manual'); 334 335 if (typeof cfCurrentData !== 'undefined') { cfCurrentData = { jd: 0, titulo: titulo, esNatal: true }; cfAstrocartoLoaded = false; } 336 337 document.getElementById('cf-carta-titulo').textContent = titulo; 338 document.getElementById('cf-ubicacion-texto').textContent = 339 (ciudad ? ciudad.split(',')[0] : '') + ' (' + parseFloat(lat).toFixed(2) + '°, ' + parseFloat(lon).toFixed(2) + '°) ' + cnFormatOffset(parseFloat(tzOffset)); 340 341 var modal = document.getElementById('cf-modal-carta'); 342 modal.style.display = 'block'; document.body.style.overflow = 'hidden'; 343 344 document.querySelectorAll('.cf-tab-btn').forEach(function(b) { 345 b.style.background = '#1a1a3e'; b.style.color = '#ccc'; b.style.borderColor = '#2a2a4a'; b.style.fontWeight = 'normal'; 346 }); 347 document.querySelectorAll('.cf-tab-content').forEach(function(c) { c.style.display = 'none'; }); 348 var firstTab = document.querySelector('.cf-tab-btn[data-tab="cf-carta"]'); 349 if (firstTab) { firstTab.style.background = 'linear-gradient(135deg,#d4af37,#b8962e)'; firstTab.style.color = '#0f0f1e'; firstTab.style.borderColor = '#d4af37'; firstTab.style.fontWeight = '600'; } 350 document.getElementById('cf-carta').style.display = 'block'; 351 document.getElementById('cf-astrochart-container').innerHTML = '<p style="text-align:center;color:#888;padding:2rem;">â³ Calculando carta natal...</p>'; 352 353 var btn = document.getElementById('cn-btn-calcular'); 354 if (btn) { btn.disabled = true; btn.textContent = 'â³ Calculando...'; } 355 356 try { 357 var url = '/api/carta-natal?fecha=' + fecha + '&hora=' + hora + '&lat=' + lat + '&lon=' + lon + '&tz_offset=' + tzOffset; 358 var r = await fetch(url); 359 var data = await r.json(); 360 if (data.exito) { 361 if (typeof cfCurrentData !== 'undefined') cfCurrentData.jd = data.jd; 362 cnUltimaCartaData = { nombre: nombre, fecha: fecha, hora: hora, ciudad: ciudad, lat: parseFloat(lat), lon: parseFloat(lon), tzOffset: parseFloat(tzOffset), resultado: data }; 363 if (typeof cfDibujarCarta === 'function') cfDibujarCarta(data); 364 if (typeof cfLlenarPosiciones === 'function') cfLlenarPosiciones(data.planetas); 365 if (typeof cfLlenarAspectos === 'function') cfLlenarAspectos(data.aspectos); 366 if (typeof cfLlenarCasas === 'function') cfLlenarCasas(data.casas, data.angulares); 367 if (typeof cfLlenarTecnicos === 'function') cfLlenarTecnicos(data.datos_tecnicos); 368 var acciones = document.getElementById('cn-acciones-post'); 369 if (acciones) acciones.style.display = 'block'; 370 } else { 371 document.getElementById('cf-astrochart-container').innerHTML = '<p style="color:#ff6347;text-align:center;padding:2rem;">â ' + data.error + '</p>'; 372 } 373 } catch(e) { 374 document.getElementById('cf-astrochart-container').innerHTML = '<p style="color:#ff6347;text-align:center;padding:2rem;">â ' + e.message + '</p>'; 375 } 376 if (btn) { btn.disabled = false; btn.textContent = 'ð® Calcular Carta Natal'; } 377} 378 379function cnVerCartaGuardada() {
380 if (!cnUltimaCartaData || !cnUltimaCartaData.resultado) { alert('No hay carta calculada.'); return; } 381 var data = cnUltimaCartaData; 382 if (typeof cfUserLat !== 'undefined') { cfUserLat = data.lat; cfUserLon = data.lon; cfUserCiudad = data.ciudad ? data.ciudad.split(',')[0] : 'Manual'; } 383 var fechaDisplay = data.fecha.split('-').reverse().join('/'); 384 var titulo = (data.nombre ? data.nombre + ' â ' : '') + 'Carta Natal â ' + fechaDisplay + ' ' + data.hora + ' â ' + (data.ciudad ? data.ciudad.split(',')[0] : 'Manual'); 385 document.getElementById('cf-carta-titulo').textContent = titulo; 386 document.getElementById('cf-ubicacion-texto').textContent = (data.ciudad ? data.ciudad.split(',')[0] : '') + ' (' + data.lat.toFixed(2) + '°, ' + data.lon.toFixed(2) + '°) ' + cnFormatOffset(data.tzOffset); 387 var modal = document.getElementById('cf-modal-carta'); modal.style.display = 'block'; document.body.style.overflow = 'hidden'; 388 document.querySelectorAll('.cf-tab-btn').forEach(function(b) { b.style.background = '#1a1a3e'; b.style.color = '#ccc'; b.style.borderColor = '#2a2a4a'; b.style.fontWeight = 'normal'; }); 389 document.querySelectorAll('.cf-tab-content').forEach(function(c) { c.style.display = 'none'; }); 390 var firstTab = document.querySelector('.cf-tab-btn[data-tab="cf-carta"]'); 391 if (firstTab) { firstTab.style.background = 'linear-gradient(135deg,#d4af37,#b8962e)'; firstTab.style.color = '#0f0f1e'; firstTab.style.borderColor = '#d4af37'; firstTab.style.fontWeight = '600'; } 392 document.getElementById('cf-carta').style.display = 'block'; 393 var res = data.resultado; 394 if (typeof cfCurrentData !== 'undefined') { cfCurrentData = { jd: res.jd, titulo: titulo, esNatal: true }; cfAstrocartoLoaded = false; } 395 if (typeof cfDibujarCarta === 'function') cfDibujarCarta(res); 396 if (typeof cfLlenarPosiciones === 'function') cfLlenarPosiciones(res.planetas); 397 if (typeof cfLlenarAspectos === 'function') cfLlenarAspectos(res.aspectos); 398 if (typeof cfLlenarCasas === 'function') cfLlenarCasas(res.casas, res.angulares); 399 if (typeof cfLlenarTecnicos === 'function') cfLlenarTecnicos(res.datos_tecnicos); 400} 401 402// ============================================================================ 403// GUARDAR / CARGAR CARTAS (localStorage) 404// ============================================================================ 405 406function cnGuardarDatos() { 407 if (!cnUltimaCartaData) { alert('Calculá una carta primero.'); return; } 408 var nombre = document.getElementById('cn-nombre') ? document.getElementById('cn-nombre').value.trim() : ''; 409 if (!nombre) { 410 nombre = prompt('Ingresá un nombre para identificar esta carta:'); 411 if (!nombre) return; 412 if (document.getElementById('cn-nombre')) document.getElementById('cn-nombre').value = nombre; 413 } 414 var guardar = { 415 id: Date.now(), nombre: nombre, fecha: cnUltimaCartaData.fecha, hora: cnUltimaCartaData.hora, 416 ciudad: cnUltimaCartaData.ciudad, lat: cnUltimaCartaData.lat, lon: cnUltimaCartaData.lon, 417 tzOffset: cnUltimaCartaData.tzOffset, guardado: new Date().toISOString() 418 }; 419 var cartas = JSON.parse(localStorage.getItem('cn_cartas_guardadas') || '[]'); 420 var existente = cartas.findIndex(function(c) { return c.nombre === nombre; }); 421 if (existente >= 0) { 422 if (!confirm('Ya existe "' + nombre + '". ¿Reemplazar?')) return; 423 cartas[existente] = guardar; 424 } else { cartas.push(guardar); } 425 localStorage.setItem('cn_cartas_guardadas', JSON.stringify(cartas)); 426 alert('â Carta de "' + nombre + '" guardada.'); 427 cnCargarGuardadas(); 428} 429 430function cnCargarGuardadas() { 431 var lista = document.getElementById('cn-guardadas-lista'); 432 if (!lista) return; 433 var cartas = JSON.parse(localStorage.getItem('cn_cartas_guardadas') || '[]'); 434 if (cartas.length === 0) { 435 lista.innerHTML = '<p style="color:#888;font-size:0.9rem;">No hay cartas guardadas todavÃa.</p>'; 436 return; 437 } 438 cartas.sort(function(a, b) { return b.id - a.id; }); 439 var html = ''; 440 cartas.forEach(function(carta) { 441 var fechaDisplay = carta.fecha.split('-').reverse().join('/'); 442 var ciudadCorta = carta.ciudad ? carta.ciudad.split(',')[0] : 'Manual'; 443 html += '<div class="cn-saved-item">' + 444 '<div onclick="cnCargarCarta(' + carta.id + ')" style="flex:1;cursor:pointer;">' + 445 '<div class="cn-saved-name">' + carta.nombre + '</div>' +
446 '<div class="cn-saved-details">' + fechaDisplay + ' ' + carta.hora + ' â ' + ciudadCorta + ' â ' + cnFormatOffset(carta.tzOffset) + '</div>' + 447 '</div>' + 448 '<div class="cn-saved-actions">' + 449 '<button onclick="cnCargarCarta(' + carta.id + ')" title="Calcular">ð</button>' + 450 '<button onclick="cnEliminarCarta(' + carta.id + ')" title="Eliminar">ðï¸</button>' + 451 '</div></div>'; 452 }); 453 html += '<div style="margin-top:0.8rem;text-align:right;">' + 454 '<button class="cn-btn-secondary" onclick="cnExportarCartas()" style="font-size:0.8rem;padding:0.4rem 0.8rem;">ð¤ Exportar</button> ' + 455 '<button class="cn-btn-secondary" onclick="cnImportarCartas()" style="font-size:0.8rem;padding:0.4rem 0.8rem;">ð¥ Importar</button></div>'; 456 lista.innerHTML = html; 457} 458 459function cnCargarCarta(id) { 460 var cartas = JSON.parse(localStorage.getItem('cn_cartas_guardadas') || '[]'); 461 var carta = cartas.find(function(c) { return c.id === id; }); 462 if (!carta) { alert('Carta no encontrada'); return; } 463 if (document.getElementById('cn-nombre')) document.getElementById('cn-nombre').value = carta.nombre; 464 document.getElementById('cn-fecha').value = carta.fecha; 465 document.getElementById('cn-hora').value = carta.hora; 466 document.getElementById('cn-ciudad').value = carta.ciudad || ''; 467 document.getElementById('cn-lat').value = carta.lat; 468 document.getElementById('cn-lon').value = carta.lon; 469 document.getElementById('cn-tz-offset').value = carta.tzOffset; 470 471 // Llenar sexagesimales 472 cnLlenarSexDesdeDecimal(carta.lat, carta.lon); 473 cnLlenarHusoDesdeDecimal(carta.tzOffset); 474 475 var ciudadCorta = carta.ciudad ? carta.ciudad.split(',')[0] : 'Manual'; 476 document.getElementById('cn-ubicacion-texto').textContent = ciudadCorta; 477 document.getElementById('cn-tz-texto').textContent = '| ' + cnFormatOffset(carta.tzOffset); 478 document.getElementById('cn-ubicacion-info').style.display = 'block'; 479 cnCalcularCarta(); 480} 481 482function cnEliminarCarta(id) { 483 if (!confirm('¿Eliminar esta carta guardada?')) return; 484 var cartas = JSON.parse(localStorage.getItem('cn_cartas_guardadas') || '[]'); 485 cartas = cartas.filter(function(c) { return c.id !== id; }); 486 localStorage.setItem('cn_cartas_guardadas', JSON.stringify(cartas)); 487 cnCargarGuardadas(); 488} 489 490// ============================================================================ 491// EXPORTAR / IMPORTAR 492// ============================================================================ 493 494function cnExportarCartas() { 495 var cartas = JSON.parse(localStorage.getItem('cn_cartas_guardadas') || '[]'); 496 if (cartas.length === 0) { alert('No hay cartas para exportar.'); return; } 497 var blob = new Blob([JSON.stringify(cartas, null, 2)], { type: 'application/json' }); 498 var url = URL.createObjectURL(blob); 499 var a = document.createElement('a'); a.href = url; 500 a.download = 'cartas-natales-' + new Date().toISOString().split('T')[0] + '.json'; 501 document.body.appendChild(a); a.click(); document.body.removeChild(a); 502 URL.revokeObjectURL(url); 503} 504 505function cnImportarCartas() { 506 var input = document.createElement('input'); input.type = 'file'; input.accept = '.json'; 507 input.onchange = function(e) { 508 var file = e.target.files[0]; if (!file) return; 509 var reader = new FileReader(); 510 reader.onload = function(ev) { 511 try { 512 var importadas = JSON.parse(ev.target.result); 513 if (!Array.isArray(importadas)) throw new Error('Formato inválido'); 514 var cartas = JSON.parse(localStorage.getItem('cn_cartas_guardadas') || '[]'); 515 var agregadas = 0; 516 importadas.forEach(function(carta) { 517 if (carta.nombre && carta.fecha && carta.lat !== undefined) { 518 var existe = cartas.find(function(c) { return c.nombre === carta.nombre; }); 519 if (!existe) { if (!carta.id) carta.id = Date.now() + agregadas; cartas.push(carta); agregadas++; } 520 } 521 }); 522 localStorage.setItem('cn_cartas_guardadas', JSON.stringify(cartas)); 523 alert('â Se importaron ' + agregadas + ' cartas nuevas.'); 524 cnCargarGuardadas(); 525 } catch(err) { alert('Error al importar: ' + err.message); } 526 }; 527 reader.readAsText(file); 528 }; 529 input.click(); 530} 531 532// ============================================================================ 533// OVERRIDE cfLlenarTecnicos 534// ============================================================================ 535 536cfLlenarTecnicos = function(d) { 537 var c = document.getElementById('cf-datos-tecnicos'); 538 if (!d) { c.innerHTML = ''; return; } 539 var row = function(label, value) { 540 return '<div style="display:flex;justify-content:space-between;padding:0.6rem 0;border-bottom:1px solid rgba(255,255,255,0.06);">' + 541 '<span style="color:#888;">' + label + '</span><span style="color:#d4af37;font-weight:600;">' + value + '</span></div>'; 542 }; 543 var h = ''; 544 if (d.tipo === 'natal') { 545 h += row('Tipo', 'ð® Carta Natal'); 546 if (d.fecha_local) h += row('Fecha y Hora Local', d.fecha_local); 547 h += row('Fecha UTC', d.fecha_utc); 548 h += row('DÃa Juliano', d.jd.toFixed(6)); 549 if (d.tz_offset !== undefined) h += row('Zona Horaria', cnFormatOffset(d.tz_offset)); 550 h += row('Ubicación', d.ubicacion_calculo.lat.toFixed(4) + '°, ' + d.ubicacion_calculo.lon.toFixed(4) + '°'); 551 } else { 552 h += row('Fecha UTC', d.fecha_utc); 553 h += row('DÃa Juliano', d.jd.toFixed(4)); 554 h += row('Ubicación', d.ubicacion_calculo.lat.toFixed(4) + '°, ' + d.ubicacion_calculo.lon.toFixed(4) + '°'); 555 } 556 c.innerHTML = h; 557}; 558 559// ============================================================================ 560// IMPRIMIR - Layout profesional A4 una sola página 561// ============================================================================ 562 563// Mapeos de sÃmbolos astrológicos 564var CN_PLANET_SYMBOLS = { 565 'Sol':'â','Luna':'â½','Mercurio':'â¿','Venus':'â','Marte':'â', 566 'Jupiter':'â','Júpiter':'â','Saturno':'â','Urano':'â ','Neptuno':'â', 567 'Pluton':'â','Plutón':'â','Nodo Norte':'â','Nodo Sur':'â' 568}; 569var CN_SIGN_SYMBOLS = { 570 'Aries':'â','Tauro':'â','Geminis':'â','Géminis':'â', 571 'Cancer':'â','Cáncer':'â','Leo':'â','Virgo':'â', 572 'Libra':'â','Escorpio':'â','Escorpión':'â', 573 'Sagitario':'â','Capricornio':'â','Acuario':'â','Piscis':'â' 574}; 575 576 577// ============================================================================
578// IMPRESION Y PDF - Layout A4 una sola pagina 579// ============================================================================ 580 581function cnGetBrightness(color) { 582 if (!color) return 999; 583 color = color.toLowerCase().trim(); 584 if (color === 'none' || color === 'transparent' || color === '') return 999; 585 if (color.indexOf('#') === 0) { 586 var hex = color.replace('#',''); 587 if (hex.length === 3) hex = hex[0]+hex[0]+hex[1]+hex[1]+hex[2]+hex[2]; 588 var r = parseInt(hex.substr(0,2),16), g = parseInt(hex.substr(2,2),16), b = parseInt(hex.substr(4,2),16); 589 return (r*299+g*587+b*114)/1000; 590 } 591 if (color.indexOf('rgb') === 0) { 592 var p = color.match(/\d+/g); 593 if (p && p.length >= 3) return (parseInt(p[0])*299+parseInt(p[1])*587+parseInt(p[2])*114)/1000; 594 } 595 return 999; 596} 597 598function cnIsSaturated(color) { 599 if (!color || color.indexOf('#') !== 0) return false; 600 var hex = color.replace('#','').toLowerCase(); 601 if (hex.length === 3) hex = hex[0]+hex[0]+hex[1]+hex[1]+hex[2]+hex[2]; 602 var r = parseInt(hex.substr(0,2),16), g = parseInt(hex.substr(2,2),16), b = parseInt(hex.substr(4,2),16); 603 return (Math.max(r,g,b) - Math.min(r,g,b)) > 50; 604} 605 606function cnModificarSvgParaImpresion(svg, maxPx) { 607 // Asegurar viewBox para escalado proporcional (mismo que carta-fase.js) 608 if (!svg.getAttribute('viewBox')) { 609 // Detectar tamaño original del SVG 610 var origW = parseFloat(svg.getAttribute('width')) || 600; 611 var origH = parseFloat(svg.getAttribute('height')) || 600; 612 svg.setAttribute('viewBox', '0 0 ' + origW + ' ' + origH); 613 } 614 svg.removeAttribute('width'); 615 svg.removeAttribute('height'); 616 var mp = maxPx || 540; 617 svg.style.width = '100%'; 618 svg.style.maxWidth = mp + 'px'; 619 svg.style.height = 'auto'; 620 svg.style.display = 'block'; 621 svg.style.margin = '0 auto'; 622 623 var zodiacSyms = '\u2648\u2649\u264A\u264B\u264C\u264D\u264E\u264F\u2650\u2651\u2652\u2653'; 624 var planetSyms = '\u2609\u263D\u263F\u2640\u2642\u2643\u2644\u2645\u2646\u2647\u260A\u260B'; 625 626 // Circulos y rects: oscuros no-saturados â blanco (fondo, anillo planetas) 627 svg.querySelectorAll('circle, path, rect').forEach(function(el) { 628 var fill = (el.getAttribute('fill') || '').trim(); 629 if (!fill || fill === 'none' || fill === 'transparent') return; 630 if (cnGetBrightness(fill) < 130 && !cnIsSaturated(fill)) { 631 el.setAttribute('fill', '#ffffff'); 632 if (el.tagName.toLowerCase() === 'circle') { 633 el.setAttribute('stroke', '#bbbbbb'); 634 el.setAttribute('stroke-width', '0.5'); 635 } 636 } 637 }); 638 639 // Textos 640 svg.querySelectorAll('text').forEach(function(t) { 641 var c = t.textContent.trim(); 642 if (zodiacSyms.indexOf(c) >= 0) { 643 t.setAttribute('fill', '#ffffff'); 644 t.setAttribute('font-weight', 'bold'); 645 t.setAttribute('stroke', '#ffffff'); 646 t.setAttribute('stroke-width', '0.3'); 647 } else if (planetSyms.indexOf(c) >= 0) { 648 t.setAttribute('fill', '#000000'); 649 t.setAttribute('font-weight', 'bold'); 650 t.setAttribute('stroke', '#000000'); 651 t.setAttribute('stroke-width', '0.3'); 652 } else { 653 t.setAttribute('fill', '#222222'); 654 t.setAttribute('font-weight', 'bold'); 655 } 656 }); 657 658 // Lineas: asegurar visibilidad 659 svg.querySelectorAll('line').forEach(function(l) { 660 var sw = parseFloat(l.getAttribute('stroke-width') || 1); 661 if (sw < 0.8) l.setAttribute('stroke-width', '0.8'); 662 var sc = (l.getAttribute('stroke') || '').toLowerCase(); 663 if (sc === '#fff' || sc === '#ffffff' || sc === 'white') l.setAttribute('stroke', '#cccccc'); 664 }); 665 return svg; 666} 667 668function cnBuildPrintPlanetas(planetas) { 669 var h = '<table style="width:100%;border-collapse:collapse;font-size:8.5pt;">'; 670 h += '<tr><th style="text-align:left;padding:1px 2px;font-size:7pt;color:#555;border-bottom:1px solid #333;" colspan="2">PLANETAS</th>'; 671 h += '<th style="text-align:right;padding:1px 2px;font-size:7pt;color:#555;border-bottom:1px solid #333;">POSICI\u00D3N</th></tr>'; 672 var orden = ['Sol','Luna','Mercurio','Venus','Marte','Jupiter','J\u00FApiter','Saturno','Urano','Neptuno','Pluton','Plut\u00F3n','Nodo Norte','Nodo Sur'];
673 orden.forEach(function(nombre) { 674 var p = planetas[nombre]; 675 if (!p) return; 676 var sym = CN_PLANET_SYMBOLS[nombre] || ''; 677 var sigSym = CN_SIGN_SYMBOLS[p.signo] || p.signo; 678 var retro = p.retrogrado ? ' \u211E' : ''; 679 var gradoStr = p.grado || ''; 680 var pos = gradoStr.split("'")[0] + "'"; 681 pos = pos.replace(/\u00B0/, '\u00B0' + sigSym); 682 h += '<tr><td style="padding:0 2px;font-size:10pt;width:16px;line-height:1.2;">' + sym + '</td>'; 683 h += '<td style="padding:0 2px;font-size:8.5pt;line-height:1.2;">' + nombre + retro + '</td>'; 684 h += '<td style="padding:0 2px;text-align:right;font-family:monospace;font-size:8.5pt;line-height:1.2;">' + pos + '</td></tr>'; 685 }); 686 h += '</table>'; 687 return h; 688} 689 690function cnBuildPrintCasas(casas, angulares) { 691 var items = []; 692 if (angulares && angulares['ASC']) items.push({l:'ASC', s:angulares['ASC'].signo, g:angulares['ASC'].grado}); 693 else if (casas['Casa 1']) items.push({l:'ASC', s:casas['Casa 1'].signo, g:casas['Casa 1'].grado}); 694 if (casas['Casa 2']) items.push({l:'II', s:casas['Casa 2'].signo, g:casas['Casa 2'].grado}); 695 if (casas['Casa 3']) items.push({l:'III', s:casas['Casa 3'].signo, g:casas['Casa 3'].grado}); 696 if (angulares && angulares['MC']) items.push({l:'MC', s:angulares['MC'].signo, g:angulares['MC'].grado}); 697 else if (casas['Casa 10']) items.push({l:'MC', s:casas['Casa 10'].signo, g:casas['Casa 10'].grado}); 698 if (casas['Casa 11']) items.push({l:'XI', s:casas['Casa 11'].signo, g:casas['Casa 11'].grado}); 699 if (casas['Casa 12']) items.push({l:'XII', s:casas['Casa 12'].signo, g:casas['Casa 12'].grado}); 700 701 var h = '<table style="width:100%;border-collapse:collapse;font-size:8.5pt;">'; 702 h += '<tr><th style="text-align:left;padding:1px 2px;font-size:7pt;color:#555;border-bottom:1px solid #333;">CASA</th>'; 703 h += '<th style="text-align:right;padding:1px 2px;font-size:7pt;color:#555;border-bottom:1px solid #333;">C\u00DASPIDE</th></tr>'; 704 items.forEach(function(c) { 705 var sigSym = CN_SIGN_SYMBOLS[c.s] || c.s; 706 var pos = (c.g || '').split("'")[0] + "'"; 707 pos = pos.replace(/\u00B0/, '\u00B0' + sigSym); 708 var bold = (c.l === 'ASC' || c.l === 'MC') ? 'font-weight:bold;' : ''; 709 h += '<tr><td style="padding:0 2px;line-height:1.2;' + bold + '">' + c.l + '</td>'; 710 h += '<td style="padding:0 2px;text-align:right;font-family:monospace;line-height:1.2;' + bold + '">' + pos + '</td></tr>'; 711 }); 712 h += '</table>'; 713 return h; 714} 715 716function cnBuildPrintHeader(data) { 717 var fd = data.fecha.split('-').reverse().join('/'); 718 var city = data.ciudad ? data.ciudad.split(',')[0].trim() : 'Manual'; 719 var latS = cnDecimalASex(data.lat), lonS = cnDecimalASex(data.lon); 720 var latStr = latS.grados + '\u00B0' + (latS.minutos<10?'0':'') + latS.minutos + "'" + (data.lat<0?'S':'N'); 721 var lonStr = lonS.grados + '\u00B0' + (lonS.minutos<10?'0':'') + lonS.minutos + "'" + (data.lon<0?'W':'E'); 722 return '<div style="text-align:center;margin-bottom:2px;padding-bottom:2px;border-bottom:2px solid #333;">' + 723 '<div style="font-size:16pt;font-weight:bold;letter-spacing:2px;">CARTA NATAL</div>' + 724 (data.nombre ? '<div style="font-size:13pt;font-weight:bold;color:#222;margin:1px 0;">' + data.nombre + '</div>' : '') + 725 '<div style="font-size:8.5pt;color:#444;">' + fd + ' \u2014 ' + data.hora + ' hs \u2014 ' + city + 726 ' \u2014 ' + latStr + ' ' + lonStr + ' \u2014 ' + cnFormatOffset(data.tzOffset) + '</div></div>'; 727} 728 729function cnBuildPrintFooter() { 730 return '<div style="text-align:center;border-top:1px solid #333;padding-top:2px;margin-top:3px;font-size:7pt;color:#555;">' + 731 '<div style="font-size:9pt;font-weight:bold;letter-spacing:1px;">La Naci\u00F3n de Urania</div>' + 732 '<div>Organizaci\u00F3n Astrol\u00F3gica \u2014 Nestor Echarte</div>' + 733 '<div>[email protected] \u2014 WhatsApp: +54 9 294 431 2096 \u2014 naciondeurania.com</div></div>'; 734} 735 736function cnBuildFullLayout(data, svgClone, maxSvgPx) { 737 var wrap = document.createElement('div'); 738 739 var hdr = document.createElement('div'); 740 hdr.innerHTML = cnBuildPrintHeader(data); 741 wrap.appendChild(hdr.firstChild); 742 743 var chartWrap = document.createElement('div'); 744 chartWrap.style.cssText = 'text-align:center;margin:2px auto;max-width:' + maxSvgPx + 'px;'; 745 chartWrap.appendChild(svgClone); 746 wrap.appendChild(chartWrap); 747 748 var cols = document.createElement('div'); 749 cols.style.cssText = 'display:flex;gap:12px;margin-top:1px;'; 750 var colP = document.createElement('div'); 751 colP.style.cssText = 'flex:1;'; 752 colP.innerHTML = cnBuildPrintPlanetas(data.resultado.planetas); 753 var colC = document.createElement('div'); 754 colC.style.cssText = 'flex:0 0 140px;'; 755 colC.innerHTML = cnBuildPrintCasas(data.resultado.casas, data.resultado.angulares); 756 cols.appendChild(colP); 757 cols.appendChild(colC); 758 wrap.appendChild(cols); 759 760 var foot = document.createElement('div'); 761 foot.innerHTML = cnBuildPrintFooter(); 762 wrap.appendChild(foot.firstChild); 763 764 return wrap; 765} 766 767// ============================================================================ 768// IMPRIMIR 769// ============================================================================ 770 771function cfImprimirCarta() {
772 if (!cnUltimaCartaData || !cnUltimaCartaData.resultado) { 773 alert('No hay carta calculada para imprimir.'); return; 774 } 775 776 // En movil, generar PDF directamente (funciona perfecto y el usuario lo imprime desde el visor) 777 var esMobile = /Android|iPhone|iPad|iPod|Mobile/i.test(navigator.userAgent); 778 if (esMobile) { 779 cfGuardarPDF(); 780 return; 781 } 782 783 // PC: iframe oculto 784 var svgOrig = document.querySelector('#cf-astrochart-container svg'); 785 if (!svgOrig) { alert('No se encontro el grafico.'); return; } 786 var svgClone = svgOrig.cloneNode(true); 787 cnModificarSvgParaImpresion(svgClone, 540); 788 789 var layout = cnBuildFullLayout(cnUltimaCartaData, svgClone, 540); 790 var tmpDiv = document.createElement('div'); 791 tmpDiv.appendChild(layout); 792 793 var fullHTML = '<!DOCTYPE html><html><head><meta charset="UTF-8">' + 794 '<title>Carta Natal</title>' + 795 '<style>' + 796 ' @page { size: A4 portrait; margin: 8mm 10mm; }' + 797 ' * { box-sizing: border-box; margin: 0; padding: 0; }' + 798 ' body { padding: 10px 20px; background: white;' + 799 ' font-family: "Segoe UI", Arial, sans-serif; color: #222; }' + 800 ' svg { max-width: 540px; width: 100%; height: auto; display: block; margin: 0 auto; }' + 801 '</style></head><body>' + 802 '<div style="max-width:660px;margin:0 auto;">' + tmpDiv.innerHTML + '</div>' + 803 '</body></html>'; 804
805 var oldFrame = document.getElementById('cn-print-iframe'); 806 if (oldFrame) oldFrame.remove(); 807 808 var iframe = document.createElement('iframe'); 809 iframe.id = 'cn-print-iframe'; 810 iframe.style.cssText = 'position:fixed;top:-9999px;left:-9999px;width:800px;height:1100px;border:none;'; 811 document.body.appendChild(iframe); 812 813 var iframeDoc = iframe.contentDocument || iframe.contentWindow.document; 814 iframeDoc.open(); 815 iframeDoc.write(fullHTML); 816 iframeDoc.close(); 817 818 iframe.onload = function() { 819 setTimeout(function() { 820 try { 821 iframe.contentWindow.print(); 822 } catch(e) { 823 window.print(); 824 } 825 setTimeout(function() { 826 var f = document.getElementById('cn-print-iframe'); 827 if (f) f.remove(); 828 }, 2000); 829 }, 400); 830 }; 831} 832 833// ============================================================================ 834// GUARDAR PDF 835// ============================================================================ 836 837var cfPdfLibsLoaded = false; 838function cfCargarLibsPDF() { 839 return new Promise(function(resolve) { 840 if (cfPdfLibsLoaded) { resolve(); return; } 841 var s1 = document.createElement('script'); 842 s1.src = 'https://cdnjs.cloudflare.com/ajax/libs/html2canvas/1.4.1/html2canvas.min.js'; 843 s1.onload = function() { 844 var s2 = document.createElement('script'); 845 s2.src = 'https://cdnjs.cloudflare.com/ajax/libs/jspdf/2.5.1/jspdf.umd.min.js'; 846 s2.onload = function() { cfPdfLibsLoaded = true; resolve(); }; 847 document.head.appendChild(s2); 848 }; 849 document.head.appendChild(s1); 850 }); 851} 852 853async function cfGuardarPDF() { 854 if (!cnUltimaCartaData || !cnUltimaCartaData.resultado) { 855 alert('No hay carta calculada para exportar.'); return; 856 } 857 var btn = event.target.closest('button'); 858 var orig = btn.innerHTML; btn.innerHTML = '\u23F3'; btn.disabled = true; 859 try { 860 await cfCargarLibsPDF(); 861 var data = cnUltimaCartaData; 862 var svgOrig = document.querySelector('#cf-astrochart-container svg'); 863 if (!svgOrig) { alert('No se encontro el grafico.'); btn.innerHTML = orig; btn.disabled = false; return; } 864 var svgClone = svgOrig.cloneNode(true); 865 866 // 1) Asegurar viewBox con las dimensiones originales del chart (600x600) 867 var vb = svgClone.getAttribute('viewBox'); 868 if (!vb) { 869 var origW = parseFloat(svgClone.getAttribute('width')) || 600; 870 var origH = parseFloat(svgClone.getAttribute('height')) || 600; 871 svgClone.setAttribute('viewBox', '0 0 ' + origW + ' ' + origH); 872 } 873 // Fijar tamaño de renderizado 874 svgClone.setAttribute('width', '860'); 875 svgClone.setAttribute('height', '860'); 876 svgClone.style.cssText = ''; 877 878 // 2) Modificar colores: fondo blanco, planetas oscuros 879 var zodiacSyms = '\u2648\u2649\u264A\u264B\u264C\u264D\u264E\u264F\u2650\u2651\u2652\u2653'; 880 var planetSyms = '\u2609\u263D\u263F\u2640\u2642\u2643\u2644\u2645\u2646\u2647\u260A\u260B'; 881 882 svgClone.querySelectorAll('circle, path, rect').forEach(function(el) { 883 var fill = (el.getAttribute('fill') || '').trim(); 884 if (!fill || fill === 'none' || fill === 'transparent') return; 885 if (cnGetBrightness(fill) < 130 && !cnIsSaturated(fill)) { 886 el.setAttribute('fill', '#ffffff'); 887 if (el.tagName.toLowerCase() === 'circle') { 888 el.setAttribute('stroke', '#bbbbbb'); 889 el.setAttribute('stroke-width', '0.5'); 890 } 891 } 892 }); 893 svgClone.querySelectorAll('text').forEach(function(t) { 894 var c = t.textContent.trim(); 895 if (zodiacSyms.indexOf(c) >= 0) { 896 t.setAttribute('fill', '#ffffff'); 897 t.setAttribute('font-weight', 'bold'); 898 t.setAttribute('stroke', '#ffffff'); 899 t.setAttribute('stroke-width', '0.3'); 900 } else if (planetSyms.indexOf(c) >= 0) { 901 t.setAttribute('fill', '#000000'); 902 t.setAttribute('font-weight', 'bold'); 903 t.setAttribute('stroke', '#000000'); 904 t.setAttribute('stroke-width', '0.3'); 905 } else { 906 t.setAttribute('fill', '#222222'); 907 t.setAttribute('font-weight', 'bold'); 908 } 909 }); 910 svgClone.querySelectorAll('line').forEach(function(l) { 911 var sw = parseFloat(l.getAttribute('stroke-width') || 1); 912 if (sw < 0.8) l.setAttribute('stroke-width', '0.8'); 913 var sc = (l.getAttribute('stroke') || '').toLowerCase(); 914 if (sc === '#fff' || sc === '#ffffff' || sc === 'white') l.setAttribute('stroke', '#cccccc'); 915 }); 916 917 // 3) Rasterizar SVG â Canvas â PNG (html2canvas no escala viewBox bien) 918 var svgData = new XMLSerializer().serializeToString(svgClone); 919 var svgBlob = new Blob([svgData], {type: 'image/svg+xml;charset=utf-8'}); 920 var svgUrl = URL.createObjectURL(svgBlob); 921 922 var chartImg = await new Promise(function(resolve, reject) { 923 var img = new Image(); 924 img.onload = function() { resolve(img); }; 925 img.onerror = function() { reject(new Error('Error rasterizando SVG')); }; 926 img.src = svgUrl; 927 }); 928 URL.revokeObjectURL(svgUrl); 929 930 // Dibujar en canvas a alta resolución 931 var rasterCanvas = document.createElement('canvas'); 932 rasterCanvas.width = 860; 933 rasterCanvas.height = 860; 934 var ctx = rasterCanvas.getContext('2d'); 935 ctx.fillStyle = '#ffffff'; 936 ctx.fillRect(0, 0, 860, 860); 937 ctx.drawImage(chartImg, 0, 0, 860, 860); 938 var chartPngUrl = rasterCanvas.toDataURL('image/png'); 939 940 // 4) Crear <img> con el PNG (esto html2canvas lo maneja perfecto) 941 var imgEl = document.createElement('img'); 942 imgEl.src = chartPngUrl; 943 imgEl.style.cssText = 'width:430px;height:430px;
943display:block;margin:0 auto;'; 944 945 // 5) Construir layout A4 946 var pdfDiv = document.createElement('div'); 947 pdfDiv.id = 'cn-pdf-layout'; 948 pdfDiv.style.cssText = 'position:fixed;top:-9999px;left:0;width:595px;height:842px;padding:20px 28px;background:white;font-family:"Segoe UI",Arial,sans-serif;color:#222;z-index:-1;box-sizing:border-box;overflow:hidden;'; 949 950 var layout = cnBuildFullLayout(data, imgEl, 430); 951 layout.style.cssText = 'max-width:539px;margin:0 auto;'; 952 pdfDiv.appendChild(layout); 953 document.body.appendChild(pdfDiv); 954 955 await new Promise(function(r) { setTimeout(r, 400); }); 956 957 var canvas = await html2canvas(pdfDiv, { 958 backgroundColor: '#ffffff', 959 scale: 2, 960 useCORS: true, 961 logging: false, 962 width: 595, 963 height: 842, 964 windowWidth: 595 965 }); 966 document.body.removeChild(pdfDiv); 967 968 var jsPDF = window.jspdf.jsPDF; 969 var pdf = new jsPDF('p', 'mm', 'a4'); 970 var imgData = canvas.toDataURL('image/jpeg', 0.95); 971 pdf.addImage(imgData, 'JPEG', 0, 0, 210, 297); 972 973 var titulo = data.nombre || 'carta-natal'; 974 pdf.save(titulo.replace(/[^a-zA-Z0-9\u00E1\u00E9\u00ED\u00F3\u00FA\u00F1\u00D1\s-]/g, '').replace(/\s+/g, '_').substring(0,50) + '.pdf'); 975 } catch(e) { alert('Error generando PDF: ' + e.message); } 976 btn.innerHTML = orig; btn.disabled = false; 977}
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.