Pureba blog pio y placa ciudadades json.
`; root.innerHTML = tpl; const $ = (id) => root.querySelector('#' + id); // ---------- util ---------- function esc(v){ if (v === null || v === undefined) return ''; if (typeof v === 'string') return v.trim(); if (typeof v === 'number' || typeof v === 'boolean') return String(v); try { return JSON.stringify(v); } catch(e) { return String(v); } } function dayKey(d){ return ['sun','mon','tue','wed','thu','fri','sat'][d.getDay()]; } function fmtDate(d){ const dias = ['Domingo','Lunes','Martes','Miércoles','Jueves','Viernes','Sábado']; const meses = ['enero','febrero','marzo','abril','mayo','junio','julio','agosto','septiembre','octubre','noviembre','diciembre']; return `${dias[d.getDay()]}, ${d.getDate()} de ${meses[d.getMonth()]} ${d.getFullYear()}`; } function fmtTime12(d){ return d.toLocaleTimeString('es-CO', { hour: 'numeric', minute: '2-digit', hour12: true }); } function time24to12(t){ if (!t || typeof t !== 'string') return ''; const [hh, mm] = t.split(':').map(n => parseInt(n, 10)); if (Number.isNaN(hh) || Number.isNaN(mm)) return t; const h12 = ((hh + 11) % 12) + 1; const suf = hh >= 12 ? 'p. m.' : 'a. m.'; return `${h12}:${String(mm).padStart(2,'0')} ${suf}`; } function fmtScheduleLabel(scheduleArr){ const parts = (scheduleArr || []) .filter(s => s && s.start && s.end) .map(s => `${time24to12(String(s.start))} – ${time24to12(String(s.end))}`); if (!parts.length) return 'Sin horario'; return `Aplica: ${parts.join(' · ')}`; } function toMinutes_(hhmm){ if (!hhmm) return null; const [h, m] = String(hhmm).split(':').map(n => parseInt(n, 10)); if (Number.isNaN(h) || Number.isNaN(m)) return null; return h * 60 + m; } function getEstadoHorario_(scheduleArr, now){ const dayMin = now.getHours() * 60 + now.getMinutes(); const slots = (scheduleArr || []) .map(s => ({ start: toMinutes_(s.start), end: toMinutes_(s.end), raw: s })) .filter(s => s.start !== null && s.end !== null) .sort((a,b) => a.start - b.start); if (!slots.length) return { label: 'Sin horario', kind: 'none' }; for (const s of slots){ if (dayMin >= s.start && dayMin < s.end){ return { label: `Aplica ahora · termina a las ${time24to12(s.raw.end)}`, kind: 'active' }; } } const next = slots.find(s => dayMin < s.start); if (next){ return { label: `No aplica ahora · empieza a las ${time24to12(next.raw.start)}`, kind: 'waiting' }; } const last = slots[slots.length - 1]; return { label: `Finalizó hoy · terminó a las ${time24to12(last.raw.end)}`, kind: 'finished' }; } // ---------- iconos (SVG inline) ---------- function iconSvg_(id){ // SVG simples, limpios, “parecen” vehículo sin verse infantiles. // Usa currentColor para heredar color. const common = `width="22" height="22" viewBox="0 0 24 24" fill="none" aria-hidden="true"`; const car = `
`;
const moto = `
`;
const taxi = `
`;
const truck = `
`;
const bus = `
`;
const shield = `
`;
const map = {
particulares: car,
motos: moto,
taxis: taxi,
carga: truck,
transporte_publico: bus,
especial: shield
};
return map[id] || car;
}
// ---------- reglas ----------
function normalizarValor_(raw){
const v = esc(raw);
if (!v) return 'Sin dato';
if (v === 'NO_APLICA') return 'No aplica';
if (v === 'PENDIENTE') return 'Sin dato';
if (v === 'TODOS') return 'Todos';
return v; // "6-7", "0-1", "9-0", etc.
}
function obtenerValor_(cat, k){
const ruleType = cat?.rules?.type || 'none';
if (ruleType === 'none') return 'No aplica';
if (ruleType === 'weekday_rotation') return normalizarValor_(cat?.rules?.days?.[k]);
return 'Sin dato';
}
function badge_(kind, text){
const cls =
kind === 'active' ? 'pz-badge pz-badge--active' :
kind === 'waiting' ? 'pz-badge pz-badge--wait' :
kind === 'finished' ? 'pz-badge pz-badge--done' :
kind === 'none' ? 'pz-badge pz-badge--none' :
'pz-badge';
return `
${esc(text)}`;
}
function renderCity(city){
const now = new Date();
$('pzTitle').textContent = `Pico y Placa ${esc(city.name)}`;
$('pzMeta').textContent = `${fmtDate(now)} · ${fmtTime12(now)}`;
const srcName = esc(city.source?.name);
const srcUrl = esc(city.source?.url);
const verified = esc(city.last_verified);
$('pzFoot').innerHTML = srcUrl
? `Basado en ${srcName
? `${srcName}`
: `fuente oficial`
}${verified ? ` · Verificado: ${verified}` : ''}`
: `${verified ? `Verificado: ${verified}` : ''}`;
const k = dayKey(now);
const cats = (city.categories || [])
.slice()
.sort((a,b) => (Number(a.priority||99) - Number(b.priority||99)) || esc(a.name).localeCompare(esc(b.name), 'es'));
$('pzGrid').innerHTML = cats.map(cat => {
const val = obtenerValor_(cat, k);
// Estado horario solo si aplica (si "No aplica", no tiene sentido)
const estado = (val === 'No aplica')
? { kind: 'na', label: 'No aplica' }
: getEstadoHorario_(cat.schedule, now);
const scheduleText = fmtScheduleLabel(cat.schedule);
// Texto de franja operativa:
// - si no aplica: no mostramos franja
// - si sin horario: mostramos "Sin horario"
// - si tiene horario: mostramos label estado (aplica ahora / empieza / finalizó)
let bandText = '';
if (val !== 'No aplica') {
bandText = (estado.kind === 'none') ? scheduleText : estado.label;
}
// Badge (estado corto) para UX
let badgeHtml = '';
if (val === 'No aplica') {
badgeHtml = badge_('none', 'No aplica');
} else if (estado.kind === 'active') {
badgeHtml = badge_('active', 'Aplica ahora');
} else if (estado.kind === 'waiting') {
badgeHtml = badge_('waiting', 'No aplica ahora');
} else if (estado.kind === 'finished') {
badgeHtml = badge_('finished', 'Finalizó');
} else {
badgeHtml = badge_('none', 'Sin horario');
}
const icon = iconSvg_(esc(cat.id));
const muted = (val === 'No aplica');
return `
${bandText ? `${esc(bandText)}
` : ''}
`;
}).join('');
}
async function init(){
const res = await fetch(DATA_URL, { cache: 'no-store' });
if (!res.ok) throw new Error('No se pudo cargar el dataset');
const data = await res.json();
const cities = (data.cities || []).slice();
if (!cities.length) throw new Error('No hay ciudades publicadas en el JSON');
const select = $('pzCitySelect');
select.innerHTML = cities
.slice()
.sort((a,b) => esc(a.name).localeCompare(esc(b.name), 'es'))
.map(c => ``)
.join('');
const preferred = CITY_ID;
const initial = cities.find(c => c.id === preferred) ? preferred : cities[0].id;
select.value = initial;
let currentCity = cities.find(c => c.id === select.value) || cities[0];
renderCity(currentCity);
select.addEventListener('change', () => {
currentCity = cities.find(c => c.id === select.value) || cities[0];
renderCity(currentCity);
});
// reloj + refresco de estados cada minuto (suficiente y liviano)
if (window.pzPypClock) clearInterval(window.pzPypClock);
window.pzPypClock = setInterval(() => {
const t = new Date();
$('pzMeta').textContent = `${fmtDate(t)} · ${fmtTime12(t)}`;
renderCity(currentCity);
}, 60 * 1000);
}
init().catch(err => {
$('pzTitle').textContent = 'Pico y Placa';
$('pzMeta').textContent = '';
$('pzGrid').innerHTML = `
No se pudo cargar la información. Revisa la URL del JSON.
`;
$('pzFoot').textContent = '';
console.error(err);
});
})();