Ponemos escala y calculamos millas

This commit is contained in:
luisgulo committed 2026-11-01 12:10:28 +01:00
1 parent e6ce241462
commit 049f86727b
2 files changed
+209 -54

No files matched your search

+55 -13
View File
@@ -147,9 +147,11 @@
<i class="fa-solid fa-crosshairs mr-1.5 animate-pulse"></i> <span id="status-text">Haz clic para añadir puntos en la carta (A -> B -> C...)</span>
</div>
<!--
<div class="absolute bottom-3 right-3 bg-slate-950/80 border border-slate-800 text-[9.5px] text-slate-400 px-2.5 py-1 rounded-md backdrop-blur-sm pointer-events-none z-10">
Usa el zoom o las barras de desplazamiento para explorar la carta
</div>
-->
</div>
</main>
@@ -559,7 +561,6 @@
}
function updateReadouts() {
// Cálculo preciso unificado del Rumbo Verdadero (Rv) considerando la diferencia entre regla y brújula
let baseReferenceAngle = (360 - state.rulerAngle) % 360;
let rv = (state.rulerAngle + 90 - (state.diskAngle - baseReferenceAngle)) % 360;
if (rv < 0) rv += 360;
@@ -609,10 +610,11 @@
draw();
}
canvas.addEventListener('mousedown', (e) => {
// --- GESTIÓN DE RATÓN Y TÁCTIL UNIFICADA ---
function handleInteractionStart(clientX, clientY) {
const rect = canvas.getBoundingClientRect();
const clickX = e.clientX - rect.left;
const clickY = e.clientY - rect.top;
const clickX = clientX - rect.left;
const clickY = clientY - rect.top;
if (state.mode === 'ADD_POINTS') {
const nextChar = String.fromCharCode(65 + state.points.length);
@@ -629,7 +631,6 @@
if (angleDeg < 0) angleDeg += 360;
state.rulerAngle = Math.round(angleDeg);
// Configuración automática de la rosa según la orientación de la regla
state.diskAngle = (360 - state.rulerAngle) % 360;
document.getElementById('ruler-angle-slider').value = state.rulerAngle;
@@ -640,34 +641,75 @@
}
draw();
return;
return true;
}
const dist = Math.hypot(clickX - state.rulerX, clickY - state.rulerY);
if (dist < 250) {
// Radio de toque amplio para asegurar que coja la regla cómodamente en tablets (aprox 300px)
if (dist < 300) {
state.isDragging = true;
state.dragStartX = clickX - state.rulerX;
state.dragStartY = clickY - state.rulerY;
canvas.classList.remove('cursor-grab-custom');
canvas.classList.add('cursor-grabbing');
return true; // Capturado por la regla
}
});
return false; // Tocó fuera de la regla, permitir scroll del mapa
}
canvas.addEventListener('mousemove', (e) => {
function handleInteractionMove(clientX, clientY) {
if (state.isDragging && state.mode === 'MOVE') {
const rect = canvas.getBoundingClientRect();
state.rulerX = (e.clientX - rect.left) - state.dragStartX;
state.rulerY = (e.clientY - rect.top) - state.dragStartY;
state.rulerX = (clientX - rect.left) - state.dragStartX;
state.rulerY = (clientY - rect.top) - state.dragStartY;
draw();
return true;
}
});
return false;
}
window.addEventListener('mouseup', () => {
function handleInteractionEnd() {
if (state.isDragging) {
state.isDragging = false;
canvas.classList.remove('cursor-grabbing');
canvas.classList.add('cursor-grab-custom');
}
}
// Eventos de Ratón
canvas.addEventListener('mousedown', (e) => {
handleInteractionStart(e.clientX, e.clientY);
});
canvas.addEventListener('mousemove', (e) => {
handleInteractionMove(e.clientX, e.clientY);
});
window.addEventListener('mouseup', () => {
handleInteractionEnd();
});
// Eventos Táctiles (Tablet / Móvil)
canvas.addEventListener('touchstart', (e) => {
if (e.touches.length === 1) {
const touch = e.touches[0];
const captured = handleInteractionStart(touch.clientX, touch.clientY);
if (captured || state.mode === 'ADD_POINTS') {
e.preventDefault(); // Evita scroll solo si estás moviendo la regla o añadiendo puntos
}
}
}, { passive: false });
canvas.addEventListener('touchmove', (e) => {
if (state.isDragging && e.touches.length === 1) {
const touch = e.touches[0];
handleInteractionMove(touch.clientX, touch.clientY);
e.preventDefault(); // Bloquea el scroll de la página mientras arrastras la regla
}
}, { passive: false });
canvas.addEventListener('touchend', () => {
handleInteractionEnd();
});
document.getElementById('disk-angle-slider').addEventListener('input', (e) => {
+154 -41
View File
@@ -3,7 +3,7 @@
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Regla Portland Plotter - Visor Pro Avanzado</title>
<title>Regla Portland Plotter - Visor Pro Avanzado con Escala</title>
<script src="https://cdn.tailwindcss.com"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<style>
@@ -32,7 +32,7 @@
<div>
<h1 class="text-sm font-bold text-white tracking-wide flex items-center gap-2">
Regla Portland Plotter
<span class="text-[9px] bg-amber-500/20 text-amber-300 border border-amber-500/30 px-2 py-0.5 rounded-full font-mono">Modo Rutas Múltiples & Guía</span>
<span class="text-[9px] bg-amber-500/20 text-amber-300 border border-amber-500/30 px-2 py-0.5 rounded-full font-mono">Modo Distancias & Escala</span>
</h1>
<p class="text-[10px] text-slate-400">Simulador Profesional de Alta Fidelidad</p>
</div>
@@ -48,6 +48,11 @@
<div><span class="text-slate-500">Rm/Ra:</span> <span id="readout-rm" class="text-emerald-400 font-bold">090°</span></div>
</div>
<!-- Botón Marcar Escala -->
<button id="btn-mode-scale" onclick="toggleScaleMode()" class="bg-cyan-600 hover:bg-cyan-500 text-white font-bold px-3 py-1 rounded-lg text-xs transition-all flex items-center gap-1.5 shadow-md" title="Calibrar escala sobre las latitudes (5 millas)">
<i class="fa-solid fa-ruler-combined"></i> <span id="btn-scale-text">Marcar Escala (5M)</span>
</button>
<!-- Botón Ayuda -->
<button onclick="toggleHelpModal(true)" class="bg-indigo-600 hover:bg-indigo-500 text-white font-bold px-3 py-1 rounded-lg text-xs transition-all flex items-center gap-1.5 shadow-md" title="Guía de uso de la regla Portland">
<i class="fa-solid fa-circle-question"></i> Ayuda
@@ -101,6 +106,9 @@
<input type="range" id="zoom-slider" min="0.5" max="3.0" step="0.05" value="1.0" class="w-full accent-amber-500">
<button onclick="adjustZoom(0.1)" class="bg-slate-800 hover:bg-slate-700 text-white px-2.5 py-1 rounded font-bold text-sm">+</button>
</div>
<div id="scale-status" class="text-[10px] text-cyan-400 font-mono mt-1">
Escala: No calibrada (Usar botón Marcar Escala)
</div>
</div>
<div class="border-b border-slate-800 pb-1.5 pt-1">
@@ -147,11 +155,9 @@
<i class="fa-solid fa-crosshairs mr-1.5 animate-pulse"></i> <span id="status-text">Haz clic para añadir puntos en la carta (A -> B -> C...)</span>
</div>
<!--
<div class="absolute bottom-3 right-3 bg-slate-950/80 border border-slate-800 text-[9.5px] text-slate-400 px-2.5 py-1 rounded-md backdrop-blur-sm pointer-events-none z-10">
Usa el zoom o las barras de desplazamiento para explorar la carta
</div>
-->
</div>
</main>
@@ -168,20 +174,15 @@
</button>
</div>
<div class="space-y-2.5 leading-relaxed">
<p><strong>1. Medir o Trazar un Rumbo Verdadero (Rv):</strong></p>
<p><strong>1. Calibrar Escala (Distancias):</strong></p>
<ul class="list-disc list-inside space-y-1 text-slate-400 pl-2">
<li>Coloca el borde recto de la regla uniendo tu origen y tu destino. Asegúrate de que la <strong>Gran Flecha Azul</strong> apunte en el sentido de la navegación.</li>
<li>Gira el disco central de la brújula (Rosa de los Vientos) hasta que las líneas de su cuadrícula interna queden perfectamente paralelas a los <strong>meridianos (líneas Norte-Sur)</strong> de la carta.</li>
<li>Lee directamente en el transportador el <strong>Rumbo Verdadero (Rv)</strong> indicado por la marca de lectura fija.</li>
<li>Haz clic en <strong>"Marcar Escala"</strong>[cite: 14].</li>
<li>Haz clic en el primer punto (Punto 0) sobre la escala de latitudes de la carta[cite: 14].</li>
<li>Haz clic en el segundo punto (Punto 1) situado exactamente a <strong>5 millas náuticas</strong> de distancia[cite: 14]. El sistema memorizará la proporción de píxeles por milla[cite: 14].</li>
</ul>
<p><strong>2. Corrección Total (Ct) y Rumbo de Aguja (Ra):</strong></p>
<p><strong>2. Medir o Trazar un Rumbo y Distancias[cite: 3, 10, 11, 12]:</strong></p>
<ul class="list-disc list-inside space-y-1 text-slate-400 pl-2">
<li>Ajusta la corrección total Ct = (±dm) + (±Δ) en el deslizador lateral.</li>
<li>Obtén el Rumbo Magnético o de Aguja (Rm/Ra) aplicando la fórmula clásica de corrección.</li>
</ul>
<p><strong>3. Trazado de Rutas Múltiples (A ➞ B ➞ C ➞ ...):</strong></p>
<ul class="list-disc list-inside space-y-1 text-slate-400 pl-2">
<li>Haz clic en <strong>"Añadir Puntos Ruta"</strong> para marcar secuencialmente los waypoints en la carta. El primer tramo (A ➞ B) orientará la regla automáticamente; para los siguientes tramos (B ➞ C ➞ ...), el usuario debe orientar y mover la regla manualmente sobre la línea trazada.</li>
<li>Usa <strong>"Añadir Puntos Ruta"</strong> para trazar waypoints ($A \rightarrow B \rightarrow C$)[cite: 10, 11, 12]. Sobre cada línea de ruta se mostrará automáticamente su distancia en millas náuticas[cite: 14].</li>
</ul>
</div>
<div class="pt-2 flex justify-end">
@@ -203,8 +204,10 @@
dragStartX: 0,
dragStartY: 0,
zoom: 1.0,
mode: 'MOVE',
mode: 'MOVE', // 'MOVE', 'ADD_POINTS', 'SCALE_P0', 'SCALE_P1'
points: [],
scalePoints: [], // Almacena [ {x, y} para P0, {x, y} para P1 ] base a zoom 1.0
pixelsPerNauticalMileBase: null, // Píxeles por milla a zoom 1.0
customChartImg: null,
baseWidth: 1400,
baseHeight: 900
@@ -259,6 +262,8 @@
const scale = img.width > maxW ? maxW / img.width : 1;
state.baseWidth = img.width * scale;
state.baseHeight = img.height * scale;
state.pixelsPerNauticalMileBase = null; // Reiniciar escala al cambiar de carta
updateScaleStatusUI();
initCanvasSize();
centerRulerAbsolute();
};
@@ -274,6 +279,8 @@
state.baseWidth = 1400;
state.baseHeight = 900;
state.zoom = 1.0;
state.pixelsPerNauticalMileBase = null;
updateScaleStatusUI();
document.getElementById('zoom-slider').value = 1.0;
initCanvasSize();
centerRulerAbsolute();
@@ -295,6 +302,17 @@
setZoom(parseFloat(e.target.value));
});
function updateScaleStatusUI() {
const st = document.getElementById('scale-status');
if (state.pixelsPerNauticalMileBase !== null) {
st.innerText = "Escala: Calibrada OK (5 Millas configuradas)";
st.className = "text-[10px] text-emerald-400 font-mono mt-1";
} else {
st.innerText = "Escala: No calibrada (Usar botón Marcar Escala)";
st.className = "text-[10px] text-cyan-400 font-mono mt-1";
}
}
function drawChart() {
if (state.customChartImg) {
ctx.drawImage(state.customChartImg, 0, 0, canvas.width, canvas.height);
@@ -336,7 +354,39 @@
ctx.setLineDash([]);
}
// Dibujar líneas de ruta encadenadas (A -> B -> C -> ...)
// Dibujar puntos y líneas de calibración de escala si existen
if (state.scalePoints.length > 0) {
state.scalePoints.forEach((sp, idx) => {
let currX = sp.x * state.zoom;
let currY = sp.y * state.zoom;
ctx.fillStyle = '#06b6d4';
ctx.beginPath();
ctx.arc(currX, currY, 6, 0, Math.PI * 2);
ctx.fill();
ctx.strokeStyle = '#ffffff';
ctx.lineWidth = 2;
ctx.stroke();
ctx.fillStyle = '#06b6d4';
ctx.font = 'bold 11px sans-serif';
ctx.fillText(`Escala P${idx} (${idx === 0 ? '0M' : '5M'})`, currX + 10, currY + 4);
});
if (state.scalePoints.length === 2) {
let p0 = { x: state.scalePoints[0].x * state.zoom, y: state.scalePoints[0].y * state.zoom };
let p1 = { x: state.scalePoints[1].x * state.zoom, y: state.scalePoints[1].y * state.zoom };
ctx.strokeStyle = '#06b6d4';
ctx.lineWidth = 2;
ctx.setLineDash([3, 3]);
ctx.beginPath();
ctx.moveTo(p0.x, p0.y);
ctx.lineTo(p1.x, p1.y);
ctx.stroke();
ctx.setLineDash([]);
}
}
// Dibujar líneas de ruta encadenadas (A -> B -> C -> ...) y sus distancias
if (state.points.length > 1) {
ctx.strokeStyle = '#f59e0b';
ctx.lineWidth = 3;
@@ -348,9 +398,41 @@
}
ctx.stroke();
ctx.setLineDash([]);
// Escribir la distancia en millas náuticas sobre cada segmento de ruta si la escala está calibrada
if (state.pixelsPerNauticalMileBase !== null) {
let currentPixelsPerNM = state.pixelsPerNauticalMileBase * state.zoom;
for (let i = 0; i < state.points.length - 1; i++) {
let pA = state.points[i];
let pB = state.points[i + 1];
let distPixels = Math.hypot(pB.x - pA.x, pB.y - pA.y);
let distNM = distPixels / currentPixelsPerNM;
let midX = (pA.x + pB.x) / 2;
let midY = (pA.y + pB.y) / 2;
ctx.save();
ctx.translate(midX, midY);
ctx.fillStyle = 'rgba(15, 23, 42, 0.85)';
ctx.strokeStyle = '#f59e0b';
ctx.lineWidth = 1;
let text = `${distNM.toFixed(2)} M`;
ctx.font = 'bold 11px monospace';
let textWidth = ctx.measureText(text).width;
ctx.fillRect(-textWidth / 2 - 6, -10, textWidth + 12, 20);
ctx.strokeRect(-textWidth / 2 - 6, -10, textWidth + 12, 20);
ctx.fillStyle = '#f59e0b';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText(text, 0, 0);
ctx.restore();
}
}
}
// Dibujar marcadores de puntos
// Dibujar marcadores de puntos de ruta
state.points.forEach(p => {
ctx.fillStyle = '#ef4444';
ctx.beginPath();
@@ -390,7 +472,7 @@
ctx.lineWidth = 1;
ctx.strokeRect(rx + 2, ry + 2, rw - 4, rh - 4);
// Gran flecha azul "DIRECTION OF BEARING" (hacia la derecha en el sistema local)
// Gran flecha azul "DIRECTION OF BEARING"
ctx.fillStyle = 'rgba(2, 132, 199, 0.85)';
ctx.beginPath();
ctx.moveTo(rx + rw - 40, 0);
@@ -409,7 +491,6 @@
ctx.lineWidth = 0.8;
const gridStep = 20;
// Líneas Verticales (omitiendo el círculo central)
for (let x = rx + 30; x <= rx + rw - 30; x += gridStep) {
if (Math.abs(x) >= radius + 2) {
ctx.beginPath();
@@ -419,7 +500,6 @@
}
}
// Líneas Horizontales (omitiendo el círculo central)
for (let y = ry + 15; y <= ry + rh - 15; y += gridStep) {
ctx.beginPath();
ctx.moveTo(rx + 30, y);
@@ -432,7 +512,6 @@
ctx.stroke();
}
// Marcas milimetradas/graduadas en los bordes superiores e inferiores
ctx.fillStyle = '#0f172a';
ctx.font = '8px monospace';
ctx.textAlign = 'center';
@@ -454,7 +533,7 @@
}
}
// --- DISCO CENTRAL GIRATORIO (TOTALMENTE TRANSPARENTE) ---
// --- DISCO CENTRAL GIRATORIO ---
ctx.save();
const diskAngleRad = (state.diskAngle * Math.PI) / 180;
ctx.rotate(diskAngleRad);
@@ -465,7 +544,6 @@
ctx.arc(0, 0, radius, 0, Math.PI * 2);
ctx.stroke();
// Cuadrícula propia del disco giratorio
ctx.strokeStyle = 'rgba(15, 23, 42, 0.4)';
ctx.lineWidth = 0.8;
const innerStep = 14;
@@ -484,7 +562,6 @@
ctx.stroke();
}
// Marcas de grados del transportador
for (let deg = 0; deg < 360; deg += 2) {
const rad = (deg * Math.PI) / 180;
const isMajor = deg % 10 === 0;
@@ -514,7 +591,6 @@
}
}
// TRIÁNGULO ROJO EN EL NORTE (0º) DEL DISCO
ctx.fillStyle = '#ef4444';
ctx.beginPath();
ctx.moveTo(0, -radius + 12);
@@ -523,9 +599,8 @@
ctx.closePath();
ctx.fill();
ctx.restore(); // Fin de rotación del disco
ctx.restore();
// Perno central de la brújula
ctx.fillStyle = '#f59e0b';
ctx.beginPath();
ctx.arc(0, 0, 7, 0, Math.PI * 2);
@@ -538,7 +613,6 @@
ctx.arc(0, 0, 2, 0, Math.PI * 2);
ctx.fill();
// --- MARCA DE LECTURA FIJA (Índice triangular negro a la derecha) ---
ctx.save();
const markerX = radius;
ctx.fillStyle = '#000000';
@@ -550,7 +624,7 @@
ctx.fill();
ctx.restore();
ctx.restore(); // Fin de la regla principal
ctx.restore();
}
function draw() {
@@ -577,6 +651,25 @@
document.getElementById('error-val').innerText = `${Math.abs(state.totalError)}° ${state.totalError < 0 ? 'W' : 'E'}`;
}
function toggleScaleMode() {
const btn = document.getElementById('btn-mode-scale');
const badge = document.getElementById('status-badge');
const statusText = document.getElementById('status-text');
if (state.mode !== 'SCALE_P0' && state.mode !== 'SCALE_P1') {
state.mode = 'SCALE_P0';
state.scalePoints = [];
canvas.className = "cursor-crosshair-custom block m-auto";
btn.className = "bg-rose-600 hover:bg-rose-500 text-white font-bold px-3 py-1 rounded-lg text-xs transition-all flex items-center gap-1.5 shadow-md";
document.getElementById('btn-scale-text').innerText = "Cancelar Escala";
badge.classList.remove('hidden');
statusText.innerText = "Haz clic en el Punto 0 (origen de 5 millas en escala de latitud)";
} else {
resetToMoveMode();
}
draw();
}
function toggleRouteMode() {
const btn = document.getElementById('btn-mode-ab');
const badge = document.getElementById('status-badge');
@@ -598,9 +691,14 @@
function resetToMoveMode() {
state.mode = 'MOVE';
canvas.className = "cursor-grab-custom block m-auto";
const btn = document.getElementById('btn-mode-ab');
btn.className = "bg-amber-500 hover:bg-amber-400 text-slate-950 font-bold px-3.5 py-1 rounded-lg text-xs transition-all flex items-center gap-2 shadow-md";
const btnAb = document.getElementById('btn-mode-ab');
btnAb.className = "bg-amber-500 hover:bg-amber-400 text-slate-950 font-bold px-3.5 py-1 rounded-lg text-xs transition-all flex items-center gap-2 shadow-md";
document.getElementById('btn-ab-text').innerText = "Añadir Puntos Ruta";
const btnScale = document.getElementById('btn-mode-scale');
btnScale.className = "bg-cyan-600 hover:bg-cyan-500 text-white font-bold px-3 py-1 rounded-lg text-xs transition-all flex items-center gap-1.5 shadow-md";
document.getElementById('btn-scale-text').innerText = "Marcar Escala (5M)";
document.getElementById('status-badge').classList.add('hidden');
}
@@ -610,12 +708,30 @@
draw();
}
// --- GESTIÓN DE RATÓN Y TÁCTIL UNIFICADA ---
function handleInteractionStart(clientX, clientY) {
const rect = canvas.getBoundingClientRect();
const clickX = clientX - rect.left;
const clickY = clientY - rect.top;
if (state.mode === 'SCALE_P0') {
state.scalePoints = [{ x: clickX / state.zoom, y: clickY / state.zoom }];
state.mode = 'SCALE_P1';
document.getElementById('status-text').innerText = "Haz clic en el Punto 1 (a 5 millas náuticas exactas del Punto 0)";
draw();
return true;
} else if (state.mode === 'SCALE_P1') {
state.scalePoints.push({ x: clickX / state.zoom, y: clickY / state.zoom });
// Calcular píxeles por milla a zoom 1.0
let p0 = state.scalePoints[0];
let p1 = state.scalePoints[1];
let distPixelsBase = Math.hypot(p1.x - p0.x, p1.y - p0.y);
state.pixelsPerNauticalMileBase = distPixelsBase / 5.0; // 5 millas náuticas
updateScaleStatusUI();
resetToMoveMode();
draw();
return true;
}
if (state.mode === 'ADD_POINTS') {
const nextChar = String.fromCharCode(65 + state.points.length);
state.points.push({ x: clickX, y: clickY, label: nextChar });
@@ -645,16 +761,15 @@
}
const dist = Math.hypot(clickX - state.rulerX, clickY - state.rulerY);
// Radio de toque amplio para asegurar que coja la regla cómodamente en tablets (aprox 300px)
if (dist < 300) {
state.isDragging = true;
state.dragStartX = clickX - state.rulerX;
state.dragStartY = clickY - state.rulerY;
canvas.classList.remove('cursor-grab-custom');
canvas.classList.add('cursor-grabbing');
return true; // Capturado por la regla
return true;
}
return false; // Tocó fuera de la regla, permitir scroll del mapa
return false;
}
function handleInteractionMove(clientX, clientY) {
@@ -676,7 +791,6 @@
}
}
// Eventos de Ratón
canvas.addEventListener('mousedown', (e) => {
handleInteractionStart(e.clientX, e.clientY);
});
@@ -689,13 +803,12 @@
handleInteractionEnd();
});
// Eventos Táctiles (Tablet / Móvil)
canvas.addEventListener('touchstart', (e) => {
if (e.touches.length === 1) {
const touch = e.touches[0];
const captured = handleInteractionStart(touch.clientX, touch.clientY);
if (captured || state.mode === 'ADD_POINTS') {
e.preventDefault(); // Evita scroll solo si estás moviendo la regla o añadiendo puntos
if (captured || state.mode !== 'MOVE') {
e.preventDefault();
}
}
}, { passive: false });
@@ -704,7 +817,7 @@
if (state.isDragging && e.touches.length === 1) {
const touch = e.touches[0];
handleInteractionMove(touch.clientX, touch.clientY);
e.preventDefault(); // Bloquea el scroll de la página mientras arrastras la regla
e.preventDefault();
}
}, { passive: false });