megafonias exteriores

This commit is contained in:
2026-02-11 01:25:13 +01:00
parent 1b0e879ba2
commit 834b4de03e
45 changed files with 157 additions and 87 deletions

View File

@@ -1,12 +1,17 @@
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Megafonías TsSAEx</title>
<link rel="stylesheet" href="/src/style.css">
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined" rel="stylesheet" />
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@400;500;700&display=swap" rel="stylesheet">
</head>
<body>
<div id="loading-screen">
@@ -28,16 +33,24 @@
<div class="vol-header"><md-icon>meeting_room</md-icon><span>Panel Interior</span></div>
<md-filled-select id="int-linea" label="Línea"></md-filled-select>
<md-filled-select id="int-parada" label="Parada" disabled></md-filled-select>
<div class="radio-container">
<label><md-radio name="tipo" value="actual" checked></md-radio>Parada actual</label>
<label><md-radio name="tipo" value="siguiente"></md-radio>Parada siguiente</label>
</div>
<div id="container-regulacion" class="regulacion-box">
<md-checkbox id="chk-regulacion"></md-checkbox>
<label for="chk-regulacion">Parada de regulación</label>
</div>
<md-filled-button id="btn-int">Anunciar Parada</md-filled-button>
</section>
<section class="m3-card secondary">
<div class="vol-header"><md-icon>campaign</md-icon><span>Exterior</span></div>
<p style="margin:0; opacity:0.7; font-size:0.9rem;">Configuración de letrero y megafonía externa para paradas.</p>
<p style="margin:0; opacity:0.7; font-size:0.9rem;">Configuración de letrero y megafonía externa para paradas.
</p>
<md-filled-tonal-button id="btn-abrir-exterior">
<md-icon slot="icon">open_in_new</md-icon>Configurar Exterior
</md-filled-tonal-button>
@@ -74,7 +87,8 @@
</div>
<div id="lista-paradas-editor"></div>
<div class="editor-actions">
<md-filled-tonal-button id="btn-add-parada-ed"><md-icon slot="icon">add</md-icon>Añadir Parada</md-filled-tonal-button>
<md-filled-tonal-button id="btn-add-parada-ed"><md-icon slot="icon">add</md-icon>Añadir
Parada</md-filled-tonal-button>
<md-filled-button id="btn-generar-json">Exportar JSON</md-filled-button>
</div>
<pre id="output-json" class="json-preview"></pre>
@@ -88,7 +102,8 @@
<md-filled-select id="ext-linea" label="Línea"></md-filled-select>
<md-filled-select id="ext-destino" label="Destino" disabled></md-filled-select>
<md-filled-select id="ext-coche" label="Coche"></md-filled-select>
<div class="vol-header" style="margin-top:10px"><md-icon>volume_down</md-icon><span>Volumen Exterior</span></div>
<div class="vol-header" style="margin-top:10px"><md-icon>volume_down</md-icon><span>Volumen Exterior</span>
</div>
<md-slider id="vol-ext" min="0" max="1" step="0.01" value="0.9"></md-slider>
</div>
</form>
@@ -101,4 +116,5 @@
<script type="module" src="/src/main.js"></script>
</body>
</html>

1
package-lock.json generated
View File

@@ -735,6 +735,7 @@
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"dev": true,
"license": "MIT",
"peer": true,
"engines": {
"node": ">=12"
},

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -7,6 +7,10 @@
"L2": {
"nombre": "Línea 2",
"destinos": ["Zafra", "Hospital JRJ"]
},
"L3": {
"nombre": "Línea 3",
"destinos": ["Zafra", "Orden Baja"]
}
}
}

View File

@@ -1,118 +1,151 @@
import '@material/web/all.js';
import "@fontsource/roboto";
import 'https://cdn.jsdelivr.net/npm/@material/web/all.js/+esm';
let dbInt = {}, dbExt = {};
let audio = null;
let dbInt = {};
let dbExt = { lineas: {} }; // Base de datos exterior inicializada
let audioActual = null;
const get = (id) => document.getElementById(id);
// --- 1. INICIO DEL SISTEMA ---
async function init() {
try {
const [rI, rE] = await Promise.all([
fetch('/data/lineas.json'),
fetch('/data/exterior.json')
const [resInt, resExt] = await Promise.all([
fetch('data/lineas.json'),
fetch('data/exterior.json')
]);
dbInt = await rI.json();
dbExt = await rE.json();
// 1. Rellenar Líneas (Interior y Exterior)
const lineas = Object.keys(dbInt);
const opts = lineas.map(id => `<md-select-option value="${id}"><div slot="headline">${id}</div></md-select-option>`).join('');
if (!resInt.ok || !resExt.ok) throw new Error("Error cargando archivos JSON");
if(get('int-linea')) get('int-linea').innerHTML = opts;
if(get('ext-linea')) get('ext-linea').innerHTML = opts;
dbInt = await resInt.json();
dbExt = await resExt.json();
// 2. Generar Coches
let coches = [];
for (let i = 315; i <= 334; i++) coches.push(i);
for (let i = 400; i <= 409; i++) coches.push(i);
if(get('ext-coche')) {
get('ext-coche').innerHTML = coches.map(c => `<md-select-option value="${c}"><div slot="headline">${c}</div></md-select-option>`).join('');
}
} catch (e) {
console.error("Error cargando TsSAEx:", e);
}
// Quitar Splash
setTimeout(() => {
const splash = get('loading-screen');
if(splash) splash.style.opacity = '0';
setTimeout(() => {
if(splash) splash.style.display = 'none';
const app = get('app');
if(app) app.style.display = 'flex';
}, 500);
}, 2000);
}
// REPRODUCTOR
function play(lista, volumen) {
if (audio) { audio.pause(); audio.currentTime = 0; }
if (!lista || lista.length === 0) return;
const file = lista.shift();
audio = new Audio(`${file}?t=${Date.now()}`);
audio.volume = volumen;
audio.play().catch(() => play(lista, volumen));
audio.onended = () => play(lista, volumen);
}
// --- EVENTO CLAVE: CAMBIO DE LÍNEA INTERIOR ---
get('int-linea')?.addEventListener('change', (e) => {
const lineaSeleccionada = e.target.value;
const paradas = dbInt[lineaSeleccionada] || [];
const selectorParadas = get('int-parada');
if (selectorParadas) {
// Insertar nuevas paradas
selectorParadas.innerHTML = paradas.map(p =>
`<md-select-option value="${p.id}"><div slot="headline">${p.nombre}</div></md-select-option>`
const lineasHTML = Object.keys(dbInt).map(id =>
`<md-select-option value="${id}"><div slot="headline">${id}</div></md-select-option>`
).join('');
// IMPORTANTE: Resetear valor y habilitar
selectorParadas.value = "";
selectorParadas.disabled = false;
if (get('int-linea')) get('int-linea').innerHTML = lineasHTML;
if (get('ext-linea')) get('ext-linea').innerHTML = lineasHTML;
// Generar números de coche de Huelva (315-334 y 400-409)
const coches = [...Array(20).keys()].map(i => i + 315).concat([...Array(10).keys()].map(i => i + 400));
if (get('ext-coche')) {
get('ext-coche').innerHTML = coches.map(c =>
`<md-select-option value="${c}"><div slot="headline">${c}</div></md-select-option>`
).join('');
}
} catch (err) {
console.error("Error cargando DB:", err);
} finally {
const splash = get('loading-screen');
if (splash) {
splash.style.opacity = '0';
setTimeout(() => {
splash.style.display = 'none';
get('app').style.display = 'flex';
}, 500);
}
}
}
// --- 2. MOTOR DE AUDIO ---
function play(cola, volId = 'vol') {
if (audioActual) { audioActual.pause(); audioActual.currentTime = 0; }
if (!cola || cola.length === 0) return;
const item = cola.shift();
audioActual = new Audio(`${item.file}?cb=${Date.now()}`);
// Usar el slider correspondiente (interior o exterior)
const slider = get(volId);
audioActual.volume = slider ? slider.value : 0.8;
audioActual.play().catch(e => console.warn("Audio no encontrado:", item.file));
audioActual.onended = () => setTimeout(() => play(cola, volId), item.gap || 400);
}
// --- 3. EVENTOS PANEL INTERIOR ---
get('int-linea')?.addEventListener('change', (e) => {
const linea = dbInt[e.target.value];
const selParada = get('int-parada');
if (linea && selParada) {
selParada.innerHTML = linea.paradas.map(p =>
`<md-select-option value="${p.id}"><div slot="headline">${p.nombre}</div></md-select-option>`
).join('');
selParada.disabled = false;
selParada.value = "";
}
get('container-regulacion').style.display = 'none';
});
get('int-parada')?.addEventListener('change', (e) => {
const paradasReg = ['zafra', 'orden_baja', 'hospital_jrj'];
const esReg = paradasReg.includes(e.target.value);
get('container-regulacion').style.display = esReg ? 'flex' : 'none';
if (!esReg) get('chk-regulacion').checked = false;
});
// BOTÓN ANUNCIAR
get('btn-int')?.addEventListener('click', () => {
const lineaId = get('int-linea').value;
const paradaId = get('int-parada').value;
const tipo = document.querySelector('md-radio[name="tipo"][checked]')?.value || "actual";
const lId = get('int-linea').value, pId = get('int-parada').value;
if (!lId || !pId) return;
if(!lineaId || !paradaId) return alert("Selecciona línea y parada");
const paradaData = dbInt[lId].paradas.find(p => p.id === pId);
const tipo = document.querySelector('md-radio[value="siguiente"]')?.checked ? "siguiente" : "actual";
const p = dbInt[lineaId]?.find(x => x.id === paradaId);
if (!p) return;
let cola = [
{ file: `audio/parada_${tipo}.wav`, gap: 400 },
{ file: `audio/${pId}.wav`, gap: 600 }
];
let cola = [`/audio/parada_${tipo}.wav`, `/audio/${p.id}.wav` ];
if (p.enlaces?.length > 0) {
cola.push(`/audio/correspondencia.wav`);
p.enlaces.includes("todas") ? cola.push(`/audio/todas_las_lineas.wav`) : p.enlaces.forEach(e => cola.push(`/audio/linea_${e}.wav`));
if (paradaData.enlaces?.length > 0) {
const enlaces = paradaData.enlaces.filter(en => en.trim() !== "");
if (enlaces.includes("todas")) {
cola.push({ file: `audio/correspondencia_todas_las_lineas.wav`, gap: 400 });
} else if (enlaces.length > 0) {
cola.push({ file: `audio/${enlaces.length === 1 ? 'correspondencia_linea' : 'correspondencia_lineas'}.wav`, gap: 300 });
enlaces.forEach(en => cola.push({ file: `audio/linea_${en}.wav`, gap: 300 }));
}
play(cola, get('vol').value);
}
if (get('chk-regulacion')?.checked) cola.push({ file: `audio/parada_regulacion.wav`, gap: 400 });
play(cola, 'vol');
});
// LOGICA EXTERIOR (POPUP)
// --- 4. LÓGICA EXTERIOR (RESTAURADA) ---
get('btn-abrir-exterior').onclick = () => get('dialog-exterior').show();
get('ext-linea')?.addEventListener('change', (e) => {
const d = dbExt.lineas[e.target.value]?.destinos || [];
get('ext-destino').innerHTML = d.map(x => `<md-select-option value="${x}"><div slot="headline">${x}</div></md-select-option>`).join('');
get('ext-destino').disabled = false;
const lineaId = e.target.value;
const destinos = dbExt.lineas[lineaId]?.destinos || [];
const selDest = get('ext-destino');
if (selDest) {
selDest.innerHTML = destinos.map(d => `<md-select-option value="${d}"><div slot="headline">${d}</div></md-select-option>`).join('');
selDest.disabled = false;
}
});
get('btn-reproducir-ext').onclick = () => {
const c = get('ext-coche').value, l = get('ext-linea').value, d = get('ext-destino').value;
if (!c || !l || !d) return;
const destFile = d.toLowerCase().replace(/ /g, "_");
play(['/audio/linea.wav', `/audio/${l}.wav`, '/audio/autobus.wav', `/audio/${c}.wav`, '/audio/destino.wav', `/audio/${destFile}.wav`], get('vol-ext').value);
};
get('btn-reproducir-ext')?.addEventListener('click', () => {
const linea = get('ext-linea').value;
const destino = get('ext-destino').value;
const coche = get('ext-coche').value;
// BOTONES ESPECIALES
get('btn-colision').onclick = () => play(['/audio/colision.wav'], get('vol').value);
get('btn-hora').onclick = () => play(['/audio/hora_salida.wav'], get('vol').value);
get('btn-saldo').onclick = () => play(['/audio/atencion_saldo.wav'], get('vol').value);
get('btn-insuficiente').onclick = () => play(['/audio/saldo_insuficiente.wav'], get('vol').value);
if (!linea || !destino || !coche) return;
const destFile = destino.toLowerCase().replace(/ /g, "_");
// Cola exterior: Línea X -> Autobús XXX -> Destino YYY
const colaExt = [
{ file: `audio/exterior/linea_${linea}.wav`, gap: 300 },
{ file: `audio/exterior/autobus_${coche}.wav`, gap: 500 },
{ file: `audio/exterior/destino_${destFile}.wav`, gap: 300 }
];
play(colaExt, 'vol-ext');
});
// --- 5. ESPECIALES ---
get('btn-colision').onclick = () => play([{ file: `audio/colision.wav` }]);
get('btn-hora').onclick = () => play([{ file: `audio/hora_salida.wav` }]);
get('btn-saldo').onclick = () => play([{ file: `audio/atencion_saldo.wav` }]);
get('btn-insuficiente').onclick = () => play([{ file: `audio/saldo_insuficiente.wav` }]);
init();

View File

@@ -110,6 +110,22 @@ md-icon { font-family: 'Material Symbols Outlined' !important; }
}
.radio-container label { display: flex; align-items: center; gap: 12px; cursor: pointer; }
/* --- ESTILOS REGULACIÓN --- */
#container-regulacion {
/* display: none; El JS lo cambia a flex */
background: rgba(109, 94, 0, 0.08); /* Uso del color primary con baja opacidad */
padding: 12px 16px;
border-radius: 20px;
border: 1px dashed var(--md-sys-color-primary);
animation: slideIn 0.3s ease-out;
}
@keyframes slideIn {
from { opacity: 0; transform: translateY(-10px); }
to { opacity: 1; transform: translateY(0); }
}
/* --- BOTONES Y ESPECIALES --- */
.button-grid-specials { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; }
.danger-btn { --md-filled-tonal-button-container-color: var(--md-sys-color-error-container); }