Pico y Placa Cali

`; } }); function pad2(n){ return String(n).padStart(2,'0'); } function getTZ(root){ return (root.getAttribute('data-tz') || 'America/Bogota').trim() || 'America/Bogota'; } function nowPartsInTZ(tz){ const dtf = new Intl.DateTimeFormat('en-CA', { timeZone: tz, year:'numeric', month:'2-digit', day:'2-digit', hour:'2-digit', minute:'2-digit', second:'2-digit', hour12:false }); const parts = dtf.formatToParts(new Date()); const m = {}; for (const p of parts) if (p.type !== 'literal') m[p.type] = p.value; return { year:+m.year, month:+m.month, day:+m.day, hour:+m.hour, minute:+m.minute, second:+m.second }; } function ymdFromParts(p){ return `${p.year}-${pad2(p.month)}-${pad2(p.day)}`; } function minutesOfDayFromParts(p){ return p.hour*60 + p.minute; } function dateUTCNoonFromYMD(ymd){ const [y,m,d] = ymd.split('-').map(Number); return new Date(Date.UTC(y, m-1, d, 12, 0, 0)); } function ymdFromUTCNoon(dt){ return `${dt.getUTCFullYear()}-${pad2(dt.getUTCMonth()+1)}-${pad2(dt.getUTCDate())}`; } function addDaysYMD(ymd, delta){ const dt = dateUTCNoonFromYMD(ymd); dt.setUTCDate(dt.getUTCDate() + delta); return ymdFromUTCNoon(dt); } function fmtTimeNoSeconds(tz){ return new Intl.DateTimeFormat('es-CO', { timeZone: tz, hour:'numeric', minute:'2-digit', hour12:true }).format(new Date()); } function weekdayUpperES(tz, ymd){ const dt = dateUTCNoonFromYMD(ymd); const w = new Intl.DateTimeFormat('es-CO', { timeZone: tz, weekday:'long' }).format(dt); return String(w).toUpperCase(); } function prettyDateES(ymd){ const [y,m,d] = ymd.split('-').map(Number); const meses = ['Ene','Feb','Mar','Abr','May','Jun','Jul','Ago','Sep','Oct','Nov','Dic']; return `${d} de ${meses[m-1]} ${y}`; } function isHHMM(s){ return /^([01]\d|2[0-3]):([0-5]\d)$/.test(String(s||'').trim()); } function toMinutes(hhmm){ if (!isHHMM(hhmm)) return null; const [h,m] = hhmm.split(':').map(Number); return h*60 + m; } function time24to12ES(hhmm){ if (!isHHMM(hhmm)) return String(hhmm||''); const [h,m] = hhmm.split(':').map(Number); const h12 = ((h + 11) % 12) + 1; const suf = h >= 12 ? 'p. m.' : 'a. m.'; return `${h12}:${pad2(m)} ${suf}`; } function scheduleLabel(schedule){ const slots = Array.isArray(schedule) ? schedule : []; const parts = slots .filter(s => s && s.start && s.end) .map(s => `${time24to12ES(String(s.start))} a ${time24to12ES(String(s.end))}`); return parts.length ? parts.join(' · ') : 'Sin restricción'; } function getHorarioEstado(schedule, nowMin){ const slots = (Array.isArray(schedule) ? schedule : []) .map(s => ({ s: toMinutes(String(s.start||'')), e: toMinutes(String(s.end||'')) })) .filter(x => x.s !== null && x.e !== null) .sort((a,b) => a.s - b.s); if (!slots.length) return { kind:'none', endMin:null }; for (const slot of slots){ if (nowMin >= slot.s && nowMin < slot.e) return { kind:'active', endMin: slot.e }; } return { kind:'inactive', endMin:null }; } function countdownHM(endMin, nowMin){ if (endMin === null) return null; const rem = Math.max(0, endMin - nowMin); return { h: Math.floor(rem/60), m: rem % 60 }; } function safeUrl(u){ const s = String(u ?? '').trim(); return /^https?:\/\//i.test(s) ? s : ''; } function dayKeyFromYMD(ymd){ const dt = dateUTCNoonFromYMD(ymd); return ['sun','mon','tue','wed','thu','fri','sat'][dt.getUTCDay()]; } function iconSvg(key){ const common = `width="22" height="22" viewBox="0 0 24 24" fill="none" aria-hidden="true"`; const car = ``; const taxi = ``; const moto = ``; const k = String(key||'').toLowerCase(); if (k.includes('moto')) return moto; if (k.includes('taxi')) return taxi; return car; } function pinSvg(){ return ``; } function calSvg(){ return ``; } // “NO_APLICA” por fin de semana / reglas, y mapa por fecha si aplica function resolveRestriction(city, cat, ymd){ const rule = cat.rule || {}; const dk = dayKeyFromYMD(ymd); if (rule.type === "weekday_pairs"){ if (rule.no_weekends && (dk === "sat" || dk === "sun")){ return { value:"NO APLICA", kind:"na", note:"Fin de semana: no aplica.", schedule: [] }; } const v = rule.weekdays && rule.weekdays[dk] ? String(rule.weekdays[dk]) : ""; if (!v) return { value:"SIN INFORMACIÓN", kind:"unknown", note:"No hay dato cargado para esta fecha.", schedule: [] }; return { value:v, kind:"digits", note:"", schedule: cat.schedule || [] }; } if (rule.type === "date_map"){ const m = rule.map || {}; const v = m[ymd]; if (!v){ return { value:"SIN INFORMACIÓN", kind:"unknown", note:String(rule.note_on_no_data||"").trim(), schedule: [] }; } if (String(v).toUpperCase() === "NO_APLICA"){ return { value:"NO APLICA", kind:"na", note:"No aplica para esta fecha.", schedule: [] }; } return { value:String(v), kind:"digits", note:"", schedule: cat.schedule || [] }; } return { value:"SIN INFORMACIÓN", kind:"unknown", note:"", schedule: [] }; } function mount(root){ const tz = getTZ(root); const key = String(root.getAttribute("data-key") || "").trim(); const cfg = window.PZ_PYP_DATA && window.PZ_PYP_DATA[key] ? window.PZ_PYP_DATA[key] : null; if (!cfg) throw new Error(`No existe window.PZ_PYP_DATA["${key}"]. Revisa data-key.`); const city = cfg.city; const options = (city.switch && Array.isArray(city.switch.options)) ? city.switch.options : []; root.innerHTML = `
PICO Y PLACA
Hora: --:--
--
--
Toca para elegir fecha
Si vas a viajar, revisa el pico y placa para la fecha de tu viaje.
`; const $ = (sel) => root.querySelector(sel); const elTitle = $('.pz-title'); const elTime = $('.pz-time'); const elCity = $('.pz-cityselect'); const elWeek = $('.pz-weekday'); const elDate = $('.pz-date'); const elDateInput = $('.pz-dateinput'); const elDateCenter = $('.pz-datecenter'); const elPrevDay = $('.pz-daybtn--left'); const elNextDay = $('.pz-daybtn--right'); const elFoot = $('.pz-foot'); const elFilter = $('.pz-filterselect'); const elTrack = $('.pz-track'); const elPrev = $('.pz-nav--prev'); const elNext = $('.pz-nav--next'); // City options elCity.innerHTML = options .slice() .sort((a,b) => String(a.name||'').localeCompare(String(b.name||''), 'es')) .map(o => ``) .join(''); elCity.value = city.id; // Filter options const catsAll = (Array.isArray(city.categories) ? city.categories : []) .slice() .sort((a,b) => (Number(a.priority||99) - Number(b.priority||99)) || String(a.name||'').localeCompare(String(b.name||''), 'es')); elFilter.innerHTML = [ ``, ...catsAll.map(c => ``) ].join(''); elFilter.value = "__all__"; let todayTZ = ymdFromParts(nowPartsInTZ(tz)); let selectedYMD = todayTZ; // Carrusel let visiblePerView = calcVisiblePerView(); let index = 0; let renderedCats = []; function calcVisiblePerView(){ return window.matchMedia('(min-width: 900px)').matches ? 2 : 1; } function updateHeader(){ elTitle.textContent = `PICO Y PLACA ${String(city.name || city.id).toUpperCase()}`.trim(); elTime.textContent = `Hora: ${fmtTimeNoSeconds(tz)}`; elWeek.textContent = weekdayUpperES(tz, selectedYMD); elDate.textContent = prettyDateES(selectedYMD); elDateInput.value = selectedYMD; } function updateFooter(){ const srcName = String(city.source && city.source.name ? city.source.name : '').trim(); const srcUrl = safeUrl(city.source && city.source.url ? city.source.url : ''); if (srcUrl){ elFoot.innerHTML = `Fuente: ${srcName || 'Fuente oficial'}`; } else { elFoot.textContent = srcName ? `Fuente: ${srcName}` : ''; } } function openPicker(){ if (typeof elDateInput.showPicker === 'function') elDateInput.showPicker(); else elDateInput.focus(); } function getFilteredCats(){ const v = String(elFilter.value || "__all__"); if (v === "__all__") return catsAll; return catsAll.filter(c => String(c.id) === v); } function cardHTML(cat){ return `
${iconSvg(cat.icon || cat.id)}
${String(cat.name || cat.id).toUpperCase()}
--
--
Termina en: --
Horario: --
`; } function clampIndex(){ const maxIndex = Math.max(0, renderedCats.length - visiblePerView); index = Math.max(0, Math.min(index, maxIndex)); } function updateNavState(){ const maxIndex = Math.max(0, renderedCats.length - visiblePerView); const showNav = renderedCats.length > visiblePerView; elPrev.style.display = showNav ? 'flex' : 'none'; elNext.style.display = showNav ? 'flex' : 'none'; elPrev.disabled = (index <= 0); elNext.disabled = (index >= maxIndex); root.querySelector('.pz-swipehint').style.display = showNav ? 'block' : 'none'; } function applyTransform(){ const card = elTrack.querySelector('.pz-card'); if (!card) return; const gap = parseFloat(getComputedStyle(elTrack).columnGap || getComputedStyle(elTrack).gap || '16') || 16; const cardW = card.getBoundingClientRect().width; const step = cardW + gap; elTrack.style.transform = `translateX(${-index * step}px)`; } function renderCards(){ visiblePerView = calcVisiblePerView(); renderedCats = getFilteredCats(); if (!renderedCats.length){ elTrack.innerHTML = `
No hay categorías para mostrar con este filtro.
`; elTrack.style.transform = 'translateX(0px)'; updateNavState(); return; } // Importante: para evitar “huecos” visuales, el carrusel siempre es una sola fila. elTrack.innerHTML = renderedCats.map(cardHTML).join(''); index = 0; clampIndex(); updateNavState(); applyTransform(); updateCardsDynamic(); } function updateCardsDynamic(){ const nowP = nowPartsInTZ(tz); todayTZ = ymdFromParts(nowP); const nowMin = minutesOfDayFromParts(nowP); const isTodaySelected = (selectedYMD === todayTZ); // Actualiza solo lo que está renderizado elTrack.querySelectorAll('.pz-card').forEach(card => { const catId = String(card.getAttribute('data-cat') || ''); const cat = renderedCats.find(c => String(c.id) === catId); if (!cat) return; const refs = { label: card.querySelector('.pz-label'), value: card.querySelector('.pz-value'), cd: card.querySelector('.pz-countdown'), cdText: card.querySelector('.pz-counttext'), band: card.querySelector('.pz-band'), bandText: card.querySelector('.pz-bandtext'), note: card.querySelector('.pz-note') }; const r = resolveRestriction(city, cat, selectedYMD); const sched = Array.isArray(r.schedule) && r.schedule.length ? r.schedule : (Array.isArray(cat.schedule) ? cat.schedule : []); const schedLabel = scheduleLabel(sched); refs.value.classList.remove('pz-value--na'); if (r.kind === 'na'){ refs.label.textContent = 'Estado:'; refs.value.textContent = 'NO APLICA'; refs.value.classList.add('pz-value--na'); refs.band.classList.remove('pz-band--yellow'); refs.band.classList.add('pz-band--gray'); refs.bandText.textContent = 'Sin restricción'; } else if (r.kind === 'unknown'){ refs.label.textContent = 'Estado:'; refs.value.textContent = 'SIN INFORMACIÓN'; refs.band.classList.remove('pz-band--yellow'); refs.band.classList.add('pz-band--gray'); refs.bandText.textContent = 'Sin restricción'; } else { refs.label.textContent = String(cat.plate_label || 'Placas terminadas en:'); refs.value.textContent = String(r.value); refs.band.classList.remove('pz-band--gray'); refs.band.classList.add('pz-band--yellow'); refs.bandText.textContent = schedLabel; } const note = String(r.note || '').trim(); if (note){ refs.note.style.display = 'block'; refs.note.textContent = note; } else { refs.note.style.display = 'none'; refs.note.textContent = ''; } // Conteo: solo si hoy y está dentro de un tramo const canCount = isTodaySelected && r.kind === 'digits' && Array.isArray(sched) && sched.length > 0; if (!canCount){ refs.cd.style.display = 'none'; refs.cdText.textContent = ''; return; } const st = getHorarioEstado(sched, nowMin); if (st.kind !== 'active'){ refs.cd.style.display = 'none'; refs.cdText.textContent = ''; return; } const hm = countdownHM(st.endMin, nowMin); refs.cd.style.display = 'flex'; refs.cdText.textContent = `Termina en: ${hm.h}h ${pad2(hm.m)}m`; }); elTime.textContent = `Hora: ${fmtTimeNoSeconds(tz)}`; } // Events elCity.addEventListener('change', () => { const nextId = elCity.value; const target = options.find(o => String(o.id) === String(nextId)); const url = target && target.url ? String(target.url).trim() : ''; if (safeUrl(url)) window.location.href = url; }); elPrevDay.addEventListener('click', () => { selectedYMD = addDaysYMD(selectedYMD, -1); updateHeader(); updateCardsDynamic(); }); elNextDay.addEventListener('click', () => { selectedYMD = addDaysYMD(selectedYMD, +1); updateHeader(); updateCardsDynamic(); }); elDateCenter.addEventListener('click', openPicker); elDateCenter.addEventListener('keydown', (e) => { if (e.key === 'Enter' || e.key === ' '){ e.preventDefault(); openPicker(); } }); elDateInput.addEventListener('change', () => { if (elDateInput.value){ selectedYMD = elDateInput.value; updateHeader(); updateCardsDynamic(); } }); elFilter.addEventListener('change', () => { renderCards(); }); elPrev.addEventListener('click', () => { index -= 1; clampIndex(); updateNavState(); applyTransform(); }); elNext.addEventListener('click', () => { index += 1; clampIndex(); updateNavState(); applyTransform(); }); // Swipe/drag (desktop + mobile) para que “se note” que es deslizable let drag = { active:false, startX:0, startT:0, moved:false }; function getTranslateX(){ const m = /translateX\(([-\d.]+)px\)/.exec(elTrack.style.transform || ''); return m ? parseFloat(m[1]) : 0; } function onDown(e){ if (renderedCats.length <= visiblePerView) return; drag.active = true; drag.moved = false; drag.startX = (e.touches ? e.touches[0].clientX : e.clientX); drag.startT = getTranslateX(); elTrack.classList.add('pz-dragging'); } function onMove(e){ if (!drag.active) return; const x = (e.touches ? e.touches[0].clientX : e.clientX); const dx = x - drag.startX; if (Math.abs(dx) > 6) drag.moved = true; elTrack.style.transform = `translateX(${drag.startT + dx}px)`; } function onUp(){ if (!drag.active) return; drag.active = false; elTrack.classList.remove('pz-dragging'); // Snap al índice más cercano const card = elTrack.querySelector('.pz-card'); if (!card){ applyTransform(); return; } const gap = parseFloat(getComputedStyle(elTrack).columnGap || getComputedStyle(elTrack).gap || '16') || 16; const cardW = card.getBoundingClientRect().width; const step = cardW + gap; const current = getTranslateX(); // negativo const rawIndex = Math.round(Math.abs(current) / step); index = rawIndex; clampIndex(); updateNavState(); applyTransform(); } const wrap = root.querySelector('.pz-trackwrap'); wrap.addEventListener('mousedown', onDown); wrap.addEventListener('mousemove', onMove); window.addEventListener('mouseup', onUp); wrap.addEventListener('touchstart', onDown, { passive:true }); wrap.addEventListener('touchmove', onMove, { passive:true }); wrap.addEventListener('touchend', onUp); window.addEventListener('resize', () => { const next = calcVisiblePerView(); if (next !== visiblePerView){ visiblePerView = next; index = 0; updateNavState(); applyTransform(); } else { applyTransform(); } }); // Init updateHeader(); updateFooter(); renderCards(); root._pzTimer && clearInterval(root._pzTimer); root._pzTimer = setInterval(() => { updateHeader(); updateCardsDynamic(); }, 30 * 1000); } })();
Redacción Seguros Bolívar

¡Hola! Somos el equipo de redacción. A través de nuestros artículos también queremos brindar tranquilidad y enriquecer la vida con integridad. Por eso, aquí ofrecemos contenidos educativos, recomendaciones, noticias y mucho más. Bienvenidos todos los lectores y las empresas que desean aprender y actualizarse con información de calidad.

Recent Posts

¿Cuánto es el aumento del arriendo en el 2026?

De acuerdo con la ley 820 de 2003, el aumento del arriendo lo determina la inflación y el IPC del…

9 January, 2026

Guía de prevención general para empresas en el 2026

Prepare su empresa para el 2026 con esta guía de prevención, así protegerá su negocio frente a riesgos y garantizará…

9 January, 2026

Seguros Bolívar destaca entre las aseguradoras líderes en sostenibilidad a nivel mundial

Según el CSA 2025, Seguros Bolívar se ubica entre los 12 mejores puntajes en sostenibilidad a nivel mundial en el…

8 January, 2026

Cómo el Seguro para copropiedades le acompaña en los retos del año nuevo

Descubra cómo el Seguro para Copropiedades le ayuda a enfrentar los retos del año nuevo con confianza y protección.

8 January, 2026

Evite el sedentarismo en vacaciones y priorice su salud

Consejos prácticos para evitar el sedentarismo y cuidar su bienestar físico y mental durante las vacaciones.

6 January, 2026

Exámenes médicos para iniciar el 2026

¡Comience el año cuidando su salud! Estos son algunos exámenes de rigor que se debe realizar. ¡Tome lápiz y papel!

6 January, 2026