{"id":39786,"date":"2025-06-27T20:20:20","date_gmt":"2025-06-27T18:20:20","guid":{"rendered":"https:\/\/cmm.imgw.pl\/?page_id=39786"},"modified":"2026-02-06T12:36:27","modified_gmt":"2026-02-06T11:36:27","slug":"imgw-pib-cmm-mapy-gorne-i-prognozy-wiazkowe","status":"publish","type":"page","link":"https:\/\/cmm.imgw.pl\/?page_id=39786","title":{"rendered":"IMGW-PIB CMM: Mapy g\u00f3rne"},"content":{"rendered":"\t\t<div data-elementor-type=\"wp-page\" data-elementor-id=\"39786\" class=\"elementor elementor-39786\">\n\t\t\t\t\t\t\t\t\t<section class=\"elementor-section elementor-top-section elementor-element elementor-element-8413ca7 elementor-section-boxed elementor-section-height-default elementor-section-height-default\" data-id=\"8413ca7\" data-element_type=\"section\">\n\t\t\t\t\t\t<div class=\"elementor-container elementor-column-gap-default\">\n\t\t\t\t\t<div class=\"elementor-column elementor-col-100 elementor-top-column elementor-element elementor-element-4f288ec\" data-id=\"4f288ec\" data-element_type=\"column\">\n\t\t\t<div class=\"elementor-widget-wrap elementor-element-populated\">\n\t\t\t\t\t\t\t\t<div class=\"elementor-element elementor-element-3ed7feb elementor-widget elementor-widget-html\" data-id=\"3ed7feb\" data-element_type=\"widget\" data-widget_type=\"html.default\">\n\t\t\t\t<div class=\"elementor-widget-container\">\n\t\t\t<div id=\"forecast-app\" class=\"forecast-app\">\n  <div class=\"layout-container\">\n    <!-- Lewa kolumna z przyciskami -->\n    <div class=\"left-column\">\n      <!-- Krok 1: Wyb\u00f3r modelu -->\n      <div class=\"section\">\n        <h3>Model:<\/h3>\n        <div id=\"models\" class=\"buttons models-buttons\"><\/div>\n      <\/div>\n\n      <!-- Krok 2: Wyb\u00f3r terminu startu -->\n      <div class=\"section\">\n        <h3>Termin startu:<\/h3>\n        <div id=\"terms\" class=\"buttons terms-buttons\"><\/div>\n      <\/div>\n\n      <!-- Krok 3: Wyb\u00f3r parametru -->\n      <div class=\"section\">\n        <h3>Parametr:<\/h3>\n        <div id=\"params\" class=\"select-container\">\n          <select id=\"paramsSelect\"><\/select>\n        <\/div>\n      <\/div>\n\n      <!-- Krok 4: Wyb\u00f3r statystyki (dla standardowych modeli) -->\n      <div class=\"section\" id=\"statsSection\" style=\"display: none;\">\n        <h3>Statystyka:<\/h3>\n        <div id=\"stats\" class=\"select-container\">\n          <select id=\"statsSelect\"><\/select>\n        <\/div>\n      <\/div>\n      \n      <!-- Krok 5: Wyb\u00f3r terminu prognozy -->\n      <div class=\"section\">\n        <h3 id=\"hoursTitle\">Godziny prognozy:<\/h3>\n        <div id=\"hours\" class=\"buttons\"><\/div>\n      <\/div>\n      \n      <!-- Informacja o czasie -->\n      <div class=\"time-note\">\n        *Je\u015bli przy formacie czasu nie widnieje adnotacja UTC to znaczy, \u017ce obraz generowany jest na wskazany termin wg czasu urz\u0119dowego.\n      <\/div>\n      \n      <!-- Informacja o obs\u0142udze klawiatury -->\n      <div class=\"keyboard-info\">\n        <strong>\ud83d\udca1 Skr\u00f3ty klawiszowe:<\/strong><br>\n        1-4 Wyb\u00f3r modelu<br>\n        &lt; &gt; Zmiana terminu startu<br>\n        \u2190 \u2192 Zmiana godzin prognozy<br>\n        \u2191 \u2193 Zmiana poziom\u00f3w ci\u015bnienia<br>\n        Tab Rozwini\u0119cie parametr\u00f3w<br>\n        A Animacja godzin prognozy<br>\n        C Por\u00f3wnanie modeli<br>\n        F Pe\u0142ny ekran\n      <\/div>\n    <\/div>\n    \n    <!-- \u015arodkowa kolumna z suwakiem poziom\u00f3w ci\u015bnienia (widoczna tylko dla odpowiednich parametr\u00f3w) -->\n    <div class=\"slider-column\" id=\"pressureLevelSliderSection\" style=\"display: none;\">\n      <div class=\"section pressure-slider\">\n        <h3>Poziom ci\u015bnienia:<\/h3>\n        <div class=\"pressure-display\">\n          <span id=\"pressureValue\">1000 hPa<\/span>\n        <\/div>\n        <!-- Slider - ukryty na urz\u0105dzeniach mobilnych -->\n        <div class=\"vertical-slider-container\" id=\"verticalSliderContainer\">\n          <input type=\"range\" id=\"pressureLevelSlider\" class=\"vertical-slider\" min=\"0\" max=\"5\" step=\"1\" value=\"0\" orient=\"vertical\">\n          <div class=\"pressure-labels\">\n            <span class=\"pressure-label\" style=\"bottom: calc(100% - 10px)\">300 hPa<\/span>\n            <span class=\"pressure-label\" style=\"bottom: calc(80% - 8px)\">500 hPa<\/span>\n            <span class=\"pressure-label\" style=\"bottom: calc(60% - 6px)\">700 hPa<\/span>\n            <span class=\"pressure-label\" style=\"bottom: calc(40% - 4px)\">850 hPa<\/span>\n            <span class=\"pressure-label\" style=\"bottom: calc(20% - 2px)\">950 hPa<\/span>\n            <span class=\"pressure-label\" style=\"bottom: 0\">1000 hPa<\/span>\n          <\/div>\n        <\/div>\n        <!-- Przyciski - widoczne tylko na urz\u0105dzeniach mobilnych -->\n        <div class=\"buttons\" id=\"pressureLevelButtons\" style=\"display: none;\"><\/div>\n      <\/div>\n    <\/div>\n    \n    <!-- Prawa kolumna z obrazkiem -->\n    <div class=\"right-column\">\n      <!--- Wy\u015bwietlanie obrazka -->\n      <div class=\"section images\">\n        <h3 id=\"imageTitle\" style=\"display: none;\"><\/h3>\n        \n        <!-- Dodatkowy poziomy slider do zmiany terminu prognozy (nad obrazkiem) -->\n        <div class=\"horizontal-slider-container\" id=\"horizontalSliderContainer\">\n          <div class=\"slider-header\">\n            <span class=\"slider-title\" id=\"sliderTitle\">Godzina prognozy:<\/span>\n            <span class=\"hour-value\" id=\"hourValue\">00h<\/span>\n            <div class=\"slider-controls\">\n              <button id=\"animateBtn\" class=\"control-btn\" title=\"Animuj godziny prognozy\">\u25b6<\/button>\n              <button id=\"compareBtn\" class=\"control-btn\" title=\"Por\u00f3wnaj 4 modele\">\u229e<\/button>\n              <button id=\"fullscreenBtn\" class=\"control-btn\" title=\"Pe\u0142ny ekran\">\u26f6<\/button>\n            <\/div>\n          <\/div>\n          <input type=\"range\" id=\"forecastHourSlider\" class=\"horizontal-slider\" min=\"0\" max=\"0\" step=\"1\" value=\"0\">\n          <div class=\"hour-labels\" id=\"hourLabels\"><\/div>\n        <\/div>\n        \n        <div id=\"dateWarning\" class=\"date-warning\"><\/div>\n        <div id=\"images\" class=\"images\"><\/div>\n        <div id=\"parameterDescription\" class=\"parameter-description\"><\/div>\n      <\/div>\n    <\/div>\n  <\/div>\n<\/div>\n\n<!-- Overlay dla trybu pe\u0142noekranowego -->\n<div id=\"fullscreenOverlay\" class=\"fullscreen-overlay\">\n  <div class=\"fullscreen-content\">\n    <div class=\"fullscreen-header\">\n      <div class=\"fullscreen-title\" id=\"fullscreenTitle\">Por\u00f3wnanie modeli<\/div>\n      <button class=\"fullscreen-close\" id=\"fullscreenClose\">\u2715<\/button>\n    <\/div>\n    <div class=\"fullscreen-images\" id=\"fullscreenImages\"><\/div>\n  <\/div>\n<\/div>\n\n<style>\n  .forecast-app { \n    font-family: Arial, sans-serif;\n    max-width: 100%;\n    margin: 0 auto;\n  }\n  \n  \/* Uk\u0142ad dwukolumnowy *\/\n  .layout-container {\n    display: flex;\n    flex-wrap: wrap;\n    gap: 10px;\n  }\n  \n  .left-column {\n    flex: 1;\n    min-width: 230px;\n    max-width: 230px;\n  }\n  \n  .time-note {\n    font-size: 10px;\n    color: #666;\n    margin-top: 15px;\n    padding: 0 10px;\n    line-height: 1.4;\n    font-style: italic;\n    text-align: justify;\n  }\n  \n  .keyboard-info {\n    font-size: 11px;\n    color: #555;\n    margin-top: 20px;\n    padding: 10px;\n    line-height: 1.6;\n    background-color: #f0f8ff;\n    border-left: 3px solid rgba(86, 221, 208, 1);\n    border-radius: 4px;\n  }\n  \n  .keyboard-info strong {\n    color: rgba(86, 221, 208, 1);\n    font-size: 12px;\n  }\n  \n  .slider-column {\n    flex: 0 0 auto;\n    width: 110px;\n    margin-top: 180px;\n    \n  }\n  \n  .right-column {\n    flex: 2;\n    min-width: 400px;\n  }\n  \n  \/* Style dla suwaka pionowego *\/\n  .pressure-slider {\n    height: 400px;\n    display: flex;\n    flex-direction: column;\n    align-items: flex-start;\n  }\n  \n  .vertical-slider-container {\n    height: 265px;\n    position: relative;\n    margin: 70px 0 10px 0;\n    padding-left: 0px;\n    display: flex; \n    justify-content: center;\n    align-items: center;\n  }\n  \n  .vertical-slider {\n    -webkit-appearance: none;\n    appearance: none;\n    width: 260px;\n    height: 10px;\n    border-radius: 5px;\n    background: #e0e0e0;\n    outline: none;\n    position: absolute;\n    left: -115px;\n    top: 50px;\n    transform: rotate(270deg);\n    margin: 0;\n  }\n  \n  .vertical-slider::-webkit-slider-thumb {\n    -webkit-appearance: none;\n    appearance: none;\n    width: 20px;\n    height: 20px;\n    border-radius: 50%;\n    background: rgba(86, 221, 208, 1);\n    cursor: pointer;\n  }\n  \n  .vertical-slider::-moz-range-thumb {\n    width: 20px;\n    height: 20px;\n    border-radius: 50%;\n    background: rgba(86, 221, 208, 1);\n    cursor: pointer;\n  }\n  \n  .pressure-display {\n    margin-top: 0px;\n    margin-bottom: 20px;\n    padding-left: 0px;\n    padding-right: 0px;\n    padding-top: 10px;\n    font-size: 1.4em;\n    font-weight: bold;\n    align-self: center;\n    color: rgba(86, 221, 208, 1);\n  }\n  \n  \/* Style dla etykiet poziomu ci\u015bnienia *\/\n  .pressure-labels {\n    position: absolute;\n    height: 240px;\n    right: -72px;\n    top: -60px;\n    width: 40px;\n    z-index: 10;\n  }\n  \n  .pressure-label {\n    position: absolute;\n    right: 0;\n    font-size: 0.65em;\n    color: #666;\n    white-space: nowrap;\n    margin-bottom: -5px;\n  }\n  \n  \/* Dodanie ma\u0142ych kropek przy etykietach *\/\n  .pressure-label:before {\n    content: \"\";\n    position: absolute;\n    left: -15px;\n    top: 50%;\n    width: 4px;\n    height: 4px;\n    background-color: #666;\n    border-radius: 50%;\n    transform: translateY(-50%);\n  }\n  \n  \/* Style dla poziomego slidera termin\u00f3w prognozy *\/\n  .horizontal-slider-container {\n    width: 100%;\n    position: relative;\n    margin: 0px 0 20px 0;\n    padding: 10px 0;\n    background-color: #f5f5f5;\n    border-radius: 5px;\n    box-shadow: 0 1px 3px rgba(0,0,0,0.1);\n    display: block;\n  }\n  \n  .horizontal-slider {\n    -webkit-appearance: none;\n    appearance: none;\n    width: 100%;\n    height: 10px;\n    border-radius: 5px;\n    background: linear-gradient(to right, #b0b0b0 0%, #b0b0b0 0%, #e0e0e0 0%, #e0e0e0 100%);\n    outline: none;\n    margin: 0px 0 10px 0;\n    position: relative;\n    z-index: 2;\n  }\n  \n  .horizontal-slider::-webkit-slider-thumb {\n    -webkit-appearance: none;\n    appearance: none;\n    width: 24px;\n    height: 24px;\n    border-radius: 50%;\n    background: rgba(86, 221, 208, 1);\n    cursor: pointer;\n    border: 2px solid #fff;\n    box-shadow: 0 1px 3px rgba(0,0,0,0.2);\n  }\n  \n  .horizontal-slider::-moz-range-thumb {\n    width: 24px;\n    height: 24px;\n    border-radius: 50%;\n    background: rgba(86, 221, 208, 1);\n    cursor: pointer;\n    border: 2px solid #fff;\n    box-shadow: 0 1px 3px rgba(0,0,0,0.2);\n  }\n  \n  .slider-header {\n    display: flex;\n    align-items: center;\n    text-align: left;\n    margin-bottom: 0px;\n    gap: 10px;\n  }\n  \n  .slider-title {\n    font-size: 1.1em;\n    font-weight: bold;\n    color: #333;\n    margin-right: 8px;\n  }\n  \n  .hour-value {\n    font-size: 1.3em;\n    font-weight: bold;\n    color: rgba(86, 221, 208, 1);\n  }\n  \n  .slider-controls {\n    margin-left: auto;\n    display: flex;\n    gap: 8px;\n  }\n  \n  .control-btn {\n    background: rgba(86, 221, 208, 0.1);\n    border: 2px solid rgba(86, 221, 208, 1);\n    color: rgba(86, 221, 208, 1);\n    font-size: 1.4em;\n    width: 40px;\n    height: 40px;\n    border-radius: 8px;\n    cursor: pointer;\n    transition: all 0.2s;\n    display: flex;\n    align-items: center;\n    justify-content: center;\n    padding: 0;\n  }\n  \n  .control-btn:hover {\n    background: rgba(86, 221, 208, 0.2);\n    transform: scale(1.05);\n  }\n  \n.control-btn:active {\n  transform: scale(0.95);\n}\n\n\/* Ukryj przycisk por\u00f3wnania na urz\u0105dzeniach mobilnych *\/\n@media (max-width: 768px) {\n  #compareBtn {\n    display: none !important;\n  }\n}  .control-btn.active {\n    background: rgba(86, 221, 208, 1);\n    color: white;\n  }\n  \n  .hour-labels {\n    display: flex;\n    justify-content: space-between;\n    margin-top: 0px;\n    color: #666;\n    font-size: 0.9em;\n    padding: 0 5px;\n  }\n  \n  \n  .section { \n    margin-bottom: 12px; \/* Zmniejszony margines dolny *\/\n    background: #f9f9f9;\n    padding: 10px; \/* Zmniejszony padding ca\u0142ej sekcji *\/\n    border-radius: 8px;\n    box-shadow: 0 1px 3px rgba(0,0,0,0.1);\n  }\n  \n  .section h3 {\n    margin-top: 0;\n    margin-bottom: 6px; \/* Mniejszy margines pod nag\u0142\u00f3wkiem *\/\n    border-bottom: 1px solid #ddd;\n    padding-bottom: 5px; \/* Zmniejszony padding pod nag\u0142\u00f3wkiem *\/\n    color: #333;\n  }\n  \n  .buttons { \n    display: flex;\n    flex-wrap: wrap;\n    gap: 5px; \/* Zmniejszony odst\u0119p *\/\n    margin-top: 3px; \/* Dodany ma\u0142y margines na g\u00f3rze *\/\n  }\n  \n  .buttons button {\n    padding: 4px 6px; \/* Zmniejszony padding poziomy *\/\n    border: 1px solid #666;\n    border-radius: 6px;\n    background: #f5f5f5;\n    cursor: pointer;\n    transition: all 0.2s;\n    font-size: 0.9em;\n    text-align: center;\n    box-sizing: border-box;\n  }\n  \n  .buttons button:hover { \n    background: #ddd; \n  }\n  \n  .section.images {\n    min-height: 600px;\n  }\n  \n  #images.images {\n    min-height: 600px;\n  }\n  \n  .parameter-description {\n    margin-top: 15px;\n    padding: 10px;\n    background-color: #f8f9fa;\n    border: 1px solid #dee2e6;\n    border-radius: 4px;\n    font-size: 14px;\n    color: #333;\n    line-height: 1.5;\n    text-align: justify;\n  }\n  \n  .date-warning {\n    display: none;\n    text-align: center;\n    padding: 8px 15px;\n    background: rgba(255, 152, 0, 0.1);\n    border: 1px solid rgba(255, 152, 0, 0.3);\n    border-radius: 6px;\n    font-size: 13px;\n    color: #e65100;\n    margin: 10px 0;\n    font-weight: 500;\n  }\n  \n  .images img {\n    max-width: 100%;\n    margin: 5px auto;\n    border: 1px solid #ccc;\n    display: block;\n    box-shadow: 0 2px 8px rgba(0,0,0,0.1);\n  }\n  \n  \/* Kontener obraz\u00f3w *\/\n  .images {\n    position: relative;\n    width: 100%;\n  }\n  \n  .buttons button.active {\n    background-color: rgba(86, 221, 208, 1);\n    color: white;\n    border-color: #2E7D32;\n  }\n  \n  \/* Style dla za\u0142adowanych godzin *\/\n  .buttons button.loaded {\n    background: linear-gradient(135deg, #d8d8d8 0%, #b8b8b8 100%);\n    position: relative;\n    box-shadow: inset 0 1px 3px rgba(0,0,0,0.15);\n  }\n  \n  .buttons button.loaded:not(.active) {\n    background: linear-gradient(135deg, #c8c8c8 0%, #a8a8a8 100%);\n    color: #333;\n  }\n  \n  .buttons button.active.loaded {\n    background: linear-gradient(135deg, rgba(86, 221, 208, 1) 0%, rgba(56, 191, 178, 1) 100%);\n    box-shadow: inset 0 1px 3px rgba(0,0,0,0.2);\n  }\n  \n  \/* Style dla przycisk\u00f3w modeli - ustawienie jeden pod drugim *\/\n  .models-buttons {\n    flex-direction: column;\n    align-items: center;\n    width: 100%;\n    gap: 3px; \/* Zmniejszony odst\u0119p mi\u0119dzy przyciskami *\/\n  }\n  \n  .models-buttons button {\n    width: 100%;\n    text-align: center;\n    margin: 1px 0; \/* Zmniejszony margines zewn\u0119trzny *\/\n    padding: 3px 12px; \/* Zmniejszony padding od g\u00f3ry i do\u0142u *\/\n  }\n  \n  \/* Style dla przycisk\u00f3w termin\u00f3w - wype\u0142nianie ca\u0142ej przestrzeni w wierszu *\/\n  .terms-buttons {\n    width: 100%;\n    justify-content: space-between;\n    flex-wrap: nowrap; \/* Zapewnia, \u017ce wszystkie przyciski b\u0119d\u0105 w jednym wierszu *\/\n  }\n  \n  .terms-buttons button {\n    flex-grow: 1;\n    text-align: center;\n    margin: 1px 0; \/* Zmniejszony margines zewn\u0119trzny *\/\n    flex-shrink: 0; \/* Zapobiega zmniejszaniu przycisk\u00f3w *\/\n  }\n  \n  \/* Style dla sekcji godzin prognozy - wype\u0142nianie kolumny *\/\n  #hours {\n    width: 100%;\n    display: flex;\n    flex-wrap: wrap;\n    gap: 3px; \/* Zmniejszony odst\u0119p mi\u0119dzy przyciskami *\/\n  }\n  \n  #hours button {\n    flex: 0 0 calc(25% - 2.25px); \/* Dok\u0142adnie 4 przyciski w rz\u0119dzie: 25% - (3px gap \u00d7 3 przerwy \/ 4 przyciski) *\/\n    width: calc(25% - 2.25px);\n    text-align: center;\n  }\n  \n  \/* Style dla list rozwijanych *\/\n  .select-container {\n    width: 100%;\n    margin-bottom: 8px;\n    position: relative;\n  }\n  \n  .select-container select {\n    width: 100%;\n    padding: 8px 12px;\n    border: 1px solid #ccc;\n    border-radius: 6px;\n    background-color: #f5f5f5;\n    font-size: 0.9em;\n    min-height: 36px; \/* Minimalna wysoko\u015b\u0107 pola select *\/\n    height: auto; \/* Wysoko\u015b\u0107 dostosowuje si\u0119 do zawarto\u015bci *\/\n    white-space: normal; \/* Tekst mo\u017ce zawija\u0107 si\u0119 do nast\u0119pnej linii *\/\n    overflow: visible; \/* Tekst nie zostanie uci\u0119ty *\/\n    appearance: none;\n    -webkit-appearance: none;\n    -moz-appearance: none;\n    background-image: url(\"data:image\/svg+xml;utf8,<svg fill='black' height='24' viewBox='0 0 24 24' width='24' xmlns='http:\/\/www.w3.org\/2000\/svg'><path d='M7 10l5 5 5-5z'\/><path d='M0 0h24v24H0z' fill='none'\/><\/svg>\");\n    background-repeat: no-repeat;\n    background-position: right 8px top 50%;\n  }\n  \n  .select-container select option {\n    padding: 8px 12px; \/* Dodatkowe wype\u0142nienie dla opcji *\/\n    min-height: 28px; \/* Minimalna wysoko\u015b\u0107 opcji *\/\n    line-height: 1.4; \/* Zwi\u0119kszony odst\u0119p mi\u0119dzy wierszami *\/\n    white-space: normal; \/* Tekst mo\u017ce zawija\u0107 si\u0119 do nast\u0119pnej linii *\/\n  }\n  \n  \/* Style dla niestandardowego selecta *\/\n  .custom-select-wrapper {\n    position: relative;\n    width: 100%;\n  }\n  \n  .custom-select {\n    width: 100%;\n    padding: 8px 12px;\n    border: 1px solid #ccc;\n    border-radius: 6px;\n    background-color: #f5f5f5;\n    font-size: 0.9em;\n    min-height: 36px;\n    cursor: pointer;\n    display: flex;\n    align-items: center;\n    justify-content: space-between;\n  }\n  \n  .custom-select:after {\n    content: '';\n    width: 0;\n    height: 0;\n    border-left: 5px solid transparent;\n    border-right: 5px solid transparent;\n    border-top: 5px solid #333;\n    margin-left: 10px;\n  }\n  \n  .custom-options {\n    position: absolute;\n    top: 100%;\n    left: 0;\n    right: 0;\n    background: white;\n    border: 1px solid #ccc;\n    border-radius: 0 0 6px 6px;\n    box-shadow: 0 2px 8px rgba(0,0,0,0.2);\n    max-height: 300px;\n    overflow-y: auto;\n    z-index: 100;\n    display: none;\n  }\n  \n  .custom-option {\n    padding: 8px 12px;\n    cursor: pointer;\n  }\n  \n  .custom-option:hover {\n    background-color: #f0f0f0;\n  }\n  \n  .custom-option.pressure-level {\n    color: rgba(86, 221, 208, 1) !important;\n    font-weight: bold;\n  }\n  \n  .select-container select:focus {\n    outline: none;\n    border-color: rgba(86, 221, 208, 1);\n    box-shadow: 0 0 5px rgba(76, 175, 80, 0.3);\n    height: auto; \/* Automatyczna wysoko\u015b\u0107 przy rozwini\u0119ciu *\/\n  }\n  \n  \/* Animacja \u0142adowania *\/\n  .loader-container {\n    display: flex;\n    justify-content: center;\n    align-items: center;\n    flex-direction: column;\n    width: 100%;\n    height: 300px;\n    z-index: 100;\n    position: relative;\n    background-color: rgba(245, 245, 245, 0.7); \/* Lekkie t\u0142o dla lepszej widoczno\u015bci *\/\n  }\n  \n  .loader {\n    border: 12px solid #f3f3f3;\n    border-radius: 50%;\n    border-top: 12px solid rgba(86, 221, 208, 1);\n    width: 100px;\n    height: 100px;\n    animation: spin 0.8s linear infinite;\n    z-index: 101;\n    box-shadow: 0px 0px 20px rgba(86, 221, 208, 0.7);\n  }\n  \n  @keyframes spin {\n    0% { transform: rotate(0deg); }\n    100% { transform: rotate(360deg); }\n  }\n  \n  \/* Style dla trybu pe\u0142noekranowego *\/\n  .fullscreen-overlay {\n    display: none;\n    position: fixed;\n    top: 0;\n    left: 0;\n    width: 100vw;\n    height: 100vh;\n    background: rgba(0, 0, 0, 0.95);\n    z-index: 10000;\n    padding: 20px;\n    overflow: auto;\n  }\n  \n  .fullscreen-overlay.active {\n    display: block;\n  }\n  \n  .fullscreen-content {\n    width: 100%;\n    height: 100%;\n    display: flex;\n    flex-direction: column;\n  }\n  \n  .fullscreen-header {\n    display: flex;\n    justify-content: space-between;\n    align-items: center;\n    margin-bottom: 20px;\n    color: white;\n  }\n  \n  .fullscreen-title {\n    font-size: 1.5em;\n    font-weight: bold;\n  }\n  \n  .fullscreen-close {\n    background: rgba(255, 255, 255, 0.1);\n    border: 2px solid white;\n    color: white;\n    font-size: 1.5em;\n    width: 50px;\n    height: 50px;\n    border-radius: 8px;\n    cursor: pointer;\n    transition: all 0.2s;\n  }\n  \n  .fullscreen-close:hover {\n    background: rgba(255, 255, 255, 0.2);\n    transform: scale(1.05);\n  }\n  \n  .fullscreen-images {\n    flex: 1;\n    display: grid;\n    grid-template-columns: 1fr 1fr;\n    gap: 20px;\n  }\n  \n  \/* Responsywno\u015b\u0107 na ma\u0142ych ekranach *\/\n  @media (max-width: 768px) {\n    .layout-container {\n      flex-direction: column;\n    }\n    \n    .left-column, .right-column, .slider-column {\n      max-width: 100%;\n      width: 100%;\n    }\n    \n    .slider-column {\n      width: auto;\n      flex: 0 0 auto;\n      margin-top: 20px;\n    }\n    \n    .pressure-slider {\n      height: auto;\n      width: fit-content;\n      margin: 0 auto;\n    }\n    \n    #pressureLevelButtons {\n      display: flex;\n      flex-wrap: wrap;\n      gap: 10px;\n      justify-content: center;\n      width: fit-content;\n      margin: 4px auto;\n    }\n    \n    #pressureLevelButtons button {\n      min-width: 70px;\n    }\n    \n    .vertical-slider-container {\n      margin: 0 auto;\n    }\n    \n    \/* Ukryj skr\u00f3ty klawiszowe na urz\u0105dzeniach mobilnych *\/\n    .keyboard-info {\n      display: none;\n    }\n    \n    \/* Usuni\u0119to sekcj\u0119 dla pressure-levels w widoku mobilnym *\/\n  }\n<\/style>\n\n<script>\nconst models = [\"ALARO\", \"GFS\", \"ICON-EU\", \"IFS\"];\nconst termsDefault = [\"00\", \"06\", \"12\", \"18\"];\n\n\/\/ Wsp\u00f3lna lista parametr\u00f3w dla wszystkich modeli\nconst commonParams = [\n  \"WWIND\", \"WWIND_LVL_300\", \"WWIND_LVL_500\", \"WWIND_LVL_700\", \"WWIND_LVL_850\", \"WWIND_LVL_925\", \"WWIND_LVL_950\",\n  \"WIND\", \"WIND_LVL_100\", \"WIND_LVL_200\", \"WIND_LVL_300\", \"WIND_LVL_500\", \"WIND_LVL_700\", \"WIND_LVL_850\", \"WIND_LVL_925\", \"WIND_LVL_950\",\n  \"TW850\", \"TW850_LVL_850_500\",\n  \"TW1000\", \"TW1000_LVL_1000_850\", \"TW1000_LVL_1000_700\", \"TW1000_LVL_1000_500\", \n  \"VOR\", \"VOR_LVL_300\", \"VOR_LVL_500\", \"VOR_LVL_700\", \"VOR_LVL_850\", \"VOR_LVL_925\", \"VOR_LVL_950\",\n  \"TTD\", \"TTD_LVL_300\", \"TTD_LVL_500\", \"TTD_LVL_700\", \"TTD_LVL_850\", \"TTD_LVL_925\", \"TTD_LVL_950\",\n  \"TEMP\", \"TEMP_LVL_100\", \"TEMP_LVL_200\", \"TEMP_LVL_300\", \"TEMP_LVL_500\", \"TEMP_LVL_700\", \"TEMP_LVL_850\", \"TEMP_LVL_925\", \"TEMP_LVL_950\",\n  \"THETA\", \"THETA_LVL_300\", \"THETA_LVL_500\", \"THETA_LVL_700\", \"THETA_LVL_850\", \"THETA_LVL_925\", \"THETA_LVL_950\",\n  \"RH\", \"RH_LVL_300\", \"RH_LVL_500\", \"RH_LVL_700\", \"RH_LVL_850\", \"RH_LVL_925\", \"RH_LVL_950\",\n  \"SH\", \"SH_LVL_700\", \"SH_LVL_850\", \"SH_LVL_925\", \"SH_LVL_950\",\n  \"DIV\", \"DIV_LVL_300\", \"DIV_LVL_500\", \"DIV_LVL_700\", \"DIV_LVL_850\", \"DIV_LVL_925\", \"DIV_LVL_950\",\n  \"MTGHT\", \"MTGHT_LVL_300\", \"MTGHT_LVL_500\", \"MTGHT_LVL_700\",\n  \"MTPRES_SFC\",\n  \"SHEAR\", \"SHEAR_LVL_0_1000\", \"SHEAR_LVL_0_3000\", \"SHEAR_LVL_0_6000\",\n  \"MTTMP\", \"MTTMP_LVL_300\", \"MTTMP_LVL_500\", \"MTTMP_LVL_700\", \"MTTMP_LVL_850\", \"MTTMP_LVL_925\", \"MTTMP_LVL_950\",\n  \"TGRAD\", \"TGRAD_LVL_500_850\"\n];\n\n\/\/ Parametry dla modeli - wszystkie modele u\u017cywaj\u0105 tej samej listy\nconst paramsForModels = {\n  \"ALARO\": commonParams,\n  \"GFS\": commonParams,\n  \"ICON-EU\": commonParams,\n  \"IFS\": commonParams\n};\n\n\/\/ Konfiguracja termin\u00f3w prognozy dla r\u00f3\u017cnych modeli\nconst forecastHours = {\n  \"ALARO\": {\n    \"00\": Array.from({length: 35}, (_, i) => i * 3), \/\/ co 3h od 0 do 102h\n    \"06\": Array.from({length: 35}, (_, i) => i * 3),\n    \"12\": Array.from({length: 35}, (_, i) => i * 3),\n    \"18\": Array.from({length: 35}, (_, i) => i * 3)\n  },\n  \"GFS\": {\n    \"00\": Array.from({length: 57}, (_, i) => i * 3), \/\/ co 3h od 0 do 168h\n    \"06\": Array.from({length: 57}, (_, i) => i * 3),\n    \"12\": Array.from({length: 57}, (_, i) => i * 3),\n    \"18\": Array.from({length: 57}, (_, i) => i * 3)\n  },\n  \"ICON-EU\": {\n    \"00\": Array.from({length: 41}, (_, i) => i * 3), \/\/ co 3h od 0 do 120h\n    \"06\": Array.from({length: 41}, (_, i) => i * 3),\n    \"12\": Array.from({length: 41}, (_, i) => i * 3),\n    \"18\": Array.from({length: 41}, (_, i) => i * 3)\n  },\n  \"IFS\": {\n    \"00\": [...Array.from({length: 49}, (_, i) => i * 3), 150, 156], \/\/ co 3h do 144h, potem 150h i 156h\n    \"06\": Array.from({length: 31}, (_, i) => i * 3), \/\/ co 3h do 90h\n    \"12\": [...Array.from({length: 41}, (_, i) => i * 3), 126, 132, 138, 144], \/\/ co 3h do 120h, potem co 6h do 144h\n    \"18\": Array.from({length: 31}, (_, i) => i * 3) \/\/ co 3h do 90h\n  },\n  \"default\": Array.from({length: 25}, (_, i) => i * 3) \/\/ fallback: co 3h od 0 do 72h\n};\n\n\/\/ Funkcja do znalezienia najd\u0142u\u017cszego zakresu prognozy spo\u015br\u00f3d wszystkich modeli dla danego terminu\nfunction getMaxForecastHours(term) {\n  let maxHours = [];\n  let maxLength = 0;\n  \n  models.forEach(model => {\n    if (forecastHours[model] && forecastHours[model][term]) {\n      const modelHours = forecastHours[model][term];\n      if (modelHours.length > maxLength) {\n        maxLength = modelHours.length;\n        maxHours = modelHours;\n      }\n    }\n  });\n  \n  return maxHours.length > 0 ? maxHours : forecastHours[\"default\"];\n}\n\n\/\/ Mapowanie przyjaznych nazw modeli dla interfejsu u\u017cytkownika\nconst modelDisplayNames = {\n  \"ALARO\": \"ALARO\",\n  \"GFS\": \"GFS\",\n  \"ICON-EU\": \"ICON-EU\",\n  \"IFS\": \"IFS\"\n};\n\n\/\/ Mapowanie przyjaznych nazw parametr\u00f3w dla interfejsu u\u017cytkownika\nconst parameterDisplayNames = {\n  WWIND: 'Pr\u0119dko\u015b\u0107 pionowa',\n  WIND: 'Wiatr i geopotencja\u0142',\n  VOR: 'Wirowo\u015b\u0107 wzgl\u0119dna',\n  TTD: 'Deficyt punktu rosy',\n  TEMP: 'Temperatura',\n  THETA: 'Temp. ekwiwal.-potencjal.',\n  RH: 'Wilgotno\u015b\u0107 i geopotencja\u0142',\n  SH: 'Wilgotno\u015b\u0107 w\u0142a\u015bciwa',\n  DIV: 'Dywergencja',\n  MTGHT: 'Tendencja geopotencja\u0142u',\n  MTPRES_SFC: 'Tendencja ci\u015bnienia',\n  SHEAR: 'Uskok wiatru',\n  TW850: 'Topografia wzgl\u0119dna 850hPa',\n  TW1000: 'Topografia wzgl\u0119dna 1000hPa',\n  MTTMP: 'Tendencja termiczna',\n  TGRAD: 'Gradient temperatury'\n};\n\n\n\/\/ Formatowanie termin\u00f3w prognozy w formacie dwucyfrowym\nconst forecastHoursDisplayNames = {\n  \"0\": \"00\",\n  \"1\": \"01\",\n  \"2\": \"02\",\n  \"3\": \"03\",\n  \"4\": \"04\", \n  \"5\": \"05\",\n  \"6\": \"06\",\n  \"7\": \"07\",\n  \"8\": \"08\",\n  \"9\": \"09\"\n};\n\n\/\/ Opisy parametr\u00f3w dla dynamicznego wy\u015bwietlania\nconst parameterDescriptions = {\n  \"WWIND\":\"Pr\u0119dko\u015b\u0107 pionowych ruch\u00f3w powietrza wyra\u017cona jest jako zmiana warto\u015bci ci\u015bnienia atmosferycznego (w Pa) w jednostce czasu (s), w zwi\u0105zku z tym warto\u015bci dodatnie oznaczaj\u0105 osiadanie, a warto\u015bci ujemne wznoszenie. Obszary wznoszenia cz\u0119sto odpowiadaj\u0105 strefom tworzenia si\u0119 zachmurzenia i opad\u00f3w atmosferycznych, zw\u0142aszcza w rejonie ni\u017c\u00f3w i front\u00f3w atmosferycznych. Wska\u017anik nie bierze pod uwag\u0119 proces\u00f3w g\u0142\u0119bokiej, wilgotnej konwekcji, jednak stanowi cenn\u0105 informacj\u0119 w ich prognozowaniu. Ujemne warto\u015bci wska\u017anika, kt\u00f3re wyst\u0119puj\u0105 w dolnej i \u015brodkowej cz\u0119\u015bci troposfery, b\u0119d\u0105 sprzyja\u0142y rozwojowi zachmurzenia konwekcyjnego podczas wyst\u0119powania stanu r\u00f3wnowagi warunkowo chwiejnej.\",\n  \"WIND\":\"Pr\u0119dko\u015b\u0107 i kierunek wiatru na poszczeg\u00f3lnych wysoko\u015bciach wyra\u017ca pr\u0119dko\u015b\u0107 i kierunek przemieszczania si\u0119 mas powietrznych. Nale\u017cy zwr\u00f3ci\u0107 uwag\u0119, \u017ce w dolnej troposferze (a dok\u0142adniej warstwie granicznej atmosfery rozci\u0105gaj\u0105cej si\u0119 od powierzchni ziemi do wysoko\u015bci 1-2 km nad ni\u0105) obok wielko\u015bci gradientu barycznego du\u017cy wp\u0142yw na pr\u0119dko\u015b\u0107 i kierunek wiatru b\u0119d\u0105 mie\u0107 w\u0142a\u015bciwo\u015bci powierzchni ziemi, w tym jej ukszta\u0142towanie i rodzaj pod\u0142o\u017ca (las, teren zabudowany, pola uprawne, powierzchnia wody) a tak\u017ce pora dnia. Ka\u017cdy rodzaj pod\u0142o\u017ca ma inny wsp\u00f3\u0142czynnik szorstko\u015bci, z czym wi\u0105\u017ce si\u0119 inna wielko\u015b\u0107 tarcia wywieranego na przemieszczaj\u0105ce si\u0119 nad nim powietrze. W warstwie granicznej atmosfery charakterystyczne jest zmniejszenie pr\u0119dko\u015bci wiatru oraz odchylenie jego kierunku na lewo wzgl\u0119dem wy\u017cszych poziom\u00f3w, wyst\u0119powanie poryw\u00f3w wiatru i turbulencji, a tak\u017ce dobowy cykl zmian w pr\u0119dko\u015bci i kierunku wiatru. W dzie\u0144 (o ile nie wyst\u0119puje zachmurzenie znacz\u0105co zmniejszaj\u0105ce dop\u0142yw promieniowania s\u0142onecznego do powierzchni ziemi) w przyziemnej cz\u0119\u015bci warstwy granicznej pr\u0119dko\u015b\u0107 wiatru wzrasta i pojawiaj\u0105 si\u0119 porywy wiatru, podczas gdy w \u015brodkowej i g\u00f3rnej cz\u0119\u015bci warstwy granicznej pr\u0119dko\u015b\u0107 wiatru spada. W nocy sytuacja si\u0119 odwraca i wiatr wyst\u0119puj\u0105cy przy ziemi zmniejsza swoj\u0105 pr\u0119dko\u015b\u0107, natomiast w \u015brodkowej i g\u00f3rnej cz\u0119\u015bci warstwy granicznej pr\u0119dko\u015b\u0107 wiatru zdecydowanie si\u0119 zwi\u0119ksza. W atmosferze swobodnej (powy\u017cej g\u00f3rnej granicy warstwy granicznej) kierunek wiatru jest zgodny z kierunkiem przebiegu izohips (linii \u0142\u0105cz\u0105cych punkty o tej samej wysoko\u015bci geopotencjalnej). \\n\\n Wysoko\u015b\u0107 geopotencjalna - koordynata pionowa u\u017cywana w meteorologii, gdzie poziomem odniesienia jest \u015bredni poziom morza. Wysoko\u015b\u0107 nad poziomem morza, na kt\u00f3rej wyst\u0119puje ci\u015bnienie atmosferyczne o okre\u015blonej warto\u015bci wyra\u017cona w dekametrach geopotencjalnych (gpdam). Wysoko\u015b\u0107 geopotencjalna w danym miejscu jest zmienna w czasie, a zmiany te warunkowane s\u0105 przemieszczaniem si\u0119 uk\u0142ad\u00f3w barycznych. Na podstawie obliczonej dla okre\u015blonych powierzchni izobarycznych (powierzchni o jednakowej warto\u015bci ci\u015bnienia atmosferycznego) wysoko\u015bci geopotencjalnej wykonuje si\u0119 mapy bezwzgl\u0119dnej topografii barycznej (TB), kt\u00f3re ze wzgl\u0119du na du\u017c\u0105 ilo\u015b\u0107 informacji o stanie atmosfery, jakie dostarczaj\u0105, nale\u017c\u0105 do najwa\u017cniejszych map u\u017cywanych przez synoptyk\u00f3w do prognozowania pogody. Do tych informacji mo\u017cemy zaliczy\u0107 m.in. po\u0142o\u017cenie i kierunki przemieszczania si\u0119 g\u0142\u00f3wnych uk\u0142ad\u00f3w barycznych kszta\u0142tuj\u0105cych pogod\u0119 w skali synoptycznej czy te\u017c cechy i nat\u0119\u017cenie cyrkulacji atmosferycznej. Na mapach bezwzgl\u0119dnej topografii barycznej znajduj\u0105 si\u0119 izohipsy, czyli linie punkty o tej samej wysoko\u015bci geopotencjalnej. Widoczne na mapach TB ograniczone obszary o obni\u017conych wysoko\u015bciach, kt\u00f3re tworz\u0105 na powierzchni izobarycznej ugi\u0119cia przypominaj\u0105ce lej, to ni\u017ce. Z kolei obszary o podwy\u017cszonych wysoko\u015bciach, kt\u00f3re tworz\u0105 ugi\u0119cia powierzchni izobarycznej w kszta\u0142cie kopu\u0142y b\u0119d\u0105 wyznacza\u0142y miejsca wyst\u0119powania wy\u017c\u00f3w. Dodatkowo na mapach powierzchni znajduj\u0105cych si\u0119 w g\u00f3rnej i \u015brodkowej troposferze mo\u017cliwe jest zidentyfikowanie po\u0142o\u017cenia rozleg\u0142ych fal atmosferycznych, nazywanych r\u00f3wnie\u017c falami Rossby\u2019go. Fale te rozwijaj\u0105 si\u0119 na skutek rotacji Ziemi, maj\u0105 zasadnicze znaczenie w wymianie ciep\u0142a pomi\u0119dzy niskimi i wysokimi szeroko\u015bciami geograficznymi oraz dla proces\u00f3w pogodotw\u00f3rczych w umiarkowanych szeroko\u015bciach geograficznych. Ich powstawaniu sprzyjaj\u0105 du\u017ce pasma g\u00f3rskie czy te\u017c kontrasty wyst\u0119puj\u0105ce pomi\u0119dzy kontynentami a oceanami. Ka\u017cda z tych fal sk\u0142ada si\u0119 z grzbietu (obszar z trzech stron otoczony ni\u017cszymi wysoko\u015bciami geopotencjalnymi, nazywanego r\u00f3wnie\u017c klinem) i doliny (obszar z trzech stron otoczony wy\u017cszymi wysoko\u015bciami geopotencjalnymi, inaczej nazywanej zatok\u0105). W grzbiecie znajduje si\u0119 ciep\u0142a masa powietrzna, natomiast w dolinie ch\u0142odna. W g\u00f3rnej cz\u0119\u015bci troposfery, wzd\u0142u\u017c kraw\u0119dzi grzbietu i doliny fali, w miejscu wyst\u0119powania du\u017cego gradientu temperatury cz\u0119sto rozwija si\u0119 d\u0142uga i w\u0105ska strefa wiatru o du\u017cych pr\u0119dko\u015bciach (30-100 m\/s), kt\u00f3r\u0105 nazywa si\u0119 pr\u0105dem strumieniowym. Nale\u017cy podkre\u015bli\u0107, \u017ce po\u0142o\u017cenie o\u015brodk\u00f3w i centr\u00f3w poszczeg\u00f3lnych uk\u0142ad\u00f3w barycznych na powierzchniach izobarycznych znajduj\u0105cych si\u0119 w dolnej cz\u0119\u015bci troposfery og\u00f3\u0142 nie odpowiada ich po\u0142o\u017ceniu na powierzchniach znajduj\u0105cych si\u0119 wy\u017cej. Ponadto im wy\u017csza powierzchnia izobaryczna, tym wolniej zachodz\u0105 du\u017ce zmiany rozk\u0142adu pola wysoko\u015bci geopotencjalnej. Rozk\u0142ad wysoko\u015bci geopotencjalnej w g\u00f3rnej troposferze b\u0119dzie kontrolowa\u0142 po\u0142o\u017cenie ni\u017c\u00f3w i wy\u017c\u00f3w w dolnej troposferze, dzi\u0119ki czemu mapy najwy\u017cszych poziom\u00f3w troposfery s\u0105 bardzo przydatne w orientacyjnym prognozowaniu przebiegu pogody w danym regionie geograficznym na d\u0142u\u017csze terminy. Najcz\u0119\u015bciej u\u017cywane mapy bezwzgl\u0119dnej topografii barycznej to mapy powierzchni izobarycznych 300, 500, 700 i 850 hPa.\",\n  \"VOR\":\"Wirowo\u015b\u0107 wzgl\u0119dna - jest to miara rotacji pola pr\u0119dko\u015bci powietrza w poziomie (wzd\u0142u\u017c pionowej osi) wzgl\u0119dem ustalonego punktu znajduj\u0105cego si\u0119 na p\u0142aszczy\u017anie Ziemi. Inaczej ujmuj\u0105c, jest to warto\u015b\u0107 opisuj\u0105ca tendencj\u0119 do skr\u0119cania trajektorii ruchu (rotacji) powietrza w poziomie. Warto\u015bci r\u00f3\u017cne od zera b\u0119d\u0105 \u015bwiadczy\u0142y o tym, \u017ce powietrze porusza si\u0119 po torze krzywoliniowym. Warto\u015bci dodatnie wskazuj\u0105 na sk\u0142onno\u015b\u0107 do tworzenia si\u0119 rotacji przeciwnej do ruchu wskaz\u00f3wek zegara (przemieszczaj\u0105ca si\u0119 cz\u0105stka powietrza b\u0119dzie mie\u0107 sk\u0142onno\u015b\u0107 do skr\u0119cania na lewo). Oznacza to, \u017ce na p\u00f3\u0142kuli p\u00f3\u0142nocnej warto\u015bci dodatnie wirowo\u015bci wzgl\u0119dnej b\u0119d\u0105 zwi\u0105zane z rotacj\u0105 cyklonaln\u0105, a na p\u00f3\u0142kuli po\u0142udniowej z rotacj\u0105 antycyklonaln\u0105. Po uwzgl\u0119dnieniu rotacji Ziemi poprzez dodanie warto\u015bci tak zwanego parametru Coriolisa do wirowo\u015bci wzgl\u0119dnej otrzymywana jest wirowo\u015b\u0107 absolutna. W prognozowaniu pogody najwa\u017cniejsze znaczenie maj\u0105 mapy wirowo\u015bci wzgl\u0119dnej dla powierzchni izobarycznych znajduj\u0105cych si\u0119 w \u015brodkowej i g\u00f3rnej troposferze. Mapy te s\u0105 pomocne w identyfikacji obszar\u00f3w tworzenia si\u0119 ni\u017c\u00f3w i wy\u017c\u00f3w. Nap\u0142yw wy\u017cszych warto\u015bci wirowo\u015bci wzgl\u0119dnej nad dany obszar cz\u0119sto wi\u0105\u017ce si\u0119 ze wznoszeniem mas powietrznych, w zwi\u0105zku z czym mapa z rozk\u0142adem warto\u015bci wirowo\u015bci mo\u017ce u\u0142atwi\u0107 rozpoznanie obszar\u00f3w tworzenia si\u0119 zachmurzenia.\",\n  \"TTD\":\"Deficyt punktu rosy jest r\u00f3\u017cnic\u0105 temperatury powietrza i temperatury punktu rosy (temperatury, do jakiej musi zosta\u0107 sch\u0142odzone powietrze, aby osi\u0105gn\u0119\u0142o stan nasycenia par\u0105 wodn\u0105). Jest miar\u0105 niedosytu pary wodnej w powietrzu. Im wy\u017csza warto\u015b\u0107 deficytu punktu rosy, tym powietrze jest wzgl\u0119dnie bardziej suche, st\u0105d wysokie warto\u015bci tego wska\u017anika b\u0119d\u0105 zazwyczaj zwi\u0105zane z obszarami wysokiego ci\u015bnienia. Z kolei warto\u015bci bardzo niskie (w tym 0\u00b0C) wskazuj\u0105 na stan powietrza bliski lub odpowiadaj\u0105cy nasyceniu par\u0105 wodn\u0105 i b\u0119d\u0105 zwi\u0105zane wyst\u0119powaniem stref zachmurzenia.\",\n  \"TEMP\":\"Temperatura powietrza jest jednym z element\u00f3w pogody. Jest ona \u015bci\u015ble zwi\u0105zana z chaotycznym ruchem cz\u0105steczek, z kt\u00f3rych zbudowane jest powietrze. Im \u015brednia pr\u0119dko\u015b\u0107 ruch cz\u0105stek jest wy\u017csza, tym wy\u017csza b\u0119dzie temperatura powietrza. W przypadku r\u00f3\u017cnic w temperaturze pomi\u0119dzy dwoma cia\u0142ami (na przyk\u0142ad masami powietrznymi), pomi\u0119dzy tymi cia\u0142ami przekazywane jest ciep\u0142o, a proces ten sko\u0144czy si\u0119 w momencie, gdy temperatura obydw\u00f3ch cia\u0142 osi\u0105gnie t\u0119 sam\u0105 warto\u015b\u0107. W warstwie granicznej atmosfery warto\u015b\u0107 temperatury podlega zmianom w cyklu dobowym. Najwi\u0119ksze zmiany dobowe zachodz\u0105 przy powierzchni ziemi, natomiast im wy\u017cej, tym zmiany s\u0105 mniejsze, a w swobodnej atmosferze nie zachodz\u0105. Z tego wynika, \u017ce rozk\u0142ad temperatury w \u015brodkowej i g\u00f3rnej troposferze kszta\u0142towany jest przez takie procesy jak transport mas powietrznych o odmiennej temperaturze, kondensacj\u0119 i parowanie czy te\u017c adiabatyczne ogrzewanie i sch\u0142adzanie powietrza. W celu zdiagnozowania w\u0142a\u015bciwo\u015bci mas powietrznych, ich granic (w tym r\u00f3wnie\u017c po\u0142o\u017cenia front\u00f3w atmosferycznych), kierunku i pr\u0119dko\u015bci przemieszczania si\u0119 najcz\u0119\u015bciej u\u017cywa si\u0119 map g\u00f3rnych dla powierzchni izobarycznej 850 hPa. Innym istotnym poziomem, dla kt\u00f3rego sprawdza si\u0119 rozk\u0142ad warto\u015bci temperatury, jest powierzchnia izobaryczna 500 hPa. Obszary z wyra\u017anie ni\u017csz\u0105 na tym poziome temperatur\u0105 cz\u0119sto wi\u0105\u017c\u0105 si\u0119 z wyst\u0119powaniem r\u00f3wnowagi warunkowo chwiejnej, a wi\u0119c z mo\u017cliwo\u015bci\u0105 rozwoju zachmurzenia konwekcyjnego, przelotnych opad\u00f3w deszczu i burz.\",\n  \"THETA\":\"Tendencja termiczna dla okre\u015blonej powierzchni izobarycznej informuje o intensywno\u015bci adwekcji termicznej i jej znaku, jak r\u00f3wnie\u017c zmian temperatury na skutek wyst\u0119powania proces\u00f3w adiabatycznych nad danym obszarem. Por\u00f3wnuj\u0105c wyniki uzyskane z r\u00f3\u017cnych poziom\u00f3w atmosfery (np. map\u0119 rozk\u0142adu tendencji termicznej na powierzchni izobarycznej 850 hPa z map\u0105 tendencji z powierzchni 500 hPa) mo\u017cemy oceni\u0107, czy troposfera staje si\u0119 chwiejna (na przyk\u0142ad na skutek wzrostu temperatury na powierzchni 925 hPa i jednoczesnego spadku na powierzchni 500 hPa) b\u0105d\u017a stabilna (na przyk\u0142ad na skutek spadku temperatury na powierzchni 925 hPa przy jednoczesnym wzro\u015bcie na powierzchni 500 hPa). Mapy obejmuj\u0105ce warstw\u0119 graniczn\u0105 troposfery (powierzchnie 850, 925 i 950 hPa) mog\u0105 by\u0107 przydatne w prognozowaniu zachmurzenia, mgie\u0142 i sytuacji ze smogiem w p\u00f3\u0142roczu ch\u0142odnym, zw\u0142aszcza podczas przej\u015bcia ciep\u0142ego frontu atmosferycznego lub zalegania ch\u0142odnego i wilgotnego powietrza w przypowierzchniowej warstwie troposfery. Przegl\u0105d map z dolnych poziom\u00f3w troposfery mo\u017ce r\u00f3wnie\u017c wspom\u00f3c prognozowanie zasi\u0119gu stref opad\u00f3w marzn\u0105cych.\",\n  \"RH\":\"Wilgotno\u015b\u0107 wzgl\u0119dna jest stosunkiem ci\u015bnienia parcjalnego pary wodnej zawartej w powietrzu do ci\u015bnienia pary nasyconej w powietrzu o danej temperaturze wyra\u017conym w procentach. Jego warto\u015b\u0107 waha si\u0119 w przedziale od 0% do 100%. Im wy\u017csza jest wilgotno\u015b\u0107 wzgl\u0119dna, tym powietrze znajduje si\u0119 bli\u017cej stanu nasycenia par\u0105 wodn\u0105. W przypadku osi\u0105gni\u0119cia warto\u015bci 100% pod wp\u0142ywem mikroskopijnych drobin (j\u0105der kondensacji) rozpoczyna si\u0119 proces kondensacji pary wodnej, tworz\u0105 si\u0119 chmury oraz opady atmosferyczne. Podobnie, jak w przypadku temperatury, wilgotno\u015b\u0107 wzgl\u0119dna w warstwie granicznej b\u0119dzie podlega\u0142a wahaniom w cyklu dobowym. Wraz ze wzrostem temperatury jej warto\u015b\u0107 b\u0119dzie si\u0119 obni\u017ca\u0107, a ze spadkiem wzrasta\u0107, chyba \u017ce nad danym obszarem nast\u0105pi nap\u0142yw masy powietrznej o innych w\u0142a\u015bciwo\u015bciach, w tym innej zawarto\u015bci pary wodnej. Nale\u017cy pami\u0119ta\u0107 o tym, \u017ce im wy\u017csza jest temperatura powietrza, tym wy\u017csze jest te\u017c ci\u015bnienie pary nasyconej. Oznacza to, \u017ce taka sama warto\u015b\u0107 ci\u015bnienia parcjalnego pary wodnej b\u0119dzie przek\u0142ada\u0107 si\u0119 na inne warto\u015bci wilgotno\u015bci wzgl\u0119dnej wyst\u0119puj\u0105ce przy r\u00f3\u017cnych warto\u015bciach temperatury. W celu wyra\u017cenia faktycznej zawarto\u015bci pary wodnej w powietrzu nale\u017cy u\u017cy\u0107 wilgotno\u015bci bezwzgl\u0119dnej.\",\n  \"SH\":\"Wilgotno\u015b\u0107 w\u0142a\u015bciwa powietrza to stosunek masy wody zawartej w danej obj\u0119to\u015bci powietrza do jej masy ca\u0142kowitej (z wod\u0105), wyra\u017cony w g\/kg. Im wy\u017csza wilgotno\u015b\u0107 w\u0142a\u015bciwa powietrza, tym wi\u0119ksza b\u0119dzie zawarto\u015b\u0107 pary wodnej w powietrzu, przy czym nale\u017cy pami\u0119ta\u0107, \u017ce wraz ze wzrostem temperatury powietrza o okre\u015blonej obj\u0119to\u015bci wzrasta te\u017c ilo\u015b\u0107 pary wodnej, jak\u0105 powietrze to mo\u017ce pomie\u015bci\u0107. Zwi\u0105zek ten jest opisany r\u00f3wnaniem wyk\u0142adniczym. Z tej zale\u017cno\u015bci wynika, \u017ce rozk\u0142ad przestrzenny wilgotno\u015bci w\u0142a\u015bciwej b\u0119dzie pomocny w ocenie rozmieszczenia i w\u0142a\u015bciwo\u015bci mas powietrznych, a tak\u017ce front\u00f3w atmosferycznych, szczeg\u00f3lnie w dolnej troposferze, gdzie wyst\u0119puje stosunkowo wysoka temperatura. Wilgotno\u015b\u0107 w\u0142a\u015bciwa powietrza jest wska\u017anikiem podobnym do stosunku zmieszania, kt\u00f3ry wyra\u017ca mas\u0119 pary wodnej (w gramach) przypadaj\u0105c\u0105 na 1 kg suchego powietrza.\",\n  \"DIV\":\"Dywergencja \u2013 wska\u017anik u\u017cywany do prognozowania obszar\u00f3w wznoszenia mas powietrznych. Dywergencja pola wiatru informuje o tym, czy linie pr\u0105du powietrza na danym obszarze rozbiegaj\u0105 si\u0119 lub zbiegaj\u0105, przy czym obszary rozbie\u017cno\u015bci linii pr\u0105du s\u0105 okre\u015blane jako obszary dywergencji, a zbie\u017cno\u015bci jako konwergencji. W przypadku rozbie\u017cno\u015bci linii pr\u0105du warto\u015b\u0107 wska\u017anika jest dodatnia, a w przypadku zbie\u017cno\u015bci ujemna. \u0141atwo sobie wyobrazi\u0107, \u017ce je\u015bli linie pr\u0105du powietrza rozbiegaj\u0105 si\u0119 w danym miejscu p\u0142aszczyzny poziomej, powietrze to musi nap\u0142ywa\u0107 z g\u00f3ry (taka sytuacja b\u0119dzie mie\u0107 miejsce w dolnej troposferze) lub z do\u0142u (w \u015brodkowej i g\u00f3rnej troposferze) i na odwr\u00f3t - w przypadku zbiegania si\u0119 linii, nadmiar powietrza b\u0119dzie si\u0119 unosi\u0142 (w dolnej troposferze) lub opada\u0142 (w \u015brodkowej i g\u00f3rnej troposferze). Tak wi\u0119c dywergencja jest jednym z element\u00f3w sk\u0142adaj\u0105cych si\u0119 na wyst\u0119powanie pionowych ruch\u00f3w powietrza w troposferze, a jej znak oraz poziom wyst\u0119powania w troposferze b\u0119dzie decydowa\u0142 o wznoszeniu lub opadaniu powietrza. Na uwadze trzeba mie\u0107 r\u00f3wnie\u017c to, \u017ce dywergentny przep\u0142yw powietrza wyst\u0119puje r\u00f3wnie\u017c na obszarach wzrostu pr\u0119dko\u015bci wiatru, w miejscu wyst\u0119powania r\u00f3wnoleg\u0142e przebiegaj\u0105cych linii pr\u0105du. W takiej samej sytuacji, na obszarach spadku pr\u0119dko\u015bci, ma miejsce przep\u0142yw konwergentny. Najistotniejsze znaczenie dla proces\u00f3w odpowiadaj\u0105cych za przebieg pogody na danym obszarze ma pole dywergencji w dolnej i \u015brodkowej troposferze. W przypadku dolnych partii troposfery (w szczeg\u00f3lno\u015bci warstwy granicznej) obszary dywergencji s\u0105 zwi\u0105zane z obszarami wy\u017c\u00f3w, wa\u0142\u00f3w i klin\u00f3w wy\u017cowych, gdzie wyst\u0119puje osiadanie, natomiast obszary konwergencji wi\u0105\u017c\u0105 si\u0119 z obszarami zatok, bruzd i o\u015brodk\u00f3w ni\u017cowych, gdzie wyst\u0119puje wznoszenie. W przypadku \u015brodkowych i g\u00f3rnych partii troposfery, obszary dywergencji wyst\u0119puj\u0105 w przedniej cz\u0119\u015bci dolin (zatok) i w tylnej cz\u0119\u015bci grzbiet\u00f3w fal g\u00f3rnych i s\u0105 zwi\u0105zane z rozleg\u0142ymi strefami wznoszenia mas powietrznych. Z kolei obszary konwergencji rozwijaj\u0105 si\u0119 na ty\u0142ach dolin fal g\u00f3rnych oraz w przednich cz\u0119\u015bciach grzbiet\u00f3w fal g\u00f3rnych, a ich po\u0142o\u017cenie odpowiada lokalizacji rozleg\u0142ych obszar\u00f3w osiadania powietrza. W wyidealizowanym modelu pr\u0105du strumieniowego o prostej osi r\u00f3wnoleg\u0142ej do jego d\u0142ugo\u015bci, dywergencja pola wiatru rozwija si\u0119 w jego lewym regionie wyj\u015bcia i w prawym regionie wej\u015bcia, z kolei konwergencja b\u0119dzie zwi\u0105zana z prawym regionem wyj\u015bcia oraz lewym regionem wej\u015bcia. W przedniej cz\u0119\u015bci zatok fal g\u00f3rnych, kt\u00f3re odpowiadaj\u0105 lewemu regionowi wyj\u015bcia powietrza z pr\u0105du strumieniowego, rozwijaj\u0105 si\u0119 cz\u0119sto ni\u017ce oraz rozleg\u0142e strefy zachmurzenia. Proces rozwoju ni\u017cu przy wyst\u0119powaniu silnej dywergencji w lewym regionie wyj\u015bcia powietrza z pr\u0105du strumieniowego mo\u017ce mie\u0107 gwa\u0142towny przebieg.\",\n  \"MTGHT\":\"Tendencja zmiany wysoko\u015bci geopotencjalnej wskazuje na regiony, do kt\u00f3rych przemieszczaj\u0105 si\u0119 ni\u017ce, zatoki, grzbiety i wy\u017ce g\u00f3rne. Mapy pola zmian wysoko\u015bci geopotencjalnej wska\u017c\u0105 r\u00f3wnie\u017c nasilanie si\u0119 lub s\u0142abni\u0119cie poszczeg\u00f3lnych uk\u0142ad\u00f3w barycznych wyst\u0119puj\u0105ce w ci\u0105gu ostatnich 3 godzin do danego terminu, je\u015bli pr\u0119dko\u015b\u0107 przemieszczania si\u0119 uk\u0142adu jest niewielka, a zmiana wysoko\u015bci geopotencjalniej w jego obr\u0119bie znaczna. Mapa mo\u017ce by\u0107 u\u017cywana pomocniczo do okre\u015blenia kierunk\u00f3w przemieszczania si\u0119 poszczeg\u00f3lnych uk\u0142ad\u00f3w oraz do zidentyfikowania ich etapu rozwoju. Dodatkowo wska\u017anik ten b\u0119dzie pomocny w okre\u015bleniu adwekcji termicznej w warstwie troposfery, kt\u00f3ra znajduje si\u0119 poni\u017cej powierzchni izobarycznej, dla kt\u00f3rej obliczono tendencj\u0119. Spadek wysoko\u015bci geopotencjalnej b\u0119dzie wtedy zwi\u0105zany z adwekcj\u0105 ch\u0142odu w tej\u017ce warstwie, z kolei wzrost z adwekcj\u0105 ciep\u0142a.\",\n  \"MTPRES_SFC\":\"Tendencja zmiany wysoko\u015bci geopotencjalnej wskazuje na regiony, do kt\u00f3rych przemieszczaj\u0105 si\u0119 ni\u017ce, zatoki, grzbiety i wy\u017ce g\u00f3rne. Ruch o\u015brodk\u00f3w barycznych jest zale\u017cny od kierunku i pr\u0119dko\u015bci wiatru na poziomie przenoszenia (znajduje si\u0119 on pomi\u0119dzy poziomami izobarycznymi 600 hPa i 700 hPa) oraz od warunk\u00f3w termicznych (ni\u017ce przemieszczaj\u0105 si\u0119 w kierunku obszar\u00f3w ciep\u0142ych, a wy\u017ce w stron\u0119 ch\u0142odu). Bardzo du\u017ce znaczenie dla ruchu o\u015brodk\u00f3w barycznych b\u0119dzie mia\u0142a ich faza rozwoju (na przyk\u0142ad m\u0142ode ni\u017ce poruszaj\u0105 si\u0119 szybko podczas gdy stare, wype\u0142niaj\u0105ce si\u0119 o\u015brodki przemieszczaj\u0105 si\u0119 powoli), a nawet kszta\u0142t izohips otaczaj\u0105cych obszar o\u015brodka niskiego ci\u015bnienia lub centrum wysokiego ci\u015bnienia. W przypadku o\u015brodka niskiego ci\u015bnienia o kszta\u0142cie kolistym o\u015brodek ten b\u0119dzie si\u0119 przemiesza\u0142 w stron\u0119 obszaru z najwi\u0119kszym spadkiem wysoko\u015bci geopotencjalnej, jednak w przypadku kszta\u0142tu eliptycznego b\u0119dzie przesuwa\u0142 si\u0119 w kierunku zgodnym z dwusieczn\u0105 k\u0105ta mi\u0119dzy d\u0142u\u017csz\u0105 osi\u0105 obszaru najni\u017cszych warto\u015bci ci\u015bnienia a prost\u0105 wyznaczaj\u0105c\u0105 kierunek gradientu izoalohipsometrycznego\/izalobarycznego (kierunkiem najszybszego spadku tendencji wysoko\u015bci geopotencjalnej lub ci\u015bnienia atmosferycznego wzgl\u0119dem odleg\u0142o\u015bci). Ponadto na p\u00f3\u0142kuli p\u00f3\u0142nocnej podczas swojej w\u0119dr\u00f3wki ni\u017c b\u0119dzie stopniowo skr\u0119ca\u0142 na lewo (przeciwnie do ruchu wskaz\u00f3wek zegara), a wy\u017c na prawo. \\n\\n Mapy pola zmian wysoko\u015bci geopotencjalnej wskazuj\u0105 r\u00f3wnie\u017c nasilanie si\u0119 lub s\u0142abni\u0119cie poszczeg\u00f3lnych uk\u0142ad\u00f3w barycznych, kt\u00f3re wyst\u0105pi\u0142o w ci\u0105gu ostatnich 3 godzin do danego terminu o ile tylko pr\u0119dko\u015b\u0107 przemieszczania si\u0119 tych uk\u0142ad\u00f3w jest niewielka, a zmiana wysoko\u015bci geopotencjalniej w ich obr\u0119bie znaczna. Mapa mo\u017ce by\u0107 zatem u\u017cywana pomocniczo do okre\u015blenia kierunk\u00f3w przemieszczania si\u0119 poszczeg\u00f3lnych uk\u0142ad\u00f3w barycznych oraz do zidentyfikowania ich etapu rozwoju. Dodatkowo wska\u017anik ten b\u0119dzie pomocny w okre\u015bleniu adwekcji termicznej w warstwie troposfery, kt\u00f3ra znajduje si\u0119 poni\u017cej powierzchni izobarycznej, dla kt\u00f3rej obliczono tendencj\u0119. Spadek wysoko\u015bci geopotencjalnej b\u0119dzie wtedy zwi\u0105zany z adwekcj\u0105 ch\u0142odu w tej\u017ce warstwie, z kolei wzrost z adwekcj\u0105 ciep\u0142a.\",\n  \"SHEAR\":\"Pionowy uskok (\u015bcinanie) wiatru to jeden z najwa\u017cniejszych wska\u017anik\u00f3w u\u017cywanych w prognozowaniu potencjalnej si\u0142y i trwa\u0142o\u015bci burz. Jest to r\u00f3\u017cnica pr\u0119dko\u015bci lub kierunku wiatru pomi\u0119dzy wy\u017cszym i ni\u017cszym poziomem w troposferze. Du\u017ce warto\u015bci tego parametru zwi\u0105zane s\u0105 z frontami atmosferycznymi, w rejonie kt\u00f3rych wyst\u0119puje znaczna warto\u015b\u0107 poziomego gradientu temperatury oraz silna adwekcja termiczna.  Pionowe uskoki wiatru s\u0105 \u017ar\u00f3d\u0142em wirowo\u015bci poziomej, na skutek kt\u00f3rej podczas wyst\u0119powania r\u00f3wnowagi warunkowo chwiejnej mo\u017ce rozwin\u0105\u0107 si\u0119 szczeg\u00f3lnie gro\u017ana forma uk\u0142adu konwekcyjnego \u2013 superkom\u00f3rka. Na mapach dost\u0119pnych w serwisie prezentowany jest rozk\u0142ad przestrzenny pionowego uskoku pr\u0119dko\u015bciowego wiatru z warstw 0-6 km, 0-3 km i 0-1 km nad powierzchni\u0105 ziemi. Szczeg\u00f3lnie istotne dla rozwoju superkom\u00f3rek burzowych jest wyst\u0119powanie du\u017cych (ok. 15 m\/s i wy\u017cszych) warto\u015bci pionowego uskoku pr\u0119dko\u015bciowego wiatru z warstwy 0-6 km. Ponadto du\u017ce warto\u015bci pionowego uskoku wiatru w warstwie 0-3 km sprzyjaj\u0105 rozwojowi liniowych uk\u0142ad\u00f3w konwekcyjnych cz\u0119sto przynosz\u0105cych bardzo silne porywy wiatru, a du\u017ce warto\u015bci pionowego pr\u0119dko\u015bciowego uskoku wiatru z warstwy 0-1 km sprzyjaj\u0105 tworzeniu si\u0119 tr\u0105b powietrznych. Og\u00f3lnie rzecz bior\u0105c, du\u017ce warto\u015bci pionowego \u015bcinania wiatru b\u0119d\u0105 sprzyja\u0107 wyst\u0119powaniu d\u0142ugotrwa\u0142ych i wewn\u0119trznie dobrze zorganizowanych uk\u0142ad\u00f3w konwekcyjnych przynosz\u0105cych burze i zjawiska im towarzysz\u0105ce o du\u017cym nat\u0119\u017ceniu. Du\u017ce warto\u015bci pionowego pr\u0119dko\u015bciowego uskoku wiatru b\u0119d\u0105 r\u00f3wnie\u017c przyczyn\u0105 wyst\u0119powania turbulencji, st\u0105d znajomo\u015b\u0107 jego aktualnych i prognozowanych warto\u015bci jest potrzebna przy planowaniu operacji lotniczych, zw\u0142aszcza w ramach lotnictwa og\u00f3lnego. \",\n  \"TW850\": \"Mapy topografii wzgl\u0119dnej (TW) to jedne z najcz\u0119\u015bciej u\u017cywanych przez synoptyk\u00f3w map prezentuj\u0105cych rozk\u0142ad warto\u015bci wska\u017anik\u00f3w. Mapy te przedstawiaj\u0105 odleg\u0142o\u015b\u0107 wyra\u017cona w gpdam (dekametrach geopotencjalnych) pomi\u0119dzy par\u0105 powierzchni izobarycznych (na przyk\u0142ad 500 i 850 hPa). Grubo\u015b\u0107 warstwy ograniczonej tymi powierzchniami jest funkcj\u0105 jej \u015bredniej temperatury. Im wy\u017csza temperatura, tym wi\u0119ksza b\u0119dzie grubo\u015b\u0107 warstwy. Z tego wzgl\u0119du izohipsy wykre\u015blone na mapie topografii wzgl\u0119dnej traktujemy jak izotermy, a miejsca ich zag\u0119szczenia b\u0119d\u0105 wskazywa\u0142y na orientacyjne po\u0142o\u017cenie front\u00f3w atmosferycznych. Ponadto mapy topografii wzgl\u0119dnej u\u017cywane s\u0105 w celu szybkiego zidentyfikowania w\u0142a\u015bciwo\u015bci mas powietrznych nap\u0142ywaj\u0105cych nad dany obszar oraz do okre\u015blenia kierunku i intensywno\u015bci adwekcji termicznej. Wzrost grubo\u015bci warstwy pomi\u0119dzy zadanymi powierzchniami b\u0119dzie oznacza\u0142 adwekcj\u0119 ciep\u0142a, za\u015b spadek adwekcj\u0119 ch\u0142odu. Na\u0142o\u017con\u0105 map\u0119 wzgl\u0119dnej topografii barycznej na map\u0119 topografii bezwzgl\u0119dnej nazywamy map\u0105 termobaryczn\u0105.\",\n  \"TW1000\": \"Mapy topografii wzgl\u0119dnej (TW) to jedne z najcz\u0119\u015bciej u\u017cywanych przez synoptyk\u00f3w map prezentuj\u0105cych rozk\u0142ad warto\u015bci wska\u017anik\u00f3w.  Mapy te przedstawiaj\u0105 odleg\u0142o\u015b\u0107 wyra\u017cona w gpdam (dekametrach geopotencjalnych) pomi\u0119dzy par\u0105 powierzchni izobarycznych (na przyk\u0142ad 500 i 850 hPa). Grubo\u015b\u0107 warstwy ograniczonej tymi powierzchniami jest funkcj\u0105 jej \u015bredniej temperatury. Im wy\u017csza temperatura, tym wi\u0119ksza b\u0119dzie grubo\u015b\u0107 warstwy. Z tego wzgl\u0119du izohipsy wykre\u015blone na mapie topografii wzgl\u0119dnej traktujemy jak izotermy, a miejsca ich zag\u0119szczenia b\u0119d\u0105 wskazywa\u0142y na orientacyjne po\u0142o\u017cenie front\u00f3w atmosferycznych. Ponadto mapy topografii wzgl\u0119dnej u\u017cywane s\u0105 w celu szybkiego zidentyfikowania w\u0142a\u015bciwo\u015bci mas powietrznych nap\u0142ywaj\u0105cych nad dany obszar oraz do okre\u015blenia kierunku i intensywno\u015bci adwekcji termicznej. Wzrost grubo\u015bci warstwy pomi\u0119dzy zadanymi powierzchniami b\u0119dzie oznacza\u0142 adwekcj\u0119 ciep\u0142a, za\u015b spadek adwekcj\u0119 ch\u0142odu. Na\u0142o\u017con\u0105 map\u0119 wzgl\u0119dnej topografii barycznej na map\u0119 topografii bezwzgl\u0119dnej nazywamy map\u0105 termobaryczn\u0105.\",\n  \"MTTMP\":\"Tendencja termiczna dla okre\u015blonej powierzchni izobarycznej informuje o intensywno\u015bci adwekcji termicznej i jej znaku, jak r\u00f3wnie\u017c zmian temperatury na skutek wyst\u0119powania proces\u00f3w adiabatycznych nad danym obszarem. Por\u00f3wnuj\u0105c wyniki uzyskane z r\u00f3\u017cnych poziom\u00f3w atmosfery (np. map\u0119 rozk\u0142adu tendencji termicznej na powierzchni izobarycznej 850 hPa z map\u0105 tendencji z powierzchni 500 hPa) mo\u017cemy oceni\u0107, czy troposfera staje si\u0119 chwiejna (na przyk\u0142ad na skutek wzrostu temperatury na powierzchni 925 hPa i jednoczesnego spadku na powierzchni 500 hPa) b\u0105d\u017a stabilna (na przyk\u0142ad na skutek spadku temperatury na powierzchni 925 hPa przy jednoczesnym wzro\u015bcie na powierzchni 500 hPa). Mapy obejmuj\u0105ce warstw\u0119 graniczn\u0105 troposfery (powierzchnie 850, 925 i 950 hPa) mog\u0105 by\u0107 przydatne w prognozowaniu zachmurzenia, mgie\u0142 i sytuacji ze smogiem w p\u00f3\u0142roczu ch\u0142odnym, zw\u0142aszcza podczas przej\u015bcia ciep\u0142ego frontu atmosferycznego lub zalegania ch\u0142odnego i wilgotnego powietrza w przypowierzchniowej warstwie troposfery. Przegl\u0105d map z dolnych poziom\u00f3w troposfery mo\u017ce r\u00f3wnie\u017c wspom\u00f3c prognozowanie zasi\u0119gu stref opad\u00f3w marzn\u0105cych.\",\n  \"TGRAD\":\"Spadek (pionowy gradient*) temperatury jest r\u00f3\u017cnic\u0105 temperatury pomi\u0119dzy powierzchniami izobarycznymi 500 a 850 hPa podzielon\u0105 przez r\u00f3\u017cnic\u0119 odleg\u0142o\u015bci mi\u0119dzy obydwiema powierzchniami. Wskazuje na tempo spadku temperatury wraz z wysoko\u015bci\u0105. Wska\u017anik ten umo\u017cliwia identyfikacje stanu r\u00f3wnowagi troposfery. Je\u015bli warto\u015b\u0107 pionowego gradientu temperatury jest wy\u017csza od warto\u015bci gradientu wilgotnoadiabatycznego, wtedy wyst\u0119puje r\u00f3wnowaga warunkowo chwiejna sprzyjaj\u0105ca powstawaniu stref burz i opad\u00f3w konwekcyjnych (przelotnych). Na og\u00f3\u0142 przyjmuje si\u0119, \u017ce r\u00f3wnowaga warunkowo chwiejna wyst\u0119puje przy pionowym gradiencie temperatury przekraczaj\u0105cym warto\u015b\u0107 0,6\u00b0C\/100 m, jednak w zale\u017cno\u015bci od pory roku i temperatury masy powietrza wyst\u0119puj\u0105cej w danym miejscu warto\u015b\u0107 ta mo\u017ce by\u0107 ni\u017csza (zw\u0142aszcza p\u00f3\u017an\u0105 wiosn\u0105 i latem) lub wy\u017csza (zw\u0142aszcza w miesi\u0105cach zimowych). Nale\u017cy pami\u0119ta\u0107, \u017ce w celu w\u0142a\u015bciwego zdiagnozowania potencja\u0142u do rozwoju chmur Cumulonimbus, stref opad\u00f3w konwekcyjnych i burz potrzebne s\u0105 jeszcze mapy prezentuj\u0105ce rozk\u0142ad przestrzenny wilgotno\u015bci wzgl\u0119dnej dla powierzchni izobarycznych w dolnej i \u015brodkowej troposferze, a tak\u017ce mapy pr\u0119dko\u015bci pionowej w dolnej i \u015brodkowej troposferze (opcjonalnie mapy linii pr\u0105du powietrza z przypowierzchniowej warstwy troposfery i mapy rozk\u0142adu adwekcji wirowo\u015bci z uwzgl\u0119dnieniem adwekcji termicznej dla \u015brodkowych partii troposfery). \\n\\n * w celu obliczenia pionowego gradient temperatury, od temperatury powierzchni izobarycznej 500 hPa powinni\u015bmy odj\u0105\u0107 temperatur\u0119 powierzchni 850 hPa, a nast\u0119pnie warto\u015b\u0107 t\u0119 podzieli\u0107 przez r\u00f3\u017cnic\u0119 odleg\u0142o\u015bci mi\u0119dzy obydwiema powierzchniami. Tak obliczone tempo zmiany temperatury wyjdzie ujemne, zgodne ze stanem rzeczywistym (temperatura wraz z wysoko\u015bci\u0105 oczywi\u015bcie spada, a sytuacje, w kt\u00f3rych ro\u015bnie, wyst\u0119puj\u0105 stosunkowo rzadko i dotycz\u0105 jedynie warstw troposfery o mi\u0105\u017cszo\u015bci zaledwie kilkuset metr\u00f3w, nie wp\u0142ywaj\u0105c w om\u00f3wionym przypadku na znak uzyskanej w wyniku oblicze\u0144 warto\u015bci). Z tego wynika, \u017ce spadek temperatury w pionie (ang. lapse rate) to nic innego jak pionowy gradient temperatury, lecz ze znakiem przeciwnym. W praktyce meteorologicznej upowszechni\u0142o si\u0119 stosowanie warto\u015bci pionowego spadku temperatury.\",\n};\n\n\/\/ Konfiguracja sufix\u00f3w dla obrazk\u00f3w standardowych modeli\nconst suffixConfig = {\n  \/\/ Nowe modele (ALARO, GFS, ICON-EU, IFS) nie maj\u0105 statystyk - tylko pojedyncze obrazy\n  \"default\": [],  \/\/ Brak statystyk dla nowych modeli\n  };\n\n\/\/ Funkcja wykrywaj\u0105ca urz\u0105dzenia mobilne\nfunction isMobileDevice() {\n  return \/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini\/i.test(navigator.userAgent) ||\n         (window.innerWidth <= 768);\n}\n\n\/\/ Funkcja do renderowania przycisk\u00f3w\nfunction renderButtons(containerId, items, onClick, activeItem = null, displayNames = null) {\n  const container = document.getElementById(containerId);\n  if (!container) {\n    return;\n  }\n  container.innerHTML = \"\";\n  \n  \/\/ Sprawd\u017a czy renderujemy przyciski modeli\n  const isModelButtons = containerId === \"models\";\n  \n  items.forEach(item => {\n    const btn = document.createElement(\"button\");\n    \n    \/\/ Ustaw data-value dla p\u00f3\u017aniejszego u\u017cycia\n    btn.dataset.value = item;\n    \n    \/\/ Je\u015bli istnieje przyjazna nazwa dla tego elementu, u\u017cyj jej\n    if (displayNames && displayNames[item]) {\n      btn.textContent = displayNames[item];\n    } else {\n      btn.textContent = item;\n    }\n    \n    \/\/ Ustaw klas\u0119 'active' dla aktywnego przycisku\n    if (activeItem && item === activeItem) {\n      btn.classList.add('active');\n    }\n    \n    btn.onclick = () => {\n      \/\/ Resetuj stan przy zmianie modelu\n      if (isModelButtons) {\n        \/\/ Wyczy\u015b\u0107 bufor obraz\u00f3w przy zmianie modelu\n        if (selectedModel !== item) {\n          resetStateForModelChange();\n        }\n      }\n      \n      \/\/ Usu\u0144 klas\u0119 'active' ze wszystkich przycisk\u00f3w w kontenerze\n      container.querySelectorAll('button').forEach(b => b.classList.remove('active'));\n      \/\/ Dodaj klas\u0119 'active' do klikni\u0119tego przycisku\n      btn.classList.add('active');\n      \n      \/\/ Wywo\u0142aj funkcj\u0119 onClick\n      onClick(item);\n    };\n    \n    container.appendChild(btn);\n  });\n}\n\n\/\/ Funkcja do renderowania list rozwijanych\nfunction renderSelect(selectId, items, onChange, displayNames = null) {\n  const select = document.getElementById(selectId);\n  select.innerHTML = \"\";\n  \n  \/\/ Dodanie domy\u015blnej opcji\n  const defaultOption = document.createElement(\"option\");\n  defaultOption.value = \"\";\n  defaultOption.textContent = \"-- Wybierz --\";\n  select.appendChild(defaultOption);\n  \n  \/\/ Dodanie opcji z listy\n  items.forEach(item => {\n    const option = document.createElement(\"option\");\n    option.value = item;\n    \n    \/\/ Je\u015bli istnieje przyjazna nazwa dla tego parametru, u\u017cyj jej\n    if (displayNames && displayNames[item]) {\n      option.textContent = displayNames[item];\n    } else {\n      option.textContent = item;\n    }\n    \n    \/\/ Ustawienie atrybutu dla parametr\u00f3w z poziomami ci\u015bnienia\n    if (selectId === \"paramsSelect\" && hasPressureLevels(item)) {\n      option.setAttribute(\"style\", \"color: rgba(86, 221, 208, 1) !important; font-weight: bold;\");\n    }\n    \n    select.appendChild(option);\n  });\n  \n  \/\/ Dodanie obs\u0142ugi zmiany\n  \/\/ Dla paramsSelect - zmiana tylko na Enter lub klikni\u0119cie myszk\u0105, nie na strza\u0142ki\n  if (selectId === \"paramsSelect\") {\n    let pendingValue = null;\n    let isSelectingWithMouse = false;\n    \n    \/\/ Wykryj klikni\u0119cie myszk\u0105 na opcji\n    select.addEventListener(\"mousedown\", function(e) {\n      isSelectingWithMouse = true;\n    });\n    \n    \/\/ Obs\u0142uga zmiany\n    select.onchange = function(e) {\n      \/\/ Je\u015bli to klikni\u0119cie myszk\u0105, wywo\u0142aj onChange\n      if (isSelectingWithMouse && this.value) {\n        onChange(this.value);\n        this.size = 0;\n        this.blur();\n        isSelectingWithMouse = false;\n        return;\n      }\n      \n      \/\/ W przeciwnym razie (strza\u0142ki) tylko zapisz warto\u015b\u0107\n      e.preventDefault();\n      pendingValue = this.value;\n      isSelectingWithMouse = false;\n    };\n    \n    \/\/ Obs\u0142uga Enter dla zatwierdzenia\n    select.addEventListener(\"keydown\", function(e) {\n      if (e.key === \"Enter\" && this.value) {\n        e.preventDefault();\n        onChange(this.value);\n        \/\/ Zamknij list\u0119 natychmiast\n        this.size = 0;\n        this.blur();\n      }\n    });\n  } else {\n    \/\/ Dla pozosta\u0142ych select - standardowa obs\u0142uga\n    select.onchange = function() {\n      if (this.value) {\n        onChange(this.value);\n      }\n    };\n  }\n}\n\n\/\/ Definiowanie domy\u015blnych warto\u015bci dla ka\u017cdego modelu\nconst defaultSettings = {\n  \"ALARO\": {\n    term: \"00\",\n    param: \"WWIND\",\n    hour: \"0\",\n    stat: null  \/\/ Nowe modele nie maj\u0105 statystyk\n  },\n  \"GFS\": {\n    term: \"00\",\n    param: \"WWIND\",\n    hour: \"0\",\n    stat: null  \/\/ Nowe modele nie maj\u0105 statystyk\n  },\n  \"ICON-EU\": {\n    term: \"00\",\n    param: \"WWIND\",\n    hour: \"0\",\n    stat: null  \/\/ Nowe modele nie maj\u0105 statystyk\n  },\n  \"IFS\": {\n    term: \"00\",\n    param: \"WWIND\",\n    hour: \"0\",\n    stat: null  \/\/ Nowe modele nie maj\u0105 statystyk\n  }\n};\n\n\/\/ Inicjalizacja interfejsu z domy\u015blnymi warto\u015bciami\nfunction initializeInterface() {\n  \/\/ Ustawienie domy\u015blnego modelu\n  selectedModel = \"ALARO\";\n  \n  \/\/ Ustawienie domy\u015blnych warto\u015bci dla wybranego modelu\n  selectedTerm = defaultSettings[selectedModel].term;\n  selectedParam = defaultSettings[selectedModel].param;\n  selectedHour = defaultSettings[selectedModel].hour;\n  selectedStat = defaultSettings[selectedModel].stat;\n  \n  \/\/ Aktualizacja etykiety terminu\n  updateTermLabel();\n  selectedPressureLevel = null;\n  \n  \/\/ Inicjalizacja przycisk\u00f3w modeli z aktywnym modelem\n  renderButtons(\"models\", models, (model) => {\n    const prevModel = selectedModel;\n    selectedModel = model;\n    \n    \/\/ Zapisz poprzednie warto\u015bci przed zmian\u0105\n    const prevTerm = selectedTerm;\n    const prevStat = selectedStat;\n    const prevHour = selectedHour;\n    \n    \/\/ Sprawd\u017a czy zmieniamy typ modelu\n    \/\/ Nowe modele s\u0105 deterministycznymi modelami bez statystyk\n    \n    \/\/ Ustawienie domy\u015blnych warto\u015bci dla wybranego modelu\n    selectedParam = defaultSettings[model].param;\n    \n    \/\/ Zachowaj godzin\u0119 je\u015bli mo\u017cliwe\n    if (prevHour) {\n      selectedHour = prevHour;\n    } else {\n      selectedHour = defaultSettings[model].hour;\n    }\n    \n    \/\/ Zachowaj termin je\u015bli mo\u017cliwe\n    if (true) {\n      if (prevTerm && termsDefault.includes(prevTerm)) {\n        selectedTerm = prevTerm;\n      } else {\n        selectedTerm = defaultSettings[model].term;\n      }\n      \n      \/\/ Zachowaj statystyk\u0119 dla zwyk\u0142ych modeli\n      const availableStats = suffixConfig[selectedParam] || suffixConfig[\"default\"];\n      if (prevStat && availableStats.includes(prevStat)) {\n        selectedStat = prevStat;\n      } else {\n        selectedStat = defaultSettings[model].stat;\n      }\n    } else {\n      selectedTerm = defaultSettings[model].term;\n      selectedStat = null; \/\/ Modele podsumowania nie maj\u0105 statystyk\n    }\n    \n    \/\/ Aktualizacja etykiety terminu\n    updateTermLabel();\n    updateTermButtons();\n  }, selectedModel, modelDisplayNames);\n  \n  \/\/ Inicjalizacja pozosta\u0142ych element\u00f3w\n  updateTermButtons();\n}\n\n\/\/ Wszystkie nowe modele (ALARO, GFS, ICON-EU, IFS) s\u0105 standardowymi modelami z terminami i statystykami\n\n\/\/ Dodajemy style CSS dla komunikatu \u0142adowania\nconst styleElement = document.createElement('style');\nstyleElement.textContent = `\n  .loading-hours {\n    padding: 10px;\n    text-align: center;\n    color: #666;\n    font-style: italic;\n  }\n`;\ndocument.head.appendChild(styleElement);\n\n\/\/ Funkcja do sprawdzenia czy plik istnieje\n\/\/ Cache dla sprawdzania obrazk\u00f3w\nconst imageExistsCache = {};\n\nfunction checkImageExists(url) {\n  \/\/ Sprawd\u017a cache\n  if (imageExistsCache.hasOwnProperty(url)) {\n    return Promise.resolve(imageExistsCache[url]);\n  }\n  \n  \/\/ U\u017cyj fetch z HEAD request - szybsze ni\u017c \u0142adowanie ca\u0142ego obrazka\n  return fetch(url, { \n    method: 'HEAD',\n    cache: 'no-cache'\n  })\n    .then(response => {\n      const exists = response.ok;\n      imageExistsCache[url] = exists;\n      return exists;\n    })\n    .catch(error => {\n      \/\/ Je\u015bli fetch nie dzia\u0142a (np. CORS), u\u017cyj fallback z Image\n      return new Promise((resolve) => {\n        const img = new Image();\n        let timer = null;\n        \n        timer = setTimeout(() => {\n          img.onload = img.onerror = null;\n          imageExistsCache[url] = false;\n          resolve(false);\n        }, 800);\n        \n        img.onload = () => {\n          clearTimeout(timer);\n          imageExistsCache[url] = true;\n          resolve(true);\n        };\n        \n        img.onerror = () => {\n          clearTimeout(timer);\n          imageExistsCache[url] = false;\n          resolve(false);\n        };\n        \n        img.src = url;\n      });\n    });\n}\n\n\/\/ Funkcja do dynamicznego ustalania dost\u0119pnych godzin dla parametr\u00f3w\n\/\/ Zmienna do \u015bledzenia aktualnie sprawdzanych dost\u0119pno\u015bci godzin\n\/\/ Aktualnie wybrane opcje\nlet selectedModel = \"ALARO\";   \/\/ Domy\u015blny model (nowe modele)\nlet selectedTerm = \"00\";       \/\/ Domy\u015blny termin startu\nlet selectedParam = \"WWIND\";   \/\/ Domy\u015blny parametr dla nowych modeli\nlet selectedHour = \"00\";        \/\/ Domy\u015blny termin prognozy\nlet selectedStat = null;        \/\/ Nowe modele nie maj\u0105 statystyk\nlet selectedPressureLevel = null;\n\n\/\/ Zmienna do obs\u0142ugi animacji \u0142adowania\nlet loaderTimeout = null;\n\n\/\/ Funkcja pod\u015bwietlaj\u0105ca aktywny przycisk\nfunction setActiveButton(container, activeValue) {\n  if (activeValue === undefined || activeValue === null) return;\n  \n  \/\/ Konwertuj activeValue na string\n  let activeValueStr = String(activeValue).trim();\n  \n  \/\/ Usu\u0144 suffix \"h\" je\u015bli istnieje (dla przycisku godziny)\n  if (activeValueStr.endsWith('h')) {\n    activeValueStr = activeValueStr.substring(0, activeValueStr.length - 1);\n  }\n  \n  const buttons = document.querySelectorAll(`#${container} button`);\n  buttons.forEach(btn => {\n    let btnText = String(btn.textContent).trim();\n    \n    \/\/ Usu\u0144 suffix \"h\" je\u015bli istnieje (dla przycisku godziny)\n    if (btnText.endsWith('h')) {\n      btnText = btnText.substring(0, btnText.length - 1);\n    }\n    \n    \/\/ Formatujemy obie warto\u015bci z wiod\u0105cym zerem dla pewno\u015bci\n    const formattedActiveValue = String(activeValueStr).padStart(2, \"0\");\n    const formattedBtnValue = String(btnText).padStart(2, \"0\");\n    \n    \/\/ Por\u00f3wnujemy sformatowane warto\u015bci\n    if (formattedBtnValue === formattedActiveValue) {\n      btn.classList.add('active');\n    } else {\n      btn.classList.remove('active');\n    }\n  });\n}\n\n\/\/ 1. Renderowanie modeli\nrenderButtons(\"models\", models, (model) => {\n  \/\/ WA\u017bNE: Wyczy\u015b\u0107 kolejk\u0119 preloadingu NATYCHMIAST na pocz\u0105tku\n  \/\/ aby zatrzyma\u0107 \u0142adowanie obrazk\u00f3w ze starego modelu\n  resetPreloading();\n  \n  const prevModel = selectedModel;\n  let prevTerm = selectedTerm;\n  let prevParam = isParameterWithPressureLevel(selectedParam) ? getBaseParam(selectedParam) : selectedParam;\n  let prevHour = selectedHour;\n  let prevStat = selectedStat;\n  let prevPressureLevel = selectedPressureLevel;\n  \n  selectedModel = model;\n  \n  setActiveButton(\"models\", model);\n  \n  \/\/ Wszystkie nowe modele s\u0105 standardowymi modelami z terminami\n  \/\/ Zachowanie terminu startu\n  if (prevTerm && termsDefault.includes(prevTerm)) {\n    selectedTerm = prevTerm;\n  } else {\n    selectedTerm = defaultSettings[model].term;\n  }\n  \n  \/\/ Sprawd\u017a czy poprzedni parametr jest dost\u0119pny w nowym modelu\n  \/\/ Pobierz list\u0119 parametr\u00f3w dla nowego modelu\n  let availableParams = paramsForModels[model] || [];\n  \n  \/\/ Filtruj parametry (usu\u0144 te z poziomem ci\u015bnienia)\n  let filteredParams = [];\n  let baseParamsAdded = new Set();\n  availableParams.forEach(param => {\n    if (\/^[A-Z]$\/.test(param)) {\n      if (!baseParamsAdded.has(param)) {\n        filteredParams.push(param);\n        baseParamsAdded.add(param);\n      }\n    } else if (isParameterWithPressureLevel(param)) {\n      const baseParam = getBaseParam(param);\n      if (!baseParamsAdded.has(baseParam)) {\n        filteredParams.push(baseParam);\n        baseParamsAdded.add(baseParam);\n      }\n    } else {\n      filteredParams.push(param);\n    }\n  });\n  \n  \/\/ Je\u015bli poprzedni parametr jest dost\u0119pny w nowym modelu, zachowaj go\n  if (prevParam && filteredParams.includes(prevParam)) {\n    selectedParam = prevParam;\n  } else {\n    selectedParam = defaultSettings[model].param;\n  }\n  \n  \/\/ Okre\u015bl dost\u0119pne godziny dla nowego modelu\n  let availableHoursTemp = [];\n  if (forecastHours[model] && forecastHours[model][selectedTerm]) {\n    availableHoursTemp = forecastHours[model][selectedTerm];\n  } else {\n    availableHoursTemp = forecastHours[\"default\"];\n  }\n  \n  \/\/ Zachowaj godzin\u0119 je\u015bli dost\u0119pna\n  if (prevHour) {\n    const prevHourInt = parseInt(String(prevHour), 10);\n    const isHourAvailable = availableHoursTemp.includes(prevHourInt);\n    \n    if (isHourAvailable) {\n      selectedHour = String(prevHour).padStart(2, \"0\");\n    } else {\n      selectedHour = String(defaultSettings[model].hour).padStart(2, \"0\");\n    }\n  } else {\n    selectedHour = String(defaultSettings[model].hour).padStart(2, \"0\");\n  }\n  \n  \/\/ Zachowaj statystyk\u0119\n  const availableStats = suffixConfig[selectedParam] || suffixConfig[\"default\"];\n  \n  if (prevStat && availableStats.includes(prevStat)) {\n    selectedStat = prevStat;\n  } else {\n    selectedStat = defaultSettings[model].stat;\n  }\n  \n  document.getElementById(\"images\").innerHTML = \"\";\n  document.getElementById(\"hours\").innerHTML = \"\";\n  document.getElementById(\"statsSelect\").innerHTML = \"\";\n  document.getElementById(\"statsSection\").style.display = \"block\";\n  document.getElementById(\"pressureLevelSliderSection\").style.display = \"none\";\n  \n  \/\/ Wszystkie nowe modele u\u017cywaj\u0105 termin\u00f3w startu (standardowe terminy)\n  \/\/ Renderowanie termin\u00f3w, parametr\u00f3w, statystyk i godzin\n  renderTermButtons();\n  renderParams();\n  renderStats();\n  renderForecastHours();\n}, selectedModel, modelDisplayNames); \/\/ Zamkni\u0119cie renderButtons(\"models\", ...)\n\n\/\/ Zdefiniowanie poziom\u00f3w ci\u015bnienia (od najni\u017cszego do najwy\u017cszego)\nconst pressureLevels = [\"950\", \"925\", \"850\", \"700\", \"500\", \"300\"];\n\n\/\/ Funkcja do pobierania dost\u0119pnych poziom\u00f3w ci\u015bnienia dla danego parametru\nfunction getAvailablePressureLevels(param) {\n  if (!param || !selectedModel) return [];\n  \n  const params = paramsForModels[selectedModel] || [];\n  const levels = [];\n  \n  \/\/ Znajd\u017a wszystkie warianty tego parametru z _LVL_\n  params.forEach(p => {\n    if (p.startsWith(param + \"_LVL_\")) {\n      \/\/ Wyci\u0105gnij numer poziomu (np. z \"WWIND_LVL_950\" -> \"950\")\n      const level = p.split(\"_LVL_\")[1];\n      if (level && !levels.includes(level)) {\n        levels.push(level);\n      }\n    }\n  });\n  \n  \/\/ Sortuj od najwy\u017cszego (1000) do najni\u017cszego (100)\n  levels.sort((a, b) => parseInt(b) - parseInt(a));\n  \n  return levels;\n}\n\n\/\/ Funkcja do aktualizacji suwaka poziom\u00f3w ci\u015bnienia dla wybranego parametru\nfunction updatePressureLevelSlider(param) {\n  const availableLevels = getAvailablePressureLevels(param);\n  const isMobile = isMobileDevice();\n  \n  \/\/ Ukryj suwak dla TGRAD (gradient temperatury) i TW850 (topografia wzgl\u0119dna 850), ale zachowaj poziom\n  if (param === \"TGRAD\" || param === \"TW850\") {\n    document.getElementById(\"pressureLevelSliderSection\").style.display = \"none\";\n    \/\/ Ustaw poziom, je\u015bli istnieje\n    if (availableLevels.length > 0) {\n      selectedPressureLevel = availableLevels[0];\n    }\n    return;\n  }\n  \n  if (availableLevels.length === 0) {\n    document.getElementById(\"pressureLevelSliderSection\").style.display = \"none\";\n    selectedPressureLevel = null;\n    return;\n  }\n  \n  \/\/ Poka\u017c sekcj\u0119\n  document.getElementById(\"pressureLevelSliderSection\").style.display = \"block\";\n  \n  \/\/ Na urz\u0105dzeniach mobilnych: ukryj slider, poka\u017c przyciski\n  const sliderContainer = document.getElementById(\"verticalSliderContainer\");\n  const buttonsContainer = document.getElementById(\"pressureLevelButtons\");\n  \n  if (isMobile) {\n    sliderContainer.style.display = \"none\";\n    buttonsContainer.style.display = \"block\";\n    \n    \/\/ Renderuj przyciski poziom\u00f3w ci\u015bnienia\n    renderPressureLevelButtons(availableLevels, param);\n    return;\n  } else {\n    sliderContainer.style.display = \"block\";\n    buttonsContainer.style.display = \"none\";\n  }\n  \n  \/\/ Zaktualizuj slider: max = liczba poziom\u00f3w - 1\n  const slider = document.getElementById(\"pressureLevelSlider\");\n  slider.max = availableLevels.length - 1;\n  \n  \/\/ Zachowaj poprzedni poziom je\u015bli jest dost\u0119pny, w przeciwnym razie ustaw pierwszy\n  let selectedIndex = 0;\n  if (selectedPressureLevel && availableLevels.includes(selectedPressureLevel)) {\n    selectedIndex = availableLevels.indexOf(selectedPressureLevel);\n  } else {\n    selectedPressureLevel = availableLevels[0];\n  }\n  \n  slider.value = selectedIndex;\n  \n  \/\/ Formatowanie warto\u015bci w zale\u017cno\u015bci od parametru\n  if (param === \"SHEAR\") {\n    \/\/ Dla uskoku wiatru: usu\u0144 \"0_\" i dodaj jednostk\u0119 \"m\"\n    const displayValue = selectedPressureLevel.replace(\/^0_\/, '');\n    document.getElementById(\"pressureValue\").textContent = `${displayValue} m`;\n  } else if (param === \"TW1000\") {\n    \/\/ Dla topografii wzgl\u0119dnej 1000: usu\u0144 prefiks \"1000_\"\n    const displayValue = selectedPressureLevel.replace(\/^1000_\/, '');\n    document.getElementById(\"pressureValue\").textContent = `${displayValue} hPa`;\n  } else {\n    document.getElementById(\"pressureValue\").textContent = `${selectedPressureLevel} hPa`;\n  }\n  \n  \/\/ Zaktualizuj etykiety\n  const labelsContainer = document.querySelector(\".pressure-labels\");\n  labelsContainer.innerHTML = \"\";\n  \n  \/\/ Odwr\u00f3\u0107 kolejno\u015b\u0107 poziom\u00f3w dla wy\u015bwietlania - najni\u017csze ci\u015bnienie na g\u00f3rze\n  const reversedLevels = [...availableLevels].reverse();\n  \n  reversedLevels.forEach((level, index) => {\n    const label = document.createElement(\"span\");\n    label.className = \"pressure-label\";\n    \/\/ Ta linijka oblicza pozycj\u0119 ka\u017cdej etykiety:\n    const position = 100 - (index \/ (reversedLevels.length - 1)) * 100;\n    label.style.bottom = `calc(${position}%)`; \/\/ Usuni\u0119to dodatkowy offset\n    \n    \/\/ Formatowanie etykiet w zale\u017cno\u015bci od parametru\n    if (param === \"SHEAR\") {\n      \/\/ Dla uskoku wiatru: usu\u0144 \"0_\" i dodaj jednostk\u0119 \"m\"\n      const displayValue = level.replace(\/^0_\/, '');\n      label.textContent = `${displayValue} m`;\n    } else if (param === \"TW1000\") {\n      \/\/ Dla topografii wzgl\u0119dnej 1000: usu\u0144 prefiks \"1000_\"\n      const displayValue = level.replace(\/^1000_\/, '');\n      label.textContent = `${displayValue} hPa`;\n    } else {\n      label.textContent = `${level} hPa`;\n    }\n    \n    labelsContainer.appendChild(label);\n  });\n  \n  return availableLevels;\n}\n\n\/\/ Funkcja renderuj\u0105ca przyciski poziom\u00f3w ci\u015bnienia dla urz\u0105dze\u0144 mobilnych\nfunction renderPressureLevelButtons(levels, param) {\n  const container = document.getElementById(\"pressureLevelButtons\");\n  container.innerHTML = \"\";\n  \n  \/\/ Zachowaj poprzedni poziom je\u015bli jest dost\u0119pny, w przeciwnym razie ustaw pierwszy\n  if (!selectedPressureLevel || !levels.includes(selectedPressureLevel)) {\n    selectedPressureLevel = levels[0];\n  }\n  \n  levels.forEach(level => {\n    const btn = document.createElement(\"button\");\n    btn.dataset.value = level;\n    \n    \/\/ Formatowanie tekstu przycisku w zale\u017cno\u015bci od parametru\n    if (param === \"SHEAR\") {\n      const displayValue = level.replace(\/^0_\/, '');\n      btn.textContent = `${displayValue} m`;\n    } else if (param === \"TW1000\") {\n      const displayValue = level.replace(\/^1000_\/, '');\n      btn.textContent = `${displayValue} hPa`;\n    } else {\n      btn.textContent = `${level} hPa`;\n    }\n    \n    \/\/ Zaznacz aktywny przycisk\n    if (level === selectedPressureLevel) {\n      btn.classList.add(\"active\");\n    }\n    \n    \/\/ Obs\u0142uga klikni\u0119cia\n    btn.onclick = function() {\n      selectedPressureLevel = level;\n      \n      \/\/ Zaktualizuj wy\u015bwietlan\u0105 warto\u015b\u0107\n      if (param === \"SHEAR\") {\n        const displayValue = level.replace(\/^0_\/, '');\n        document.getElementById(\"pressureValue\").textContent = `${displayValue} m`;\n      } else if (param === \"TW1000\") {\n        const displayValue = level.replace(\/^1000_\/, '');\n        document.getElementById(\"pressureValue\").textContent = `${displayValue} hPa`;\n      } else {\n        document.getElementById(\"pressureValue\").textContent = `${level} hPa`;\n      }\n      \n      \/\/ Zaznacz aktywny przycisk\n      container.querySelectorAll(\"button\").forEach(b => b.classList.remove(\"active\"));\n      this.classList.add(\"active\");\n      \n      \/\/ Za\u0142aduj obrazek\n      showImages();\n      preloadForecastImages();\n    };\n    \n    container.appendChild(btn);\n  });\n  \n  \/\/ Zaktualizuj wy\u015bwietlan\u0105 warto\u015b\u0107\n  if (param === \"SHEAR\") {\n    const displayValue = selectedPressureLevel.replace(\/^0_\/, '');\n    document.getElementById(\"pressureValue\").textContent = `${displayValue} m`;\n  } else if (param === \"TW1000\") {\n    const displayValue = selectedPressureLevel.replace(\/^1000_\/, '');\n    document.getElementById(\"pressureValue\").textContent = `${displayValue} hPa`;\n  } else {\n    document.getElementById(\"pressureValue\").textContent = `${selectedPressureLevel} hPa`;\n  }\n}\n\n\/\/ Sprawdzenie czy parametr ma warianty poziom\u00f3w ci\u015bnienia\nfunction hasPressureLevels(param) {\n  \/\/ Sprawdza czy jest to parametr bazowy maj\u0105cy warianty z poziomami ci\u015bnienia\n  \/\/ Parametry bazowe to te bez sufiksu _LVL_XXX\n  const baseParams = [\"WWIND\", \"WIND\", \"VOR\", \"TTD\", \"TEMP\", \"THETA\", \"RH\", \"SH\", \"DIV\", \"MTGHT\", \"SHEAR\", \"MTTMP\", \"TGRAD\", \"TW1000\"];\n  return baseParams.includes(param) && !param.includes(\"_LVL_\");\n}\n\n\/\/ Sprawdzenie czy parametr jest z poziomem ci\u015bnienia\nfunction isParameterWithPressureLevel(param) {\n  \/\/ Sprawdza czy parametr ma poziom ci\u015bnienia (format: PARAM_LVL_XXX)\n  return param.includes(\"_LVL_\");\n}\n\n\/\/ Funkcja do ekstrakcji podstawowego parametru bez poziomu ci\u015bnienia\nfunction getBaseParam(param) {\n  \/\/ Zwraca cz\u0119\u015b\u0107 parametru przed _LVL_ (np. WWIND_LVL_300 \u2192 WWIND)\n  return param.split(\"_LVL_\")[0];\n}\n\n\/\/ 2. Renderowanie parametr\u00f3w\nfunction renderParams() {\n  if (!selectedModel) return;\n  \n  \/\/ Nowe modele nie maj\u0105 statystyk - ukryj sekcj\u0119\n  document.getElementById(\"statsSection\").style.display = \"none\";\n  \n  let params = [];\n  \n  \/\/ Pobieranie parametr\u00f3w dla wybranego modelu\n  params = paramsForModels[selectedModel] || [];\n  \n  \/\/ Zapami\u0119tanie aktualnie wybranego parametru\n  const currentParam = selectedParam;\n  \n  \/\/ Filtrowanie parametr\u00f3w, aby usun\u0105\u0107 te z poziomem ci\u015bnienia, ale zachowa\u0107 parametr podstawowy\n  let filteredParams = [];\n  let baseParamsAdded = new Set();\n  \n  params.forEach(param => {\n    \/\/ Dla jednoliterowych parametr\u00f3w (A, B, T, itp.) - zawsze traktujemy jako parametry z poziomami\n    if (\/^[A-Z]$\/.test(param)) {\n      if (!baseParamsAdded.has(param)) {\n        filteredParams.push(param);\n        baseParamsAdded.add(param);\n      }\n    }\n    \/\/ Je\u015bli to jest parametr z poziomem ci\u015bnienia (np. WWIND_LVL_300, TEMP_LVL_850)\n    else if (isParameterWithPressureLevel(param)) {\n      \/\/ Pobieranie podstawowego parametru\n      const baseParam = getBaseParam(param);\n      \/\/ dodawanie\n      if (!baseParamsAdded.has(baseParam)) {\n        filteredParams.push(baseParam);\n        baseParamsAdded.add(baseParam);\n      }\n    } else {\n      \/\/ Dla parametr\u00f3w bez poziomu ci\u015bnienia, dodajemy je tylko je\u015bli nie by\u0142y ju\u017c dodane\n      if (!baseParamsAdded.has(param)) {\n        filteredParams.push(param);\n        baseParamsAdded.add(param);\n      }\n    }\n  });\n  \n  renderSelect(\"paramsSelect\", filteredParams, (param) => {\n    \/\/ WA\u017bNE: Wyczy\u015b\u0107 kolejk\u0119 preloadingu NATYCHMIAST na pocz\u0105tku\n    \/\/ aby zatrzyma\u0107 \u0142adowanie obrazk\u00f3w ze starego parametru\n    resetPreloading();\n    \n    \/\/ Resetowanie wszystkich flag wykonywania przy zmianie parametru\n    isShowImagesRunning = false;\n    isShowImagesRunningTimestamp = 0;\n    isRenderingForecastHours = false;\n    isRenderingForecastHoursTimestamp = 0;\n    isPreloadingImagesTimestamp = 0;\n    \n    \/\/ Zapami\u0119tanie poprzednich warto\u015bci terminu, statystyki i poziomu ci\u015bnienia\n    let prevHour = selectedHour;\n    let prevStat = selectedStat;\n    let prevPressureLevel = selectedPressureLevel;\n    \n    selectedParam = param;\n    \n    \/\/ Nie resetujemy wcze\u015bniej wybranego terminu prognozy i statystyki\n    \/\/ je\u015bli zmieniamy tylko parametr\n    \n    document.getElementById(\"images\").innerHTML = \"\";\n    document.getElementById(\"hours\").innerHTML = \"\";\n    \n    \/\/ Zaktualizuj suwak poziom\u00f3w ci\u015bnienia dla wybranego parametru\n    updatePressureLevelSlider(param);\n    \n    \/\/ Nowe modele nie maj\u0105 statystyk - ukryj sekcj\u0119\n    document.getElementById(\"statsSection\").style.display = \"none\";\n    \n    \/\/ Dla wszystkich modeli pokazujemy wyb\u00f3r terminu prognozy\n    renderForecastHours();\n    \/\/ Wst\u0119pne wczytanie obrazk\u00f3w\n    preloadForecastImages();\n    \n    \/\/ Sprawdzanie czy poprzednia statystyka jest dost\u0119pna dla nowego parametru\n    const availableStats = suffixConfig[param] || suffixConfig[\"default\"];\n    \n    \/\/ Dla nowych modeli (bez statystyk) ustaw selectedStat na null\n    if (availableStats.length === 0) {\n      selectedStat = null;\n    } else {\n      \/\/ Dla modeli ze statystykami ustaw domy\u015blnie na 'mean'\n      selectedStat = \"mean\";\n    }\n    \n    \/\/ Zachowaj selectedHour przed wywo\u0142aniem renderStatistics()\n    const preservedHour = selectedHour;\n    renderStatistics();\n    \n    \/\/ Przywr\u00f3\u0107 zachowan\u0105 godzin\u0119 je\u015bli zosta\u0142a zmieniona\n    if (preservedHour && selectedHour !== preservedHour) {\n      selectedHour = preservedHour;\n    }\n    \n    \/\/ Od\u015bwie\u017c widok pe\u0142noekranowy je\u015bli jest aktywny\n    refreshFullscreen();\n  }, parameterDisplayNames);\n  \n  \/\/ Po zako\u0144czeniu renderowania parametr\u00f3w, ustaw warto\u015b\u0107 select'a\n  if (currentParam && filteredParams.includes(currentParam)) {\n    const paramsSelect = document.getElementById(\"paramsSelect\");\n    paramsSelect.value = currentParam;\n  } else if (selectedParam && filteredParams.includes(selectedParam)) {\n    const paramsSelect = document.getElementById(\"paramsSelect\");\n    paramsSelect.value = selectedParam;\n  } else {\n    \/\/ Je\u015bli nie mamy wybranego parametru, wybierz domy\u015blny dla danego modelu\n    if (defaultSettings[selectedModel] && defaultSettings[selectedModel].param && filteredParams.includes(defaultSettings[selectedModel].param)) {\n      const paramsSelect = document.getElementById(\"paramsSelect\");\n      paramsSelect.value = defaultSettings[selectedModel].param;\n      selectedParam = defaultSettings[selectedModel].param;\n    }\n  }\n  \n  \/\/ Dla wszystkich modeli wywo\u0142aj renderStatistics()\n  renderStatistics();\n  \n  \/\/ Zaktualizuj suwak poziom\u00f3w ci\u015bnienia dla wybranego parametru\n  if (selectedParam) {\n    updatePressureLevelSlider(selectedParam);\n  }\n}\n\n\/\/ 3. Renderowanie statystyk (tylko dla standardowych modeli)\nfunction renderStatistics() {\n  \/\/ Wszystkie nowe modele maj\u0105 statystyki\n  if (!selectedParam) return;\n  \n  \/\/ Pobieranie dost\u0119pnych statystyk dla wybranego parametru\n  const stats = suffixConfig[selectedParam] || suffixConfig[\"default\"];\n  \n  \/\/ Nowe modele nie maj\u0105 statystyk - pusta tablica\n  if (stats.length === 0) {\n    selectedStat = null;\n    \/\/ Ukrycie sekcji wyboru statystyk\n    document.getElementById(\"statsSection\").style.display = \"none\";\n    \n    \/\/ Zaktualizuj suwak poziom\u00f3w ci\u015bnienia\n    updatePressureLevelSlider(selectedParam);\n    \n    \/\/ Bezpo\u015brednie przej\u015bcie do wyboru terminu prognozy\n    renderForecastHours();\n    \/\/ Wst\u0119pne wczytanie obrazk\u00f3w\n    preloadForecastImages();\n    return;\n  }\n  \n  \/\/ Je\u015bli jest tylko jedna statystyka (mean) - wybierz j\u0105 automatycznie\n  if (stats.length === 1 && stats[0] === \"mean\") {\n    selectedStat = \"mean\";\n    \/\/ Ukrycie sekcji wyboru statystyk, gdy\u017c nie ma co wybiera\u0107\n    document.getElementById(\"statsSection\").style.display = \"none\";\n    \/\/ Bezpo\u015brednie przej\u015bcie do wyboru terminu prognozy\n    renderForecastHours();\n    \/\/ Wst\u0119pne wczytanie obrazk\u00f3w po automatycznym wyborze statystyki\n    preloadForecastImages();\n    return;\n  }\n  \n  \/\/ Je\u015bli jest wi\u0119cej statystyk, poka\u017c list\u0119 wyboru\n  document.getElementById(\"statsSection\").style.display = \"block\";\n  \n  \/\/ Zapami\u0119taj aktualnie wybran\u0105 statystyk\u0119\n  const currentStat = selectedStat;\n  \n  renderSelect(\"statsSelect\", stats, (stat) => {\n    \/\/ Resetowanie wszystkich flag wykonywania przy zmianie statystyki\n    isShowImagesRunning = false;\n    isShowImagesRunningTimestamp = 0;\n    isRenderingForecastHours = false;\n    isRenderingForecastHoursTimestamp = 0;\n    isPreloadingImages = false;\n    isPreloadingImagesTimestamp = 0;\n    isLoadingImages = false;\n    \n    selectedStat = stat;\n    \/\/ Po wyborze statystyki przechodzimy do wyboru terminu prognozy\n    renderForecastHours();\n    \/\/ Preload obrazk\u00f3w po zmianie statystyki\n    preloadForecastImages();\n    \/\/ Obrazek zostanie wy\u015bwietlony po renderForecastHours(), wi\u0119c nie ma potrzeby wywo\u0142ywa\u0107 showImages() tutaj\n  }, statisticsDisplayNames);\n  \n  \/\/ Je\u015bli mamy wybran\u0105 statystyk\u0119 i jest ona dost\u0119pna, ustaw j\u0105 w interfejsie\n  if (currentStat && stats.includes(currentStat)) {\n    const statsSelect = document.getElementById(\"statsSelect\");\n    statsSelect.value = currentStat;\n    \/\/ Renderuj interfejs godzin bez symulowania zdarzenia change\n    renderForecastHours();\n  } else if (selectedStat && stats.includes(selectedStat)) {\n    \/\/ Je\u015bli mamy domy\u015bln\u0105 statystyk\u0119 i jest dost\u0119pna\n    const statsSelect = document.getElementById(\"statsSelect\");\n    statsSelect.value = selectedStat;\n    \/\/ Renderuj interfejs godzin bez symulowania zdarzenia change\n    renderForecastHours();\n  }\n}\n\n\/\/ 4. Renderowanie termin\u00f3w prognozy\n\/\/ Zmienna do \u015bledzenia, czy funkcja renderForecastHours jest aktualnie wykonywana\nlet isRenderingForecastHours = false;\nlet isRenderingForecastHoursTimestamp = 0;\nlet latestRenderRequest = null;\nlet pendingRenderRequest = false;\n\nfunction renderForecastHours() {\n  if (!selectedModel || !selectedTerm || !selectedParam) return;\n  \n  \/\/ Nowe modele nie wymagaj\u0105 statystyki (selectedStat mo\u017ce by\u0107 null)\n  \n  \/\/ Zapami\u0119taj aktualne \u017c\u0105danie\n  latestRenderRequest = {\n    model: selectedModel,\n    term: selectedTerm,\n    param: selectedParam,\n    stat: selectedStat\n  };\n  \n  \/\/ Je\u015bli funkcja ju\u017c jest wykonywana, zaznacz \u017ce jest nowe \u017c\u0105danie\n  if (isRenderingForecastHours) {\n    pendingRenderRequest = true;\n    return;\n  }\n  \n  \/\/ Ustaw flag\u0119 wykonywania\n  isRenderingForecastHours = true;\n  isRenderingForecastHoursTimestamp = Date.now();\n  \n  \/\/ Dodaj wska\u017anik \u0142adowania je\u015bli nie mamy wcze\u015bniej preloadowanych danych\n  if (!preloadedModels[selectedModel]) {\n    const hoursContainer = document.getElementById(\"hours\");\n    if (hoursContainer) {\n      hoursContainer.innerHTML = '<div class=\"loading-hours\">Wczytywanie danych modelu...<\/div>';\n    }\n    \n    const imagesContainer = document.getElementById(\"images\");\n    if (imagesContainer) {\n      imagesContainer.innerHTML = \"\";\n      const loaderContainer = document.createElement(\"div\");\n      loaderContainer.className = \"loader-container\";\n      const loader = document.createElement(\"div\");\n      loader.className = \"loader\";\n      loaderContainer.appendChild(loader);\n      loaderContainer.appendChild(document.createElement(\"p\")).textContent = \"\u0141adowanie prognozy...\";\n      imagesContainer.appendChild(loaderContainer);\n    }\n  }\n  \n  \/\/ Aktualizacja tytu\u0142u sekcji godzin w zale\u017cno\u015bci od typu modelu\n  const hoursTitle = document.getElementById(\"hoursTitle\");\n  if (hoursTitle) {\n    if (false && selectedTerm === \"12h\") {\n      hoursTitle.textContent = \"Kolejne prognozy:\";\n    } else {\n      hoursTitle.textContent = \"Godziny prognozy:\";\n    }\n  }\n  \n  let hours = [];\n  \n  \/\/ Sprawd\u017a czy jeste\u015bmy w trybie por\u00f3wnywania modeli\n  const compareBtn = document.getElementById(\"compareBtn\");\n  const isCompareMode = compareBtn && compareBtn.classList.contains(\"active\");\n  \n  if (isCompareMode) {\n    \/\/ W trybie por\u00f3wnywania u\u017cywamy najd\u0142u\u017cszego zakresu prognozy\n    hours = getMaxForecastHours(selectedTerm);\n  } else {\n    \/\/ Wszystkie nowe modele u\u017cywaj\u0105 standardowych godzin\n    if (forecastHours[selectedModel] && forecastHours[selectedModel][selectedTerm]) {\n      hours = forecastHours[selectedModel][selectedTerm];\n    } else {\n      hours = forecastHours[\"default\"];\n    }\n  }\n  \n  \/\/ Zachowujemy aktualnie wybrany termin\n  const currentHour = selectedHour;\n  \n  renderButtons(\"hours\", hours.map(h => {\n    \/\/ Zawsze u\u017cywamy sformatowanych warto\u015bci z wiod\u0105cym zerem\n    return String(h).padStart(2, \"0\");\n  }), (hour) => {\n    \/\/ Ustaw wybran\u0105 godzin\u0119\n    selectedHour = hour;\n    \n    \/\/ Aktualizuj wizualizacj\u0119 za\u0142adowanych godzin\n    updateLoadedHoursVisualization();\n    \n    \/\/ Aktualizacja poziomego slidera\n    \/\/ Najpierw konwertujemy wszystkie godziny do tego samego formatu (string z wiod\u0105cym zerem)\n    const formattedSelectedHour = String(selectedHour).padStart(2, \"0\");\n    const formattedHours = hours.map(h => String(h).padStart(2, \"0\"));\n    \n    \/\/ Teraz szukamy indeksu po sformatowanych godzinach\n    const hourIndex = formattedHours.indexOf(formattedSelectedHour);\n    \n    if (hourIndex !== -1) {\n      document.getElementById(\"forecastHourSlider\").value = hourIndex;\n      \/\/ Aktualizacja wy\u015bwietlanej warto\u015bci\n      if (false && selectedTerm === \"12h\") {\n        \/\/ U\u017cywamy mapowania do wy\u015bwietlenia godziny w formacie dwucyfrowym\n        document.getElementById(\"hourValue\").textContent = forecastHoursDisplayNames[hour] || hour;\n      } else {\n        \/\/ U\u017cywamy mapowania do wy\u015bwietlenia godziny w formacie dwucyfrowym z sufiksem \"h\"\n        const displayHour = forecastHoursDisplayNames[hour] || hour;\n        document.getElementById(\"hourValue\").textContent = displayHour + \"h\";\n      }\n    }\n    \n    showImages();\n    \n    \/\/ Od\u015bwie\u017c widok pe\u0142noekranowy je\u015bli jest aktywny\n    refreshFullscreen();\n    \n    \/\/ WA\u017bNE: Po zmianie godziny przez klikni\u0119cie przycisku, zaktualizuj preloading\n    setTimeout(() => {\n      preloadForecastImages();\n    }, 10);\n  }, currentHour, forecastHoursDisplayNames);\n  \n  \/\/ Preload wszystkich obrazk\u00f3w dla wybranego parametru i terminu\n  \/\/ U\u017cywamy setTimeout aby preloadForecastImages wykona\u0142o si\u0119 po zako\u0144czeniu renderowania\n  setTimeout(() => {\n    preloadForecastImages();\n  }, 10); \/\/ Bardzo szybkie wywo\u0142anie aby obrazy by\u0142y pre\u0142adowane natychmiast\n  \n  \/\/ Inicjalizacja poziomego slidera termin\u00f3w prognozy\n  setupHorizontalSlider(hours);\n  \n  \/\/ Resetuj flag\u0119 wykonywania\n  setTimeout(() => {\n    isRenderingForecastHours = false;\n    isRenderingForecastHoursTimestamp = 0;\n    \n    \/\/ Sprawd\u017a, czy pojawi\u0142o si\u0119 nowe \u017c\u0105danie podczas wykonywania tej funkcji\n    if (pendingRenderRequest) {\n      pendingRenderRequest = false;\n      \n      \/\/ Natychmiast renderuj najnowsze \u017c\u0105danie\n      setTimeout(() => {\n        renderForecastHours();\n      }, 10); \/\/ Bardzo kr\u00f3tki timeout\n    }\n  }, 10); \/\/ Bardzo kr\u00f3tki timeout, aby szybko reagowa\u0107 na zmiany\n  \n  \/\/ Je\u015bli mamy wybrany termin prognozy, pod\u015bwietl odpowiedni przycisk\n  \/\/ hours mo\u017ce zawiera\u0107 liczby [0, 3, 6, ...] lub stringi [\"01\", \"02\", \"03\", ...]\n  \/\/ Sprawdzamy oba typy dla kompatybilno\u015bci\n  const currentHourNum = parseInt(currentHour, 10);\n  const currentHourStr = String(currentHour).padStart(2, \"0\");\n  \n  \/\/ Sprawd\u017a czy hours zawiera liczby czy stringi\n  const hoursContainsNumbers = hours.length > 0 && typeof hours[0] === 'number';\n  const hoursContainsStrings = hours.length > 0 && typeof hours[0] === 'string';\n  \n  const isCurrentHourAvailable = hoursContainsNumbers \n    ? hours.includes(currentHourNum) \n    : hours.includes(currentHourStr);\n  \n  if (currentHour && isCurrentHourAvailable) {\n    setActiveButton(\"hours\", currentHour);\n    selectedHour = currentHour;\n    \n    \/\/ Aktualizuj slider\n    const hourIndex = hoursContainsNumbers ? hours.indexOf(currentHourNum) : hours.indexOf(currentHourStr);\n    if (hourIndex !== -1) {\n      document.getElementById(\"forecastHourSlider\").value = hourIndex;\n      \/\/ Aktualizacja wy\u015bwietlanej warto\u015bci\n      if (false && selectedTerm === \"12h\") {\n        \/\/ U\u017cywamy mapowania do wy\u015bwietlenia godziny w formacie dwucyfrowym\n        document.getElementById(\"hourValue\").textContent = forecastHoursDisplayNames[currentHour] || currentHour;\n      } else {\n        \/\/ U\u017cywamy mapowania do wy\u015bwietlenia godziny w formacie dwucyfrowym z sufiksem \"h\"\n        const displayHour = forecastHoursDisplayNames[currentHour] || currentHour;\n        document.getElementById(\"hourValue\").textContent = displayHour + \"h\";\n      }\n    }\n    \n    \/\/ Wy\u015bwietl obrazek\n    showImages();\n  } else {\n    \/\/ Fallback: sprawd\u017a selectedHour\n    const selectedHourNum = parseInt(selectedHour, 10);\n    const selectedHourStr = String(selectedHour).padStart(2, \"0\");\n    const isSelectedHourAvailable = hoursContainsNumbers \n      ? hours.includes(selectedHourNum) \n      : hours.includes(selectedHourStr);\n    \n    if (selectedHour && isSelectedHourAvailable) {\n      setActiveButton(\"hours\", selectedHour);\n      \n      \/\/ Aktualizuj slider\n      const hourIndex = hoursContainsNumbers ? hours.indexOf(selectedHourNum) : hours.indexOf(selectedHourStr);\n      if (hourIndex !== -1) {\n        document.getElementById(\"forecastHourSlider\").value = hourIndex;\n        \n        \/\/ Aktualizacja wy\u015bwietlanej warto\u015bci\n        if (false && selectedTerm === \"12h\") {\n          \/\/ U\u017cywamy mapowania do wy\u015bwietlenia godziny w formacie dwucyfrowym\n          document.getElementById(\"hourValue\").textContent = forecastHoursDisplayNames[selectedHour] || selectedHour;\n        } else {\n          \/\/ U\u017cywamy mapowania do wy\u015bwietlenia godziny w formacie dwucyfrowym z sufiksem \"h\"\n          const displayHour = forecastHoursDisplayNames[selectedHour] || selectedHour;\n          document.getElementById(\"hourValue\").textContent = displayHour + \"h\";\n        }\n      }\n      \n      \/\/ Wy\u015bwietl obrazek\n      showImages();\n    } else {\n      \/\/ Je\u015bli nie mamy wybranego terminu prognozy, wybierz domy\u015blny dla danego modelu\n      let defaultHour = defaultSettings[selectedModel].hour;\n      \/\/ Zapewniamy, \u017ce domy\u015blna godzina jest w odpowiednim formacie\n      defaultHour = String(defaultHour).padStart(2, \"0\");\n      \n      \/\/ Sprawd\u017a czy domy\u015blna godzina jest dost\u0119pna w aktualnych godzinach\n      const hourExists = hours.some(h => String(h).padStart(2, \"0\") === defaultHour);\n      if (hourExists) {\n        selectedHour = defaultHour;\n      } else {\n        \/\/ Je\u015bli nie, wybierz pierwsz\u0105 dost\u0119pn\u0105\n        selectedHour = String(hours[0]).padStart(2, \"0\");\n      }\n      \n      \/\/ Ustawiamy aktywny przycisk dla sformatowanej godziny\n      setActiveButton(\"hours\", selectedHour);\n      \n      const hourIndex = hours.findIndex(h => h === selectedHour.padStart(2, \"0\"));\n      document.getElementById(\"forecastHourSlider\").value = hourIndex !== -1 ? hourIndex : 0;\n      \n      \/\/ U\u017cyjemy globalnej listy modeli typu \"podsumowanie\"\n      \n      \/\/ Aktualizacja wy\u015bwietlanej warto\u015bci\n      if (false && selectedTerm === \"12h\") {\n        document.getElementById(\"hourValue\").textContent = selectedHour;\n      } else {\n        document.getElementById(\"hourValue\").textContent = selectedHour + \"h\";\n      }\n      \n      \/\/ Wy\u015bwietl obrazek\n      showImages();\n    }\n  }\n  \n  \/\/ Na ko\u0144cu aktualizuj wizualizacj\u0119 za\u0142adowanych godzin\n  setTimeout(() => updateLoadedHoursVisualization(), 100);\n}\n\n\/\/ Obs\u0142uga zdarzenia zmiany poziomu ci\u015bnienia\ndocument.getElementById(\"pressureLevelSlider\").addEventListener(\"input\", function() {\n  \/\/ WA\u017bNE: Wyczy\u015b\u0107 kolejk\u0119 preloadingu NATYCHMIAST na pocz\u0105tku\n  \/\/ aby zatrzyma\u0107 \u0142adowanie obrazk\u00f3w ze starego poziomu ci\u015bnienia\n  resetPreloading();\n  \n  \/\/ Pobranie warto\u015bci z suwaka i mapowanie na odpowiedni poziom ci\u015bnienia\n  const sliderValue = parseInt(this.value);\n  const availableLevels = getAvailablePressureLevels(selectedParam);\n  \n  if (availableLevels.length > sliderValue) {\n    selectedPressureLevel = availableLevels[sliderValue];\n  } else {\n    selectedPressureLevel = availableLevels[0];\n  }\n  \n  \/\/ Aktualizacja wy\u015bwietlanej warto\u015bci\n  if (selectedParam === \"SHEAR\") {\n    \/\/ Dla uskoku wiatru: usu\u0144 \"0_\" i dodaj jednostk\u0119 \"km\"\n    const displayValue = selectedPressureLevel.replace(\/^0_\/, '');\n    document.getElementById(\"pressureValue\").textContent = `${displayValue} m`;\n  } else {\n    document.getElementById(\"pressureValue\").textContent = `${selectedPressureLevel} hPa`;\n  }\n  \n  \/\/ Od\u015bwie\u017cenie obrazka, je\u015bli wszystkie potrzebne parametry s\u0105 ju\u017c wybrane\n  if (selectedModel && selectedTerm && selectedParam && selectedHour) {\n    \/\/ PRIORYTET: Najpierw za\u0142aduj obraz dla aktualnie wybranej godziny\n    showImages();\n    \n    \/\/ Od\u015bwie\u017c widok pe\u0142noekranowy je\u015bli jest aktywny\n    refreshFullscreen();\n    \n    \/\/ Potem preloaduj pozosta\u0142e godziny dla nowego poziomu ci\u015bnienia\n    setTimeout(() => {\n      preloadForecastImages();\n    }, 100);\n    \/\/ Aktualizuj wizualizacj\u0119 za\u0142adowanych godzin\n    setTimeout(() => {\n      updateLoadedHoursVisualization();\n    }, 150);\n  }\n});\n\n\/\/ Funkcja do inicjalizacji i konfiguracji poziomego slidera termin\u00f3w prognozy\nfunction setupHorizontalSlider(hours) {\n  if (!hours || hours.length === 0) {\n    document.getElementById(\"horizontalSliderContainer\").style.display = \"none\";\n    return;\n  }\n  \n  \/\/ Poka\u017c kontener slidera\n  document.getElementById(\"horizontalSliderContainer\").style.display = \"block\";\n  \n  \/\/ Ustaw tytu\u0142 slidera w zale\u017cno\u015bci od typu modelu\n  const sliderTitle = document.getElementById(\"sliderTitle\");\n  if (false && selectedTerm === \"12h\") {\n    sliderTitle.textContent = \"Kolejne prognozy:\";\n  } else {\n    sliderTitle.textContent = \"Godzina prognozy:\";\n  }\n  \n  const slider = document.getElementById(\"forecastHourSlider\");\n  const hourLabelsContainer = document.getElementById(\"hourLabels\");\n  \n  \/\/ Konfiguracja slidera\n  slider.min = 0;\n  slider.max = hours.length - 1;\n  \n  \/\/ Je\u015bli ju\u017c mamy wybran\u0105 godzin\u0119, ustaw slider na t\u0119 warto\u015b\u0107\n  if (selectedHour) {\n    const hourIndex = hours.findIndex(h => h === selectedHour.padStart(2, \"0\"));\n    if (hourIndex !== -1) {\n      slider.value = hourIndex;\n      \/\/ Dla modeli typu podsumowanie w wariancie 12h nie dodajemy \"h\"\n      if (false && selectedTerm === \"12h\") {\n        \/\/ U\u017cywamy mapowania do wy\u015bwietlenia godziny w formacie dwucyfrowym\n        document.getElementById(\"hourValue\").textContent = forecastHoursDisplayNames[selectedHour] || selectedHour;\n      } else {\n        \/\/ U\u017cywamy mapowania do wy\u015bwietlenia godziny w formacie dwucyfrowym z sufiksem \"h\"\n        const displayHour = forecastHoursDisplayNames[selectedHour] || selectedHour;\n        document.getElementById(\"hourValue\").textContent = displayHour + \"h\";\n      }\n    } else {\n      slider.value = 0;\n      \/\/ Dla modeli typu podsumowanie w wariancie 12h nie dodajemy \"h\"\n      if (false && selectedTerm === \"12h\") {\n        \/\/ U\u017cywamy mapowania do wy\u015bwietlenia godziny w formacie dwucyfrowym\n        document.getElementById(\"hourValue\").textContent = forecastHoursDisplayNames[hours[0]] || hours[0];\n      } else {\n        \/\/ U\u017cywamy mapowania do wy\u015bwietlenia godziny w formacie dwucyfrowym z sufiksem \"h\"\n        const displayHour = forecastHoursDisplayNames[hours[0]] || hours[0];\n        document.getElementById(\"hourValue\").textContent = displayHour + \"h\";\n      }\n    }\n  } else {\n    \/\/ Domy\u015blnie pierwszy termin\n    slider.value = 0;\n    \/\/ Dla modeli typu podsumowanie w wariancie 12h nie dodajemy \"h\"\n    if (false && selectedTerm === \"12h\") {\n      \/\/ U\u017cywamy mapowania do wy\u015bwietlenia godziny w formacie dwucyfrowym\n      document.getElementById(\"hourValue\").textContent = forecastHoursDisplayNames[hours[0]] || hours[0];\n    } else {\n      \/\/ U\u017cywamy mapowania do wy\u015bwietlenia godziny w formacie dwucyfrowym z sufiksem \"h\"\n      const displayHour = forecastHoursDisplayNames[hours[0]] || hours[0];\n      document.getElementById(\"hourValue\").textContent = displayHour + \"h\";\n    }\n  }\n  \n  \/\/ Wygeneruj etykiety godzin\n  hourLabelsContainer.innerHTML = \"\";\n  \n  \/\/ Dodajemy kilka reprezentatywnych etykiet, nie wszystkie\n  const labelsToShow = Math.min(7, hours.length); \/\/ Zwi\u0119kszona liczba etykiet\n  const step = Math.max(1, Math.floor(hours.length \/ (labelsToShow - 1)));\n  \n  for (let i = 0; i < hours.length; i += step) {\n    if (i > hours.length - 1) break;\n    \n    const label = document.createElement(\"div\");\n    \/\/ Dla modeli typu podsumowanie w wariancie 12h nie dodajemy \"h\"\n    if (false && selectedTerm === \"12h\") {\n      \/\/ U\u017cywamy mapowania do wy\u015bwietlenia godziny w formacie dwucyfrowym\n      label.textContent = forecastHoursDisplayNames[hours[i]] || hours[i];\n    } else {\n      \/\/ U\u017cywamy mapowania do wy\u015bwietlenia godziny w formacie dwucyfrowym z sufiksem \"h\"\n      const displayHour = forecastHoursDisplayNames[hours[i]] || hours[i];\n      label.textContent = displayHour + \"h\";\n    }\n    hourLabelsContainer.appendChild(label);\n    \n    \/\/ Je\u015bli to ostatnia iteracja, ale nie doszli\u015bmy do ko\u0144ca, dodaj ostatni\u0105 etykiet\u0119\n    if (i + step > hours.length - 1 && i !== hours.length - 1) {\n      const lastLabel = document.createElement(\"div\");\n      \/\/ Dla modeli typu podsumowanie w wariancie 12h nie dodajemy \"h\"\n      if (false && selectedTerm === \"12h\") {\n        \/\/ U\u017cywamy mapowania do wy\u015bwietlenia godziny w formacie dwucyfrowym\n        lastLabel.textContent = forecastHoursDisplayNames[hours[hours.length - 1]] || hours[hours.length - 1];\n      } else {\n        \/\/ U\u017cywamy mapowania do wy\u015bwietlenia godziny w formacie dwucyfrowym z sufiksem \"h\"\n        const displayHour = forecastHoursDisplayNames[hours[hours.length - 1]] || hours[hours.length - 1];\n        lastLabel.textContent = displayHour + \"h\";\n      }\n      hourLabelsContainer.appendChild(lastLabel);\n    }\n  }\n  \n  \/\/ Usu\u0144 wcze\u015bniejsze nas\u0142uchiwanie event\u00f3w, aby unikn\u0105\u0107 duplikacji\n  const oldSlider = slider.cloneNode(true);\n  slider.parentNode.replaceChild(oldSlider, slider);\n  \n  \/\/ Zmienna do op\u00f3\u017anienia aktualizacji obrazk\u00f3w przy szybkim przesuwaniu suwaka\n  let sliderUpdateTimeout = null;\n  let lastSliderPosition = -1;\n  \n  \/\/ Dodaj obs\u0142ug\u0119 zdarzenia zmiany warto\u015bci slidera\n  oldSlider.addEventListener(\"input\", function() {\n    const index = parseInt(this.value);\n    \n    \/\/ Je\u015bli pozycja si\u0119 nie zmieni\u0142a, nie r\u00f3b nic\n    if (index === lastSliderPosition) return;\n    lastSliderPosition = index;\n    \n    const realHourValue = hours[index]; \/\/ Rzeczywista warto\u015b\u0107 u\u017cywana w nazwach plik\u00f3w\n    \n    \/\/ Formatuj godzin\u0119 jako ci\u0105g z wiod\u0105cym zerem\n    const formattedHour = String(realHourValue).padStart(2, \"0\");\n    \n    \/\/ Okre\u015blenie warto\u015bci do wy\u015bwietlania z u\u017cyciem mapowania dla formatu dwucyfrowego\n    let displayHourValue = forecastHoursDisplayNames[realHourValue] || formattedHour;\n    \n    \/\/ Aktualizacja wy\u015bwietlanej warto\u015bci\n    if (false && selectedTerm === \"12h\") {\n      document.getElementById(\"hourValue\").textContent = displayHourValue;\n    } else {\n      document.getElementById(\"hourValue\").textContent = displayHourValue + \"h\";\n    }\n    \n    \/\/ Aktualizacja wybranej godziny (u\u017cywamy rzeczywistej warto\u015bci)\n    selectedHour = formattedHour;\n    \n    \/\/ Aktualizacja aktywnego przycisku w lewym panelu - szukamy przycisku z odpowiedni\u0105 warto\u015bci\u0105\n    setActiveButton(\"hours\", formattedHour);\n    \n    \/\/ Wyczy\u015b\u0107 poprzedni timeout, je\u015bli istnieje\n    if (sliderUpdateTimeout) {\n      clearTimeout(sliderUpdateTimeout);\n    }\n    \n    \/\/ Wywo\u0142aj showImages od razu bez op\u00f3\u017anienia - obrazy z cache \u0142aduj\u0105 si\u0119 natychmiast\n    \/\/ Dzi\u0119ki temu prze\u0142\u0105czanie jest p\u0142ynne bez migania\n    showImages();\n    \n    \/\/ Od\u015bwie\u017c widok pe\u0142noekranowy je\u015bli jest aktywny\n    refreshFullscreen();\n    \n    \/\/ WA\u017bNE: Po zmianie godziny, zaktualizuj preloading dla nowej pozycji\n    \/\/ Dzi\u0119ki temu kolejne obrazy b\u0119d\u0105 pre\u0142adowane wok\u00f3\u0142 nowo wybranej godziny\n    setTimeout(() => {\n      preloadForecastImages();\n    }, 10); \/\/ Bardzo ma\u0142e op\u00f3\u017anienie \u017ceby nie blokowa\u0107 UI, ale uruchomi\u0107 szybko\n  });\n}\n\n\/\/ Funkcja do generowania nazwy pliku na podstawie parametr\u00f3w\nfunction generateFileName(model, term, param, hour, stat, pressureLevel = null, useYesterday = false) {\n  \/\/ Nowy format: MODEL_EUROPA_PARAM_TERMz_DATA_GODZINA_PROGNOZA.webp\n  \/\/ Przyk\u0142ad: IFS_EUROPA_WWIND_LVL_950_00z_2025-11-18_21_00_00.webp\n  \n  \/\/ Je\u015bli mamy ustalon\u0105 dat\u0119 bazow\u0105 dla tej konfiguracji, u\u017cyj jej\n  if (forecastBaseDateString && !useYesterday) {\n    return generateFileNameWithDate(model, term, param, hour, stat, pressureLevel, forecastBaseDateString);\n  }\n  \n  let fileExtension = \"webp\";\n  \n  \/\/ 1. Przygotuj parametr z poziomem ci\u015bnienia je\u015bli istnieje\n  let paramToUse = param;\n  if (pressureLevel && !param.includes(\"_LVL_\")) {\n    \/\/ Dodaj poziom ci\u015bnienia do parametru: WWIND + 950 -> WWIND_LVL_950\n    paramToUse = `${param}_LVL_${pressureLevel}`;\n  }\n  \n  \/\/ 2. Oblicz aktualn\u0105 dat\u0119 i godzin\u0119 (lub wczorajsz\u0105 je\u015bli useYesterday=true)\n  const now = new Date();\n  if (useYesterday) {\n    now.setDate(now.getDate() - 1);\n  }\n  \n  \/\/ Godzina inicjalizacji to term (00, 06, 12, 18)\n  const initHour = parseInt(term);\n  \n  \/\/ 3. Oblicz rzeczywist\u0105 godzin\u0119 prognozy (inicjalizacja + offset)\n  const forecastOffset = parseInt(hour);\n  let forecastDate = new Date(now);\n  forecastDate.setHours(initHour + forecastOffset);\n  \n  \/\/ Formatowanie daty prognozy\n  const year = forecastDate.getFullYear();\n  const month = String(forecastDate.getMonth() + 1).padStart(2, \"0\");\n  const day = String(forecastDate.getDate()).padStart(2, \"0\");\n  const forecastHour = String(forecastDate.getHours()).padStart(2, \"0\");\n  \n  \/\/ Data w formacie YYYY-MM-DD\n  const dateStr = `${year}-${month}-${day}`;\n  \n  \/\/ 4. Z\u0142\u00f3\u017c nazw\u0119 pliku\n  \/\/ Format: MODEL_EUROPA_PARAM_TERMz_DATA_GODZINARZECZYWISTA_00_00.webp\n  const initHourStr = String(term).padStart(2, \"0\");\n  const fileName = `${model}_EUROPA_${paramToUse}_${initHourStr}z_${dateStr}_${forecastHour}_00_00.${fileExtension}`;\n  \n  return fileName;\n}\n\n\/\/ ========================================\n\/\/ FUNKCJE POMOCNICZE DO OBS\u0141UGI DAT\n\/\/ ========================================\n\nfunction getDateString(daysBack) {\n  daysBack = daysBack || 0;\n  const now = new Date();\n  now.setDate(now.getDate() - daysBack);\n  const year = now.getFullYear();\n  const month = String(now.getMonth() + 1).padStart(2, \"0\");\n  const day = String(now.getDate()).padStart(2, \"0\");\n  return year + \"-\" + month + \"-\" + day;\n}\n\nfunction getCurrentDate() {\n  return getDateString(0);\n}\n\n\/\/ Bazowa data dla prognozy - null = nie ustalona\nlet forecastBaseDate = null;\nlet forecastBaseDateString = null; \/\/ Rzeczywista data YYYY-MM-DD\n\n\/\/ Funkcja do sprawdzenia dost\u0119pno\u015bci prognozy dla danej konfiguracji\n\/\/ Sprawdza po dacie modyfikacji pliku (HTTP Last-Modified header)\nfunction checkForecastAvailability(model, term, param, onComplete) {\n  const configKey = model + \"_\" + term + \"_\" + param;\n  \n  \/\/ Je\u015bli ju\u017c sprawdzali\u015bmy t\u0119 konfiguracj\u0119, u\u017cyj zapisanej daty\n  if (forecastBaseDate !== null && forecastBaseDate === configKey) {\n    onComplete(forecastBaseDateString);\n    return;\n  }\n  \n  \/\/ Sprawd\u017a pierwsz\u0105 godzin\u0119 prognozy (00)\n  const testHour = \"00\";\n  \n  \/\/ Dla terminu 18z pliki s\u0105 gotowe nast\u0119pnego dnia, wi\u0119c szukamy wczoraj\/przedwczoraj\n  \/\/ Dla termin\u00f3w 00, 06, 12 szukamy dzisiaj\/wczoraj\n  if (term === \"18\") {\n    \/\/ Termin 18z: najpierw wczoraj, potem przedwczoraj\n    \/\/ Testujemy plik z offsetem 00h (analiza) - dla biegu 18z z wczoraj walidacja = wczoraj 18:00\n    const yesterdayDate = getDateString(1);\n    const testFileName = generateSimpleFileName(model, term, param, \"00\", selectedPressureLevel, yesterdayDate);\n    const yesterdayPath = \"\/wp-content\/uploads\/production\/mapyGorne\/\" + term + \"\/\" + testFileName;\n    \n    fetch(yesterdayPath, { method: 'HEAD' })\n      .then(function(response) {\n        if (!response.ok) {\n          throw new Error('File not found');\n        }\n        \n        \/\/ Sprawd\u017a dat\u0119 modyfikacji pliku\n        const lastModified = response.headers.get('Last-Modified');\n        if (lastModified) {\n          const fileDate = new Date(lastModified);\n          const fileDateStr = fileDate.getFullYear() + \"-\" + \n                             String(fileDate.getMonth() + 1).padStart(2, \"0\") + \"-\" + \n                             String(fileDate.getDate()).padStart(2, \"0\");\n          \n          \/\/ Dla terminu 18z, pliki s\u0105 produkowane DZISIAJ (po 18:00 wczoraj + czas produkcji)\n          \/\/ Wi\u0119c sprawdzamy czy plik powsta\u0142 DZISIAJ (co oznacza bieg 18z z wczoraj)\n          const todayDateStr = getDateString(0);\n          \n          if (fileDateStr === todayDateStr || fileDateStr === yesterdayDate) {\n            \/\/ Plik stworzony dzisiaj lub wczoraj = bieg 18z z wczoraj (\u015bwie\u017cy)\n            forecastBaseDate = configKey;\n            forecastBaseDateString = getDateString(1);\n            onComplete(getDateString(1));\n          } else {\n            \/\/ Plik jest starszy - spr\u00f3buj przedwczoraj\n            tryDayBeforeYesterday();\n          }\n        } else {\n          \/\/ Brak headera, u\u017cyj wczoraj jako domy\u015blne\n          forecastBaseDate = configKey;\n          forecastBaseDateString = getDateString(1);\n          onComplete(getDateString(1));\n        }\n      })\n      .catch(function() {\n        \/\/ Nie ma wczoraj, spr\u00f3buj przedwczoraj\n        tryDayBeforeYesterday();\n      });\n    \n    function tryDayBeforeYesterday() {\n      const dayBeforeDate = getDateString(2);\n      const testFileName = generateSimpleFileName(model, term, param, \"00\", selectedPressureLevel, dayBeforeDate);\n      const dayBeforePath = \"\/wp-content\/uploads\/production\/mapyGorne\/\" + term + \"\/\" + testFileName;\n      \n      fetch(dayBeforePath, { method: 'HEAD' })\n        .then(function(response) {\n          if (response.ok) {\n            forecastBaseDate = configKey;\n            forecastBaseDateString = getDateString(2);\n            onComplete(getDateString(2));\n          } else {\n            forecastBaseDate = configKey;\n            forecastBaseDateString = null;\n            onComplete(null);\n          }\n        })\n        .catch(function() {\n          forecastBaseDate = configKey;\n          forecastBaseDateString = null;\n          onComplete(null);\n        });\n    }\n  } else {\n    \/\/ Terminy 00, 06, 12: najpierw dzisiaj, potem wczoraj\n    const todayDate = getDateString(0);\n    const testFileName = generateSimpleFileName(model, term, param, \"00\", selectedPressureLevel, todayDate);\n    const todayPath = \"\/wp-content\/uploads\/production\/mapyGorne\/\" + term + \"\/\" + testFileName;\n    \n    fetch(todayPath, { method: 'HEAD' })\n      .then(function(response) {\n        if (!response.ok) {\n          throw new Error('File not found');\n        }\n        \n        \/\/ Sprawd\u017a dat\u0119 modyfikacji pliku\n        const lastModified = response.headers.get('Last-Modified');\n        if (lastModified) {\n          const fileDate = new Date(lastModified);\n          const fileDateStr = fileDate.getFullYear() + \"-\" + \n                             String(fileDate.getMonth() + 1).padStart(2, \"0\") + \"-\" + \n                             String(fileDate.getDate()).padStart(2, \"0\");\n          \n          \/\/ Sprawd\u017a czy plik zosta\u0142 stworzony DZISIAJ\n          \/\/ Je\u015bli tak, to jest bieg z dzisiaj\n          \/\/ Je\u015bli nie, to plik z dzisiejsz\u0105 dat\u0105 walidacji jest cz\u0119\u015bci\u0105 wczorajszego biegu (offset >24h)\n          if (fileDateStr === todayDate) {\n            \/\/ Plik stworzony dzisiaj = bieg z dzisiaj\n            forecastBaseDate = configKey;\n            forecastBaseDateString = getDateString(0);\n            onComplete(getDateString(0));\n          } else {\n            \/\/ Plik stworzony wczoraj lub wcze\u015bniej = to offset z wczorajszego biegu\n            \/\/ Szukaj wczorajszej bazy\n            tryYesterdayForecast();\n          }\n        } else {\n          \/\/ Brak headera, sprawd\u017a czas - je\u015bli jeste\u015bmy przed terminem, u\u017cyj wczoraj\n          const now = new Date();\n          const termHour = parseInt(term);\n          if (now.getHours() < termHour + 2) {\n            \/\/ Jeste\u015bmy przed terminem + 2h bufor - prawdopodobnie nie ma jeszcze dzisiejszego biegu\n            tryYesterdayForecast();\n          } else {\n            forecastBaseDate = configKey;\n            forecastBaseDateString = getDateString(0);\n            onComplete(getDateString(0));\n          }\n        }\n      })\n      .catch(function() {\n        \/\/ Nie ma dzisiaj, spr\u00f3buj wczoraj\n        tryYesterdayForecast();\n      });\n    \n    function tryYesterdayForecast() {\n      const yesterdayDate = getDateString(1);\n      const testFileName = generateSimpleFileName(model, term, param, \"00\", selectedPressureLevel, yesterdayDate);\n      const yesterdayPath = \"\/wp-content\/uploads\/production\/mapyGorne\/\" + term + \"\/\" + testFileName;\n      \n      fetch(yesterdayPath, { method: 'HEAD' })\n        .then(function(response) {\n          if (response.ok) {\n            forecastBaseDate = configKey;\n            forecastBaseDateString = getDateString(1);\n            onComplete(getDateString(1));\n          } else {\n            forecastBaseDate = configKey;\n            forecastBaseDateString = null;\n            onComplete(null);\n          }\n        })\n        .catch(function() {\n          forecastBaseDate = configKey;\n          forecastBaseDateString = null;\n          onComplete(null);\n        });\n    }\n  }\n}\n\n\/\/ Funkcja pomocnicza do generowania nazwy pliku dla sprawdzania dost\u0119pno\u015bci\n\/\/ NIE modyfikuje daty - u\u017cywa bezpo\u015brednio podanej daty bazowej\nfunction generateSimpleFileName(model, term, param, hour, pressureLevel, dateStr) {\n  let fileExtension = \"webp\";\n  \n  \/\/ Przygotuj parametr z poziomem ci\u015bnienia je\u015bli istnieje\n  let paramToUse = param;\n  if (pressureLevel && !param.includes(\"_LVL_\")) {\n    paramToUse = `${param}_LVL_${pressureLevel}`;\n  }\n  \n  \/\/ Godzina inicjalizacji to term (00, 06, 12, 18)\n  const initHour = parseInt(term);\n  \n  \/\/ Oblicz godzin\u0119 walidacji (inicjalizacja + offset)\n  const forecastOffset = parseInt(hour);\n  const validationHour = initHour + forecastOffset;\n  \n  \/\/ Je\u015bli godzina walidacji >= 24, przesu\u0144 dat\u0119\n  let finalDate = dateStr;\n  let finalHour = validationHour;\n  \n  if (validationHour >= 24) {\n    const dateParts = dateStr.split(\"-\");\n    const date = new Date(parseInt(dateParts[0]), parseInt(dateParts[1]) - 1, parseInt(dateParts[2]));\n    date.setDate(date.getDate() + Math.floor(validationHour \/ 24));\n    \n    const year = date.getFullYear();\n    const month = String(date.getMonth() + 1).padStart(2, \"0\");\n    const day = String(date.getDate()).padStart(2, \"0\");\n    finalDate = `${year}-${month}-${day}`;\n    finalHour = validationHour % 24;\n  }\n  \n  const initHourStr = String(term).padStart(2, \"0\");\n  const validationHourStr = String(finalHour).padStart(2, \"0\");\n  const fileName = `${model}_EUROPA_${paramToUse}_${initHourStr}z_${finalDate}_${validationHourStr}_00_00.${fileExtension}`;\n  \n  return fileName;\n}\n\n\/\/ Funkcja pomocnicza do generowania nazwy pliku z konkretn\u0105 dat\u0105\nfunction generateFileNameWithDate(model, term, param, hour, stat, pressureLevel, dateStr) {\n  let fileExtension = \"webp\";\n  \n  \/\/ Przygotuj parametr z poziomem ci\u015bnienia je\u015bli istnieje\n  let paramToUse = param;\n  if (pressureLevel && !param.includes(\"_LVL_\")) {\n    paramToUse = `${param}_LVL_${pressureLevel}`;\n  }\n  \n  \/\/ Godzina inicjalizacji to term (00, 06, 12, 18)\n  const initHour = parseInt(term);\n  \n  \/\/ Oblicz rzeczywist\u0105 godzin\u0119 prognozy (inicjalizacja + offset)\n  const forecastOffset = parseInt(hour);\n  \n  \/\/ Parsuj dat\u0119 (format YYYY-MM-DD)\n  const dateParts = dateStr.split(\"-\");\n  let forecastDate = new Date(parseInt(dateParts[0]), parseInt(dateParts[1]) - 1, parseInt(dateParts[2]));\n  forecastDate.setHours(initHour + forecastOffset);\n  \n  \/\/ Formatowanie daty prognozy\n  const year = forecastDate.getFullYear();\n  const month = String(forecastDate.getMonth() + 1).padStart(2, \"0\");\n  const day = String(forecastDate.getDate()).padStart(2, \"0\");\n  const forecastHour = String(forecastDate.getHours()).padStart(2, \"0\");\n  \n  \/\/ Data w formacie YYYY-MM-DD\n  const finalDateStr = `${year}-${month}-${day}`;\n  \n  \/\/ Z\u0142\u00f3\u017c nazw\u0119 pliku\n  const initHourStr = String(term).padStart(2, \"0\");\n  const fileName = `${model}_EUROPA_${paramToUse}_${initHourStr}z_${finalDateStr}_${forecastHour}_00_00.${fileExtension}`;\n  \n  return fileName;\n}\n\n\/\/ Zmienne do obs\u0142ugi kolejki \u0142adowania obraz\u00f3w\nlet imageLoadQueue = [];\nlet isLoadingImages = false;\nconst MAX_CONCURRENT_LOADS = 8; \/\/ Maksymalna liczba jednocze\u015bnie \u0142adowanych obraz\u00f3w\n\n\/\/ Unikalny identyfikator sesji preloadingu - zwi\u0119kszany przy ka\u017cdej zmianie parametr\u00f3w\nlet preloadSessionId = 0;\n\n\/\/ Funkcja pomocnicza do resetowania systemu preloadingu\nfunction resetPreloading() {\n  preloadSessionId++; \/\/ Zwi\u0119ksz ID sesji aby przerwa\u0107 aktywne \u0142adowanie\n  imageLoadQueue = [];\n  lastPreloadedHour = null;\n  isLoadingImages = false;\n  isPreloadingImages = false;\n  isPreloadingImagesTimestamp = 0;\n  \/\/ Resetuj bazow\u0105 dat\u0119 prognozy\n  forecastBaseDate = null;\n  forecastBaseDateString = null;\n}\n\n\/\/ Funkcja do preloadowania obrazk\u00f3w dla wszystkich termin\u00f3w prognozy\n\/\/ Zmienna do \u015bledzenia, czy funkcja preloadForecastImages jest aktualnie wykonywana\nlet isPreloadingImages = false;\nlet isPreloadingImagesTimestamp = 0;\n\n\/\/ Zmienna do przechowywania listy pre\u0142adowanych obraz\u00f3w - zdefiniowana poza funkcj\u0105 \n\/\/ \u017ceby by\u0142a dost\u0119pna w addImageToLoadQueue\nlet preloadImagesSet = null;\n\n\/\/ Lista obraz\u00f3w do za\u0142adowania w kolejno\u015bci priorytetowej - zdefiniowana globalnie\n\/\/ \u017ceby by\u0142a dost\u0119pna w addImageToLoadQueue\nlet imagesToLoad = [];\n\n\/\/ Zapami\u0119taj ostatni\u0105 godzin\u0119 dla kt\u00f3rej by\u0142o preloadowanie\nlet lastPreloadedHour = null;\n\nfunction preloadForecastImages() {\n  if (!selectedModel || !selectedTerm || !selectedParam) {\n    return;\n  }\n  \n  \/\/ Sprawd\u017a dost\u0119pno\u015b\u0107 prognozy przed preloadingiem\n  const configKey = selectedModel + \"_\" + selectedTerm + \"_\" + selectedParam;\n  if (forecastBaseDate !== configKey) {\n    \/\/ Najpierw sprawd\u017a dost\u0119pno\u015b\u0107\n    checkForecastAvailability(selectedModel, selectedTerm, selectedParam, function(baseDate) {\n      if (baseDate) {\n        \/\/ Mamy dat\u0119 bazow\u0105, teraz preloaduj\n        preloadForecastImages();\n      }\n    });\n    return;\n  }\n  \n  \/\/ Je\u015bli funkcja ju\u017c jest wykonywana DLA TEJ SAMEJ godziny, przerwij\n  if (isPreloadingImages && lastPreloadedHour === selectedHour) {\n    return;\n  }\n  \n  \/\/ Je\u015bli zmieniono godzin\u0119, zresetuj flag\u0119\n  if (lastPreloadedHour !== selectedHour) {\n    isPreloadingImages = false;\n    isPreloadingImagesTimestamp = 0;\n  }\n  \n  lastPreloadedHour = selectedHour;\n  \n  \/\/ WA\u017bNE: Wyczy\u015b\u0107 ca\u0142\u0105 kolejk\u0119 \u0142adowania przed rozpocz\u0119ciem nowego preloadingu\n  \/\/ To zapobiega \u0142adowaniu starych obrazk\u00f3w po zmianie parametru\n  imageLoadQueue = [];\n  isLoadingImages = false;\n  \n  \/\/ Zapisz aktualny sessionId dla tej sesji preloadingu\n  const thisSessionId = preloadSessionId;\n  \n  \/\/ Ustaw flag\u0119 wykonywania\n  isPreloadingImages = true;\n  isPreloadingImagesTimestamp = Date.now();\n  \n  try {\n    \/\/ Pobranie dost\u0119pnych godzin prognozy\n    let hours = [];\n    \n    \/\/ Sprawd\u017a czy jeste\u015bmy w trybie por\u00f3wnywania modeli\n    const compareBtn = document.getElementById(\"compareBtn\");\n    const isCompareMode = compareBtn && compareBtn.classList.contains(\"active\");\n    \n    if (isCompareMode) {\n      \/\/ W trybie por\u00f3wnywania u\u017cywamy najd\u0142u\u017cszego zakresu prognozy\n      hours = getMaxForecastHours(selectedTerm);\n    } else {\n      \/\/ Wszystkie nowe modele u\u017cywaj\u0105 standardowych godzin\n      if (forecastHours[selectedModel] && forecastHours[selectedModel][selectedTerm]) {\n        hours = forecastHours[selectedModel][selectedTerm];\n      } else {\n        hours = forecastHours[\"default\"];\n      }\n    }\n    \n    \/\/ Znajd\u017a indeks aktualnie wybranej godziny\n    const selectedHourIndex = hours.findIndex(h => String(h).padStart(2, \"0\") === String(selectedHour).padStart(2, \"0\"));\n    if (selectedHourIndex === -1) {\n      return;\n    }\n    \n    \/\/ Tablica do \u015bledzenia ju\u017c za\u0142adowanych obrazk\u00f3w - inicjalizacja globalnej zmiennej\n    preloadImagesSet = new Set();\n    \n    \/\/ Resetuj list\u0119 obraz\u00f3w do za\u0142adowania (u\u017cywamy globalnej zmiennej)\n    imagesToLoad = [];\n    \n    \/\/ Pobierz dost\u0119pne poziomy ci\u015bnienia dla aktualnego parametru\n    const availableLevels = getAvailablePressureLevels(selectedParam);\n    const hasMultipleLevels = availableLevels.length > 1;\n    \n    \/\/ PRIORYTET 1: Aktualna godzina z aktualnym poziomem ci\u015bnienia\n    addImageToLoadQueue(selectedHourIndex, hours);\n    \n    \/\/ PRIORYTET 2: S\u0105siednie poziomy ci\u015bnienia dla aktualnej godziny\n    if (hasMultipleLevels && selectedPressureLevel) {\n      const currentLevelIndex = availableLevels.indexOf(selectedPressureLevel);\n      if (currentLevelIndex > 0) {\n        addImageToLoadQueueWithLevel(selectedHourIndex, hours, availableLevels[currentLevelIndex - 1]);\n      }\n      if (currentLevelIndex < availableLevels.length - 1) {\n        addImageToLoadQueueWithLevel(selectedHourIndex, hours, availableLevels[currentLevelIndex + 1]);\n      }\n    }\n    \n    \/\/ PRIORYTET 3: Najbli\u017csze godziny (\u00b11, \u00b12, \u00b13)\n    for (let offset = 1; offset <= 3; offset++) {\n      if (selectedHourIndex + offset < hours.length) {\n        addImageToLoadQueue(selectedHourIndex + offset, hours);\n      }\n      if (selectedHourIndex - offset >= 0) {\n        addImageToLoadQueue(selectedHourIndex - offset, hours);\n      }\n    }\n    \n    \/\/ PRIORYTET 4: Dalsze godziny (\u00b14 i wi\u0119cej)\n    let maxOffset = Math.max(selectedHourIndex, hours.length - selectedHourIndex - 1);\n    for (let offset = 4; offset <= maxOffset; offset++) {\n      if (selectedHourIndex + offset < hours.length) {\n        addImageToLoadQueue(selectedHourIndex + offset, hours);\n      }\n      if (selectedHourIndex - offset >= 0) {\n        addImageToLoadQueue(selectedHourIndex - offset, hours);\n      }\n    }\n    \n    \/\/ Dodaj obrazy do g\u0142\u00f3wnej kolejki \u0142adowania\n    if (imagesToLoad.length > 0) {\n      imageLoadQueue = imageLoadQueue.concat(imagesToLoad);\n      if (!isLoadingImages) {\n        processImageQueue(thisSessionId);\n      }\n    }\n  } catch (error) {\n    console.error(\"B\u0142\u0105d podczas preloadowania obrazk\u00f3w:\", error);\n  } finally {\n    \/\/ Zawsze resetuj flag\u0119 wykonywania, nawet w przypadku b\u0142\u0119du\n    isPreloadingImages = false;\n    isPreloadingImagesTimestamp = 0;\n  }\n  \n  \/\/ Funkcja pomocnicza do dodawania obrazu do kolejki \u0142adowania\n  function addImageToLoadQueue(hourIndex, hoursArray) {\n    const hour = hoursArray[hourIndex];\n    const formattedHour = String(hour);\n    \n    \/\/ Sprawd\u017a czy jeste\u015bmy w trybie por\u00f3wnywania\n    const compareBtn = document.getElementById(\"compareBtn\");\n    const isCompareMode = compareBtn && compareBtn.classList.contains(\"active\");\n    \n    \/\/ W trybie por\u00f3wnania \u0142aduj dla wszystkich modeli, w pojedynczym tylko dla wybranego\n    const modelsToLoad = isCompareMode ? models : [selectedModel];\n    \n    modelsToLoad.forEach(model => {\n      \/\/ Sprawd\u017a czy ten model ma t\u0119 godzin\u0119\n      const modelHours = forecastHours[model]?.[selectedTerm] || forecastHours[\"default\"];\n      if (!modelHours.includes(hour)) {\n        return; \/\/ Ten model nie ma tej godziny, pomi\u0144\n      }\n      \n      \/\/ Wygeneruj nazw\u0119 pliku\n      const fileName = generateFileName(\n        model, \n        selectedTerm, \n        selectedParam, \n        formattedHour, \n        selectedStat, \n        selectedPressureLevel\n      );\n      \n      \/\/ Unikaj duplikat\u00f3w\n      const imagePath = `\/wp-content\/uploads\/production\/mapyGorne\/${selectedTerm}\/${fileName}`;\n      if (preloadImagesSet && preloadImagesSet.has(imagePath)) {\n        return;\n      }\n      \n      \/\/ Generuj unikalne ID dla obrazu do buforowania\n      const imageId = `img_${model}_${selectedTerm}_${selectedParam}_${formattedHour}_${selectedStat}_${selectedPressureLevel}`.replace(\/\\s+\/g, '_');\n      \n      \/\/ Je\u015bli obraz jest ju\u017c w buforze, pomijamy\n      if (imageCache[imageId]) {\n        return;\n      }\n      \n      \/\/ Dodanie do listy preloadowanych\n      if (preloadImagesSet) preloadImagesSet.add(imagePath);\n      \n      \/\/ Dodaj do listy do za\u0142adowania\n      imagesToLoad.push({\n        url: imagePath,\n        id: imageId,\n        fileName: fileName,\n        model: model \/\/ Dodaj model do informacji o obrazie\n      });\n    });\n  }\n  \n  \/\/ Funkcja pomocnicza do dodawania obrazu z okre\u015blonym poziomem ci\u015bnienia\n  function addImageToLoadQueueWithLevel(hourIndex, hoursArray, pressureLevel) {\n    const hour = hoursArray[hourIndex];\n    const formattedHour = String(hour);\n    \n    \/\/ Sprawd\u017a czy jeste\u015bmy w trybie por\u00f3wnywania\n    const compareBtn = document.getElementById(\"compareBtn\");\n    const isCompareMode = compareBtn && compareBtn.classList.contains(\"active\");\n    \n    \/\/ W trybie por\u00f3wnania \u0142aduj dla wszystkich modeli, w pojedynczym tylko dla wybranego\n    const modelsToLoad = isCompareMode ? models : [selectedModel];\n    \n    modelsToLoad.forEach(model => {\n      \/\/ Sprawd\u017a czy ten model ma t\u0119 godzin\u0119\n      const modelHours = forecastHours[model]?.[selectedTerm] || forecastHours[\"default\"];\n      if (!modelHours.includes(hour)) {\n        return; \/\/ Ten model nie ma tej godziny, pomi\u0144\n      }\n      \n      \/\/ Wygeneruj nazw\u0119 pliku z innym poziomem ci\u015bnienia\n      const fileName = generateFileName(\n        model, \n        selectedTerm, \n        selectedParam, \n        formattedHour, \n        selectedStat, \n        pressureLevel\n      );\n      \n      \/\/ Unikaj duplikat\u00f3w\n      const imagePath = `\/wp-content\/uploads\/production\/mapyGorne\/${selectedTerm}\/${fileName}`;\n      if (preloadImagesSet && preloadImagesSet.has(imagePath)) {\n        return;\n      }\n      \n      \/\/ Generuj unikalne ID\n      const imageId = `img_${model}_${selectedTerm}_${selectedParam}_${formattedHour}_${selectedStat}_${pressureLevel}`.replace(\/\\s+\/g, '_');\n      \n      \/\/ Je\u015bli obraz jest ju\u017c w buforze, pomijamy\n      if (imageCache[imageId]) {\n        return;\n      }\n      \n      \/\/ Dodanie do listy preloadowanych\n      if (preloadImagesSet) preloadImagesSet.add(imagePath);\n      \n      \/\/ Dodaj do listy do za\u0142adowania\n      imagesToLoad.push({\n        url: imagePath,\n        id: imageId,\n        fileName: fileName,\n        model: model \/\/ Dodaj model do informacji o obrazie\n      });\n    });\n  }\n}\n\n\/\/ Funkcja do przetwarzania kolejki obraz\u00f3w z limitowaniem jednoczesnych pobra\u0144\nfunction processImageQueue(sessionId) {\n  \/\/ Sprawd\u017a czy to wci\u0105\u017c aktualna sesja - je\u015bli nie, przerwij\n  if (sessionId !== undefined && sessionId !== preloadSessionId) {\n    isLoadingImages = false;\n    return;\n  }\n  \n  if (imageLoadQueue.length === 0) {\n    isLoadingImages = false;\n    return;\n  }\n  \n  isLoadingImages = true;\n  const currentBatch = imageLoadQueue.splice(0, MAX_CONCURRENT_LOADS);\n  let loadedCount = 0;\n  \n  currentBatch.forEach(imageInfo => {\n    \/\/ Sprawd\u017a ponownie czy sesja jest aktualna przed \u0142adowaniem ka\u017cdego obrazka\n    if (sessionId !== undefined && sessionId !== preloadSessionId) {\n      loadedCount++;\n      if (loadedCount === currentBatch.length) {\n        isLoadingImages = false;\n      }\n      return;\n    }\n    \n    \/\/ Sprawd\u017a czy obraz ju\u017c istnieje w buforze\n    if (imageCache[imageInfo.id]) {\n      loadedCount++;\n      if (loadedCount === currentBatch.length) {\n        setTimeout(() => processImageQueue(sessionId), 10); \/\/ Przeka\u017c sessionId dalej\n      }\n      return;\n    }\n    \n    \/\/ Wczytaj obrazek w tle\n    const preloadImg = new Image();\n    \n    preloadImg.onload = () => {\n      \/\/ Sprawd\u017a czy sesja jest wci\u0105\u017c aktualna przed zapisem do cache\n      if (sessionId !== undefined && sessionId !== preloadSessionId) {\n        loadedCount++;\n        if (loadedCount === currentBatch.length) {\n          isLoadingImages = false;\n        }\n        return;\n      }\n      \n      \/\/ Zapisz obrazek w buforze\n      imageCache[imageInfo.id] = preloadImg;\n      \n      \/\/ Oznacz godzin\u0119 jako za\u0142adowan\u0105\n      \/\/ U\u017cywamy informacji o modelu przechowywanej w imageInfo\n      const model = imageInfo.model || selectedModel;\n      \/\/ imageInfo.id ma format: img_MODEL_TERM_PARAM_HOUR_STAT_LEVEL\n      const idParts = imageInfo.id.split('_');\n      if (idParts.length >= 5) {\n        const hourFromId = idParts[4];\n        const termFromId = idParts[2];\n        const paramFromId = idParts[3];\n        \/\/ Poziom zaczyna si\u0119 od indeksu 6 i mo\u017ce zawiera\u0107 podkre\u015blenia\n        const levelFromId = idParts.length > 6 ? idParts.slice(6).join('_') : (idParts[6] || null);\n        markHourAsLoaded(model, termFromId, paramFromId, hourFromId, levelFromId);\n      }\n      \n      loadedCount++;\n      if (loadedCount === currentBatch.length) {\n        \/\/ Kiedy wszystkie obrazki z tej partii s\u0105 za\u0142adowane, przejd\u017a do nast\u0119pnej\n        setTimeout(() => processImageQueue(sessionId), 10); \/\/ Przeka\u017c sessionId dalej\n      }\n    };\n    \n    preloadImg.onerror = () => {\n      \/\/ Je\u015bli nie uda\u0142o si\u0119 za\u0142adowa\u0107, po prostu kontynuuj\n      \/\/ (checkForecastAvailability ju\u017c sprawdzi\u0142o dost\u0119pno\u015b\u0107 wczorajszych plik\u00f3w)\n      loadedCount++;\n      if (loadedCount === currentBatch.length) {\n        \/\/ Nawet je\u015bli wyst\u0105pi\u0142 b\u0142\u0105d, przejd\u017a do nast\u0119pnej partii\n        setTimeout(() => processImageQueue(sessionId), 10); \/\/ Przeka\u017c sessionId dalej\n      }\n    };\n    \n    preloadImg.src = imageInfo.url;\n  });\n}\n\n\/\/ Funkcja do aktualizacji interfejsu na podstawie wybranych warto\u015bci\nfunction updateTermLabel() {\n  \/\/ Znalezienie etykiety dla terminu\n  const sections = document.querySelectorAll('.left-column .section');\n  if (sections.length >= 2) {\n    const termLabel = sections[1].querySelector('h3');\n    if (termLabel) {\n      \/\/ Sprawdzenie czy model jest jednym z modeli \"podsumowanie\"\n      termLabel.textContent = \"Termin startu:\";\n    }\n  }\n}\n\nfunction updateTermButtons() {\n  if (!selectedModel) return;\n  \n  \/\/ Aktualizacja etykiety na podstawie wybranego modelu\n  updateTermLabel();\n  \n  \/\/ Ustawienie domy\u015blnych warto\u015bci z ustawie\u0144 dla danego modelu\n  if (!selectedTerm) {\n    selectedTerm = defaultSettings[selectedModel].term;\n  }\n  if (!selectedParam) {\n    selectedParam = defaultSettings[selectedModel].param;\n  }\n  if (!selectedHour) {\n    selectedHour = defaultSettings[selectedModel].hour;\n  }\n  if (!selectedStat && defaultSettings[selectedModel].stat) {\n    selectedStat = defaultSettings[selectedModel].stat;\n  }\n  \n  \/\/ Renderowanie przycisk\u00f3w terminu\n  \n  \/\/ Wszystkie nowe modele u\u017cywaj\u0105 standardowych termin\u00f3w startu\n  renderButtons(\"terms\", termsDefault, (term) => {\n    \/\/ WA\u017bNE: Wyczy\u015b\u0107 kolejk\u0119 preloadingu NATYCHMIAST na pocz\u0105tku\n    \/\/ aby zatrzyma\u0107 \u0142adowanie obrazk\u00f3w ze starego terminu\n    resetPreloading();\n    \n    selectedTerm = term;\n    \n    renderParams();\n    \n    \/\/ Od\u015bwie\u017c widok pe\u0142noekranowy je\u015bli jest aktywny\n    refreshFullscreen();\n  }, selectedTerm);\n  \n  renderParams();\n}\n\n\/\/ Funkcja do optymalizacji pobierania element\u00f3w DOM - buforuje znalezione elementy\nconst domCache = {};\nfunction getElementCached(id) {\n  if (!domCache[id]) {\n    domCache[id] = document.getElementById(id);\n  }\n  return domCache[id];\n}\n\n\/\/ Funkcja do resetowania wszystkich flag wykonywania\nfunction resetAllExecutionFlags() {\n  \n  \/\/ Resetowanie flag wykonywania\n  isShowImagesRunning = false;\n  isShowImagesRunningTimestamp = 0;\n  isRenderingForecastHours = false;\n  isRenderingForecastHoursTimestamp = 0;\n  isPreloadingImages = false;\n  isPreloadingImagesTimestamp = 0;\n  isLoadingImages = false;\n  \n  \/\/ Resetowanie zmiennej ostatniej preloadowanej godziny\n  lastPreloadedHour = null;\n  \n  \/\/ Wyczy\u015b\u0107 wszystkie timery\n  if (loaderTimeout) {\n    clearTimeout(loaderTimeout);\n    loaderTimeout = null;\n  }\n  \n  if (imageLoadTimeout) {\n    clearTimeout(imageLoadTimeout);\n    imageLoadTimeout = null;\n  }\n  \n  \/\/ Opcjonalnie - zresetuj kolejk\u0119 obraz\u00f3w\n  imageLoadQueue = [];\n}\n\n\/\/ Funkcja resetuj\u0105ca stan przy zmianie modelu\nfunction resetStateForModelChange() {\n  \/\/ Resetuj wszystkie flagi wykonywania\n  resetAllExecutionFlags();\n  \n  \/\/ Resetuj system preloadingu\n  resetPreloading();\n  console.log(`\ud83d\udd04 Zwi\u0119kszono preloadSessionId do: ${preloadSessionId} (zmiana modelu)`);\n  \n  \/\/ Resetuj bufor obraz\u00f3w - czy\u015bcimy zawarto\u015b\u0107 obiektu zamiast przypisywa\u0107 nowy\n  Object.keys(imageCache).forEach(key => {\n    delete imageCache[key];\n  });\n  \n  \/\/ Resetuj ostatnio wy\u015bwietlany obraz\n  lastDisplayedImage = {\n    model: \"\",\n    term: \"\",\n    param: \"\",\n    hour: \"\",\n    stat: \"\",\n    pressureLevel: \"\"\n  };\n  \n  \/\/ Wyczy\u015b\u0107 aktywny timeout loadera je\u015bli istnieje\n  if (loaderTimeout) {\n    clearTimeout(loaderTimeout);\n    loaderTimeout = null;\n  }\n  \n  \/\/ Wyczy\u015b\u0107 kontenery obraz\u00f3w\n  const imagesContainer = getElementCached(\"images\");\n  if (imagesContainer) {\n    imagesContainer.innerHTML = \"\";\n  }\n  \n  \/\/ Wyczy\u015b\u0107 kolejk\u0119 \u0142adowania obraz\u00f3w\n  imageLoadQueue = [];\n  isLoadingImages = false;\n  \n  \/\/ Wyczy\u015b\u0107 timeout \u0142adowania obrazu je\u015bli istnieje\n  if (imageLoadTimeout) {\n    clearTimeout(imageLoadTimeout);\n    imageLoadTimeout = null;\n  }\n}\n\n\/\/ Timeout \u0142adowania obrazu\nlet imageLoadTimeout = null;\n\n\/\/ Funkcja do aktualizacji wizualizacji za\u0142adowanych godzin\nfunction updateLoadedHoursVisualization() {\n  \/\/ Sprawd\u017a czy jeste\u015bmy w trybie por\u00f3wnywania modeli\n  const compareBtn = document.getElementById(\"compareBtn\");\n  const isCompareMode = compareBtn && compareBtn.classList.contains(\"active\");\n  \n  let hoursLoaded;\n  \n  if (isCompareMode) {\n    \/\/ W trybie por\u00f3wnania - godzina jest za\u0142adowana tylko gdy jest za\u0142adowana dla wszystkich modeli (kt\u00f3re maj\u0105 t\u0119 godzin\u0119)\n    hoursLoaded = new Set();\n    \n    \/\/ Pobierz wszystkie dost\u0119pne godziny dla najd\u0142u\u017cszego modelu\n    const allHours = getMaxForecastHours(selectedTerm);\n    \n    allHours.forEach(hour => {\n      const hourStr = String(hour).padStart(2, \"0\");\n      let isLoadedForAllModels = true;\n      \n      \/\/ Sprawd\u017a ka\u017cdy model\n      models.forEach(model => {\n        const modelHours = forecastHours[model]?.[selectedTerm] || forecastHours[\"default\"];\n        const hourNum = parseInt(hourStr, 10);\n        \n        \/\/ Je\u015bli model ma t\u0119 godzin\u0119, sprawd\u017a czy jest za\u0142adowana\n        if (modelHours.includes(hourNum)) {\n          const configKey = `${model}_${selectedTerm}_${selectedParam}_${selectedPressureLevel}`;\n          const modelLoadedHours = loadedHours[configKey] || new Set();\n          \n          if (!modelLoadedHours.has(hourStr)) {\n            isLoadedForAllModels = false;\n          }\n        }\n      });\n      \n      \/\/ Je\u015bli za\u0142adowana dla wszystkich odpowiednich modeli, dodaj do zestawu\n      if (isLoadedForAllModels) {\n        hoursLoaded.add(hourStr);\n      }\n    });\n  } else {\n    \/\/ Tryb pojedynczego modelu\n    const configKey = `${selectedModel}_${selectedTerm}_${selectedParam}_${selectedPressureLevel}`;\n    hoursLoaded = loadedHours[configKey] || new Set();\n  }\n  \n  \/\/ Aktualizacja przycisk\u00f3w godzin\n  const hourButtons = document.querySelectorAll('#hours button');\n  hourButtons.forEach(button => {\n    const hour = button.dataset.value;\n    if (hoursLoaded.has(hour)) {\n      button.classList.add('loaded');\n    } else {\n      button.classList.remove('loaded');\n    }\n  });\n  \n  \/\/ Aktualizacja slidera - oblicz procent za\u0142adowanych godzin\n  let hours = [];\n  \n  if (isCompareMode) {\n    \/\/ W trybie por\u00f3wnywania u\u017cywamy najd\u0142u\u017cszego zakresu prognozy\n    hours = getMaxForecastHours(selectedTerm);\n  } else {\n    if (forecastHours[selectedModel] && forecastHours[selectedModel][selectedTerm]) {\n      hours = forecastHours[selectedModel][selectedTerm];\n    } else {\n      hours = forecastHours[\"default\"];\n    }\n  }\n  \n  if (hours.length > 0) {\n    \/\/ Znajd\u017a najwy\u017cszy indeks za\u0142adowanej godziny\n    let maxLoadedIndex = -1;\n    hours.forEach((hour, index) => {\n      const hourStr = String(hour).padStart(2, \"0\");\n      if (hoursLoaded.has(hourStr) && index > maxLoadedIndex) {\n        maxLoadedIndex = index;\n      }\n    });\n    \n    const sliderProgress = document.getElementById('sliderProgress');\n    if (sliderProgress && maxLoadedIndex >= 0) {\n      const percentage = ((maxLoadedIndex + 1) \/ hours.length) * 100;\n      sliderProgress.style.width = percentage + '%';\n    }\n  }\n}\n\n\/\/ Obiekt do \u015bledzenia za\u0142adowanych godzin dla aktualnej konfiguracji\nlet loadedHours = {};\n\n\/\/ Funkcja do oznaczenia godziny jako za\u0142adowanej\nfunction markHourAsLoaded(model, term, param, hour, pressureLevel) {\n  const configKey = `${model}_${term}_${param}_${pressureLevel}`;\n  if (!loadedHours[configKey]) {\n    loadedHours[configKey] = new Set();\n  }\n  const hourStr = String(hour).padStart(2, \"0\");\n  loadedHours[configKey].add(hourStr);\n  \n  \/\/ Aktualizuj wizualizacj\u0119\n  \/\/ W trybie por\u00f3wnania aktualizuj dla wszystkich modeli, w trybie pojedynczym tylko dla wybranego\n  const compareBtn = document.getElementById(\"compareBtn\");\n  const isCompareMode = compareBtn && compareBtn.classList.contains(\"active\");\n  \n  if (isCompareMode) {\n    \/\/ W trybie por\u00f3wnania - aktualizuj wizualizacj\u0119 je\u015bli term i param si\u0119 zgadzaj\u0105\n    if (term === selectedTerm && param === selectedParam && pressureLevel === selectedPressureLevel) {\n      updateLoadedHoursVisualization();\n    }\n  } else {\n    \/\/ W trybie pojedynczym - aktualizuj tylko dla wybranego modelu\n    if (model === selectedModel && term === selectedTerm && \n        param === selectedParam && pressureLevel === selectedPressureLevel) {\n      updateLoadedHoursVisualization();\n    }\n  }\n}\n\n\/\/ Funkcja do aktualizacji wizualizacji za\u0142adowanych godzin\nfunction updateLoadedHoursVisualization() {\n  \/\/ Sprawd\u017a czy jeste\u015bmy w trybie por\u00f3wnywania modeli\n  const compareBtn = document.getElementById(\"compareBtn\");\n  const isCompareMode = compareBtn && compareBtn.classList.contains(\"active\");\n  \n  let hoursLoaded;\n  \n  if (isCompareMode) {\n    \/\/ W trybie por\u00f3wnania - godzina jest za\u0142adowana tylko gdy jest za\u0142adowana dla wszystkich modeli (kt\u00f3re maj\u0105 t\u0119 godzin\u0119)\n    hoursLoaded = new Set();\n    \n    \/\/ Pobierz wszystkie dost\u0119pne godziny dla najd\u0142u\u017cszego modelu\n    const allHours = getMaxForecastHours(selectedTerm);\n    \n    allHours.forEach(hour => {\n      const hourStr = String(hour).padStart(2, \"0\");\n      let isLoadedForAllModels = true;\n      \n      \/\/ Sprawd\u017a ka\u017cdy model\n      models.forEach(model => {\n        const modelHours = forecastHours[model]?.[selectedTerm] || forecastHours[\"default\"];\n        const hourNum = parseInt(hourStr, 10);\n        \n        \/\/ Je\u015bli model ma t\u0119 godzin\u0119, sprawd\u017a czy jest za\u0142adowana\n        if (modelHours.includes(hourNum)) {\n          const configKey = `${model}_${selectedTerm}_${selectedParam}_${selectedPressureLevel}`;\n          const modelLoadedHours = loadedHours[configKey] || new Set();\n          \n          if (!modelLoadedHours.has(hourStr)) {\n            isLoadedForAllModels = false;\n          }\n        }\n      });\n      \n      \/\/ Je\u015bli za\u0142adowana dla wszystkich odpowiednich modeli, dodaj do zestawu\n      if (isLoadedForAllModels) {\n        hoursLoaded.add(hourStr);\n      }\n    });\n  } else {\n    \/\/ Tryb pojedynczego modelu\n    const configKey = `${selectedModel}_${selectedTerm}_${selectedParam}_${selectedPressureLevel}`;\n    hoursLoaded = loadedHours[configKey] || new Set();\n  }\n  \n  \/\/ Aktualizacja przycisk\u00f3w godzin\n  const hourButtons = document.querySelectorAll('#hours button');\n  hourButtons.forEach(button => {\n    const hour = button.dataset.value;\n    if (hoursLoaded.has(hour)) {\n      button.classList.add('loaded');\n    } else {\n      button.classList.remove('loaded');\n    }\n  });\n  \n  \/\/ Aktualizacja slidera - oblicz procent za\u0142adowanych godzin\n  let hours = [];\n  \n  if (isCompareMode) {\n    \/\/ W trybie por\u00f3wnywania u\u017cywamy najd\u0142u\u017cszego zakresu prognozy\n    hours = getMaxForecastHours(selectedTerm);\n  } else {\n    if (forecastHours[selectedModel] && forecastHours[selectedModel][selectedTerm]) {\n      hours = forecastHours[selectedModel][selectedTerm];\n    } else {\n      hours = forecastHours[\"default\"];\n    }\n  }\n  \n  if (hours.length > 0) {\n    \/\/ Znajd\u017a najd\u0142u\u017cszy ci\u0105g\u0142y ci\u0105g za\u0142adowanych godzin OD POCZ\u0104TKU\n    let continuousLoadedIndex = -1;\n    for (let index = 0; index < hours.length; index++) {\n      const hourStr = String(hours[index]).padStart(2, \"0\");\n      if (hoursLoaded.has(hourStr)) {\n        continuousLoadedIndex = index;\n      } else {\n        \/\/ Przerwij gdy napotkamy pierwsz\u0105 nieza\u0142adowan\u0105 godzin\u0119\n        break;\n      }\n    }\n    \n    const slider = document.getElementById('forecastHourSlider');\n    if (slider && continuousLoadedIndex >= 0) {\n      const percentage = ((continuousLoadedIndex + 1) \/ hours.length) * 100;\n      slider.style.background = `linear-gradient(to right, #b0b0b0 0%, #b0b0b0 ${percentage}%, #e0e0e0 ${percentage}%, #e0e0e0 100%)`;\n    }\n  }\n}\n\n\/\/ Zmienna do \u015bledzenia ostatniego wy\u015bwietlonego obrazu\nlet lastDisplayedImage = {\n  model: \"\",\n  term: \"\",\n  param: \"\",\n  hour: \"\",\n  stat: \"\",\n  pressureLevel: \"\"\n};\n\n\/\/ Bufor za\u0142adowanych obraz\u00f3w dla optymalizacji\nconst imageCache = {};\n\n\/\/ Zmienna do \u015bledzenia, czy funkcja showImages jest aktualnie wykonywana\nlet isShowImagesRunning = false;\nlet isShowImagesRunningTimestamp = 0;\n\/\/ Zmienne do \u015bledzenia ostatniego \u017c\u0105dania obrazu\nlet latestImageRequest = null;\nlet pendingImageRequest = false;\n\n\/\/ Funkcja do od\u015bwie\u017cania widoku por\u00f3wnania modeli\nfunction refreshCompareView() {\n  const compareBtn = document.getElementById(\"compareBtn\");\n  const imagesContainer = document.getElementById(\"images\");\n  \n  if (!compareBtn || !compareBtn.classList.contains(\"active\")) {\n    return; \/\/ Tryb por\u00f3wnania nie jest aktywny\n  }\n  \n  \/\/ Zaznacz wszystkie modele jako aktywne\n  const modelButtons = document.querySelectorAll('#models button');\n  modelButtons.forEach(btn => {\n    btn.classList.add('active');\n  });\n  \n  \/\/ Utw\u00f3rz siatk\u0119 2x2 dla 4 modeli\n  imagesContainer.style.display = \"grid\";\n  imagesContainer.style.gridTemplateColumns = \"1fr 1fr\";\n  imagesContainer.style.gap = \"10px\";\n  \n  \/\/ Wyczy\u015b\u0107 kontener\n  imagesContainer.innerHTML = \"\";\n  \n  \/\/ Dla ka\u017cdego modelu utw\u00f3rz kontener z obrazem\n  models.forEach((model, index) => {\n    const modelContainer = document.createElement(\"div\");\n    modelContainer.style.position = \"relative\";\n    modelContainer.style.border = \"2px solid \" + (model === selectedModel ? \"rgba(86, 221, 208, 1)\" : \"#ddd\");\n    modelContainer.style.borderRadius = \"8px\";\n    modelContainer.style.padding = \"5px\";\n    modelContainer.style.backgroundColor = \"#f9f9f9\";\n    \n    \/\/ Dodaj etykiet\u0119 modelu\n    const label = document.createElement(\"div\");\n    label.textContent = modelDisplayNames[model];\n    label.style.position = \"absolute\";\n    label.style.top = \"10px\";\n    label.style.left = \"10px\";\n    label.style.background = \"rgba(0, 0, 0, 0.7)\";\n    label.style.color = \"white\";\n    label.style.padding = \"5px 10px\";\n    label.style.borderRadius = \"4px\";\n    label.style.fontSize = \"0.9em\";\n    label.style.fontWeight = \"bold\";\n    label.style.zIndex = \"10\";\n    \n    \/\/ Sprawd\u017a czy wybrana godzina jest dost\u0119pna dla tego modelu\n    const modelHours = forecastHours[model]?.[selectedTerm] || forecastHours[\"default\"];\n    const selectedHourNum = parseInt(selectedHour, 10);\n    const isHourAvailable = modelHours.includes(selectedHourNum);\n    \n    \/\/ Utw\u00f3rz element obrazu\n    const img = document.createElement(\"img\");\n    img.style.width = \"100%\";\n    img.style.height = \"auto\";\n    img.style.display = \"block\";\n    img.style.borderRadius = \"4px\";\n    \n    if (!isHourAvailable) {\n      \/\/ Je\u015bli godzina nie jest dost\u0119pna dla tego modelu, wy\u015bwietl informacj\u0119\n      img.style.display = \"none\";\n      const message = document.createElement(\"div\");\n      message.textContent = `Prognoza niedost\u0119pna dla ${selectedHour}h`;\n      message.style.padding = \"20px\";\n      message.style.textAlign = \"center\";\n      message.style.color = \"#666\";\n      message.style.fontSize = \"0.9em\";\n      modelContainer.appendChild(label);\n      modelContainer.appendChild(message);\n      imagesContainer.appendChild(modelContainer);\n      return;\n    }\n    \n    \/\/ Generuj nazw\u0119 pliku dla tego modelu\n    const fileName = generateFileName(model, selectedTerm, selectedParam, selectedHour, selectedStat, selectedPressureLevel);\n    const imagePath = `\/wp-content\/uploads\/production\/mapyGorne\/${selectedTerm}\/${fileName}`;\n    \n    img.src = imagePath;\n    img.alt = modelDisplayNames[model];\n    \n    \/\/ Obs\u0142uga b\u0142\u0119d\u00f3w \u0142adowania - pr\u00f3buj z wczorajsz\u0105 dat\u0105\n    img.onerror = function() {\n      if (!this.dataset.triedYesterday) {\n        this.dataset.triedYesterday = \"true\";\n        const yesterdayFileName = generateFileName(model, selectedTerm, selectedParam, selectedHour, selectedStat, selectedPressureLevel, true);\n        this.src = `\/wp-content\/uploads\/production\/mapyGorne\/${selectedTerm}\/${yesterdayFileName}`;\n      } else {\n        this.src = \"\/wp-content\/uploads\/production\/mapyGorne\/configuration_not.png\";\n      }\n    };\n    \n    modelContainer.appendChild(label);\n    modelContainer.appendChild(img);\n    imagesContainer.appendChild(modelContainer);\n  });\n}\n\nfunction showImages() {\n  if (!selectedModel || !selectedTerm || !selectedParam || !selectedHour) return;\n  \/\/ Nowe modele nie wymagaj\u0105 selectedStat (mo\u017ce by\u0107 null)\n  \n  \/\/ Sprawd\u017a czy tryb por\u00f3wnania jest aktywny\n  const compareBtn = document.getElementById(\"compareBtn\");\n  if (compareBtn && compareBtn.classList.contains(\"active\")) {\n    refreshCompareView();\n    return;\n  }\n  \n  \/\/ Zapami\u0119taj aktualne \u017c\u0105danie\n  latestImageRequest = {\n    model: selectedModel,\n    term: selectedTerm,\n    param: selectedParam,\n    hour: selectedHour,\n    stat: selectedStat,\n    pressureLevel: selectedPressureLevel\n  };\n  \n  \/\/ Je\u015bli funkcja jest ju\u017c w trakcie wykonywania, zaznacz, \u017ce jest nowe \u017c\u0105danie oczekuj\u0105ce\n  if (isShowImagesRunning) {\n    pendingImageRequest = true;\n    return;\n  }\n  \n  \/\/ Ustaw flag\u0119 na true - funkcja jest teraz wykonywana\n  isShowImagesRunning = true;\n  isShowImagesRunningTimestamp = Date.now();\n  \n  try {\n    \/\/ Generujemy unikalny identyfikator obrazu na podstawie parametr\u00f3w zanim cokolwiek zrobimy\n    const imageId = `img_${selectedModel}_${selectedTerm}_${selectedParam}_${selectedHour}_${selectedStat}_${selectedPressureLevel}`.replace(\/\\s+\/g, '_');\n  \n  \/\/ Sprawd\u017a, czy te same parametry obrazu s\u0105 ju\u017c wy\u015bwietlane (zapobieganie duplikacji)\n  if (lastDisplayedImage && \n      lastDisplayedImage.model === selectedModel &&\n      lastDisplayedImage.term === selectedTerm &&\n      lastDisplayedImage.param === selectedParam &&\n      lastDisplayedImage.hour === selectedHour &&\n      lastDisplayedImage.stat === selectedStat &&\n      lastDisplayedImage.pressureLevel === selectedPressureLevel) {\n    \n    \/\/ Sprawd\u017a, czy obraz faktycznie istnieje w DOM\n    if (document.getElementById(imageId)) {\n      \/\/ Resetuj flag\u0119 wykonywania\n      isShowImagesRunning = false;\n      return; \/\/ Obraz ju\u017c istnieje, przerywamy\n    }\n    \/\/ Je\u015bli obraz nie istnieje mimo \u017ce parametry si\u0119 zgadzaj\u0105, kontynuujemy aby go doda\u0107\n  }\n  \n  \/\/ U\u017cywamy zoptymalizowanego dost\u0119pu do element\u00f3w DOM\n  const container = getElementCached(\"images\");\n  \n  \/\/ Dodatkowe sprawdzenie czy obraz z tym ID ju\u017c istnieje w DOM\n  if (document.getElementById(imageId)) {\n    \/\/ Resetuj flag\u0119 wykonywania\n    isShowImagesRunning = false;\n    return; \/\/ Przerywamy funkcj\u0119 je\u015bli obraz ju\u017c istnieje\n  }\n  \n  \/\/ Sprawd\u017a, czy mamy ju\u017c za\u0142adowany ten obraz w buforze\n  const isCachedImage = imageCache[imageId];\n  \n  \/\/ Tworzymy znacznik identyfikuj\u0105cy to konkretne wywo\u0142anie funkcji\n  const requestId = Date.now();\n  container.dataset.lastRequestId = requestId;\n  \n  \/\/ Je\u015bli obraz jest w buforze, dodajmy go natychmiast bez pokazywania animacji \u0142adowania\n  if (isCachedImage) {\n    \n    \/\/ Sprawd\u017a czy przypadkiem nie jest to dok\u0142adnie ten sam obraz, kt\u00f3ry ju\u017c jest wy\u015bwietlany\n    const existingImages = container.querySelectorAll('img');\n    if (existingImages.length === 1 && existingImages[0].id === imageId) {\n      isShowImagesRunning = false;\n      return;\n    }\n    \n    \/\/ Sprawd\u017a czy ju\u017c jest jaki\u015b obraz w kontenerze\n    if (existingImages.length > 0) {\n      \/\/ Je\u015bli jest, po prostu zamie\u0144 jego src i id - to jest najszybsze i bez migania\n      const existingImg = existingImages[0];\n      existingImg.src = imageCache[imageId].src;\n      existingImg.id = imageId;\n      \n      \/\/ Usu\u0144 pozosta\u0142e obrazy je\u015bli s\u0105 (nie powinny by\u0107, ale dla pewno\u015bci)\n      for (let i = 1; i < existingImages.length; i++) {\n        existingImages[i].remove();\n      }\n    } else {\n      \/\/ Je\u015bli nie ma obrazu, usu\u0144 loadery i dodaj nowy\n      container.innerHTML = '';\n      const cachedImg = imageCache[imageId].cloneNode(true);\n      cachedImg.style.display = \"block\";\n      cachedImg.id = imageId;\n      container.appendChild(cachedImg);\n    }\n    \n    \/\/ Dodaj ostrze\u017cenie o dacie je\u015bli u\u017cywamy wczorajszej prognozy\n    const dateWarning = document.getElementById(\"dateWarning\");\n    if (forecastBaseDateString && forecastBaseDateString !== getDateString(0)) {\n      dateWarning.textContent = \"\u26a0 Prognoza z dnia \" + forecastBaseDateString + \" (najnowsza dost\u0119pna)\";\n      dateWarning.style.display = \"block\";\n    } else {\n      dateWarning.textContent = \"\";\n      dateWarning.style.display = \"none\";\n    }\n    \n    \/\/ Zapisz stan ostatniego wy\u015bwietlonego obrazu\n    lastDisplayedImage = {\n      model: selectedModel,\n      term: selectedTerm,\n      param: selectedParam,\n      hour: selectedHour,\n      stat: selectedStat,\n      pressureLevel: selectedPressureLevel,\n      imageId: imageId\n    };\n    \n    \/\/ WA\u017bNE: Resetuj flag\u0119 wykonywania, poniewa\u017c sko\u0144czyli\u015bmy \u0142adowa\u0107 obraz z cache\n    isShowImagesRunning = false;\n    \n    return; \/\/ Ko\u0144czymy funkcj\u0119 wcze\u015bniej - bez pokazywania loadera\n  }\n  \n  \/\/ Je\u015bli obrazu NIE MA w buforze, najpierw sprawd\u017a dost\u0119pno\u015b\u0107 prognozy\n  \/\/ (tylko je\u015bli jeszcze nie ustalili\u015bmy daty bazowej dla tej konfiguracji)\n  const configKey = selectedModel + \"_\" + selectedTerm + \"_\" + selectedParam;\n  if (forecastBaseDate !== configKey) {\n    \/\/ Sprawd\u017a dost\u0119pno\u015b\u0107 prognozy przed za\u0142adowaniem\n    checkForecastAvailability(selectedModel, selectedTerm, selectedParam, function(baseDate) {\n      if (!baseDate) {\n        \/\/ Prognoza niedost\u0119pna\n        container.innerHTML = \"\";\n        \n        const errorImg = document.createElement(\"img\");\n        errorImg.src = \"\/wp-content\/uploads\/production\/prognozy_wiazkowe\/configuration_not.png\";\n        errorImg.alt = \"Konfiguracja niedost\u0119pna\";\n        errorImg.style.maxWidth = \"100%\";\n        errorImg.style.display = \"block\";\n        container.appendChild(errorImg);\n        \n        const errorMsg = document.createElement(\"p\");\n        errorMsg.textContent = \"Brak dost\u0119pnych prognoz dla wybranej konfiguracji.\";\n        errorMsg.style.color = \"red\";\n        errorMsg.style.textAlign = \"center\";\n        errorMsg.style.marginTop = \"10px\";\n        container.appendChild(errorMsg);\n        \n        isShowImagesRunning = false;\n        return;\n      }\n      \n      \/\/ Mamy dat\u0119 bazow\u0105, za\u0142aduj obrazki\n      showImages(); \/\/ Wywo\u0142aj ponownie, teraz z ustawion\u0105 forecastBaseDateString\n    });\n    return;\n  }\n  \n  \/\/ Je\u015bli obrazu NIE MA w buforze, teraz czy\u015bcimy kontener i pokazujemy loader\n  container.innerHTML = \"\";\n  \n  \/\/ Dodanie identyfikatora i timeout aby usun\u0105\u0107 animacj\u0119 je\u015bli utkn\u0119\u0142a\n  const loaderId = \"loader-\" + new Date().getTime();\n  \n  \/\/ Dodanie loadera do kontenera z unikalnym ID (u\u017cywamy DocumentFragment dla lepszej wydajno\u015bci)\n  const fragment = document.createDocumentFragment();\n  const loaderContainer = document.createElement(\"div\");\n  loaderContainer.className = \"loader-container\";\n  loaderContainer.id = loaderId;\n  \n  const loader = document.createElement(\"div\");\n  loader.className = \"loader\";\n  \n  loaderContainer.appendChild(loader);\n  fragment.appendChild(loaderContainer);\n  container.appendChild(fragment);\n\n  \/\/ Resetowanie timeouta je\u015bli istnieje\n  if (loaderTimeout) {\n    clearTimeout(loaderTimeout);\n  }\n\n  \/\/ je\u015bli animacja \u0142adowania nie zostanie usuni\u0119ta w ci\u0105gu 15 sekund,\n  \/\/ to pokazany b\u0119dzie komunikat o b\u0142\u0119dzie\n  loaderTimeout = setTimeout(function() {\n    const loaderElement = document.getElementById(loaderId);\n    if (loaderElement) {\n      container.innerHTML = \"\";\n      \n      const errorMessage = document.createElement(\"div\");\n      errorMessage.innerHTML = '<h3 style=\"color: red; text-align: center;\">Nie uda\u0142o si\u0119 za\u0142adowa\u0107 obrazu<\/h3>' +\n                               '<p style=\"text-align: center;\">Sprawd\u017a po\u0142\u0105czenie internetowe lub spr\u00f3buj ponownie p\u00f3\u017aniej.<\/p>';\n      container.appendChild(errorMessage);\n      \n      \/\/ Resetuj lastDisplayedImage aby nie blokowa\u0107 ponownych pr\u00f3b wczytania obrazu\n      lastDisplayedImage = {\n        model: \"\",\n        term: \"\",\n        param: \"\",\n        hour: \"\",\n        stat: \"\",\n        pressureLevel: \"\"\n      };\n    }\n  }, 15000); \/\/ 15 sekund\n  \n  \/\/ Generowanie nazwy pliku\n  const fileName = generateFileName(\n    selectedModel, \n    selectedTerm, \n    selectedParam, \n    selectedHour, \n    selectedStat, \n    selectedPressureLevel\n  );\n  \n  \/\/ Formatowanie godziny do wy\u015bwietlenia w nazwie pliku\n  let formattedHour = String(selectedHour).padStart(2, \"0\"); \/\/ Format z wiod\u0105cym zerem\n  \n  \/\/ Formatowanie godziny do wy\u015bwietlenia w tytule\n  let displayHour = formattedHour;\n  \n  \/\/ Aktualizowanie tytu\u0142u slidera\n  const sliderTitle = document.getElementById(\"sliderTitle\");\n  sliderTitle.textContent = \"Godzina prognozy:\";\n  \n  \/\/ Aktualizowanie tytu\u0142u sekcji w lewej kolumnie dla sp\u00f3jno\u015bci\n  const hoursTitle = document.getElementById(\"hoursTitle\");\n  if (hoursTitle) {\n    hoursTitle.textContent = \"Godziny prognozy:\";\n  }\n  \n  \/\/ Ukrywanie g\u0142\u00f3wnego tytu\u0142u sekcji obrazka\n  document.getElementById(\"imageTitle\").textContent = \"\";\n  \n  \/\/ Aktualizacja opisu parametru\n  const descriptionContainer = document.getElementById(\"parameterDescription\");\n  const paramDescription = parameterDescriptions[selectedParam] || `Brak dodatkowego opisu dla parametru ${selectedParam}.`;\n  \/\/ U\u017cyj innerHTML zamiast textContent, aby obs\u0142ugiwa\u0107 tagi HTML i zamie\u0144 \\n na <br>\n  descriptionContainer.innerHTML = paramDescription.replace(\/\\n\/g, '<br>');\n  \n  \/\/ Sprawdzamy, czy obraz jest ju\u017c w buforze\n  if (imageCache[imageId]) {\n    \/\/ Usuni\u0119cie animacji \u0142adowania\n    const loaderElements = container.querySelectorAll('.loader-container');\n    for (let i = 0; i < loaderElements.length; i++) {\n      if (loaderElements[i] && loaderElements[i].parentNode) {\n        container.removeChild(loaderElements[i]);\n      }\n    }\n    \n    \/\/ Zapisz stan ostatniego wy\u015bwietlonego obrazu\n    lastDisplayedImage = {\n      model: selectedModel,\n      term: selectedTerm,\n      param: selectedParam,\n      hour: selectedHour,\n      stat: selectedStat,\n      pressureLevel: selectedPressureLevel,\n      imageId: imageId\n    };\n    \n    \/\/ Klonowanie obrazu z bufora i dodanie go do DOM\n    const cachedImg = imageCache[imageId].cloneNode(true);\n    cachedImg.style.display = \"block\";\n    cachedImg.id = imageId;\n    container.appendChild(cachedImg);\n    \n    \/\/ Wyczy\u015b\u0107 timeout je\u015bli istnieje\n    if (imageLoadTimeout) {\n      clearTimeout(imageLoadTimeout);\n      imageLoadTimeout = null;\n    }\n    \n    return; \/\/ Ko\u0144czymy funkcj\u0119 wcze\u015bniej\n  }\n  \n  \/\/ Utworzenie elementu obrazu\n  const img = new Image();\n  img.id = imageId; \/\/ Ustawienie ID dla obrazu\n  img.style.display = \"none\"; \/\/ Ukrycie obrazka podczas \u0142adowania\n  \n  \/\/ Zapami\u0119tujemy ID \u017c\u0105dania w obrazku\n  img.dataset.requestId = requestId;\n\n  \/\/ Ustawienie callback\u00f3w przed ustawieniem src!\n  img.onload = function() {\n    \n    \/\/ Czyszczenie timeoutu je\u015bli istnieje\n    if (imageLoadTimeout) {\n      clearTimeout(imageLoadTimeout);\n      imageLoadTimeout = null;\n    }\n    \n    \/\/ Sprawd\u017a, czy to jest ostatnie \u017c\u0105danie (czy kontener nie zosta\u0142 zast\u0105piony przez nowsze \u017c\u0105danie)\n    if (container.dataset.lastRequestId !== this.dataset.requestId) {\n      return;\n    }\n    \n    \/\/ Usuni\u0119cie animacji \u0142adowania - bezpieczna metoda\n    const loaderElements = container.querySelectorAll('.loader-container');\n    for (let i = 0; i < loaderElements.length; i++) {\n      if (loaderElements[i] && loaderElements[i].parentNode) {\n        container.removeChild(loaderElements[i]);\n      }\n    }\n    \n    \/\/ Wyczy\u015b\u0107 kontener przed dodaniem obrazka, aby upewni\u0107 si\u0119, \u017ce nie ma kilku obraz\u00f3w\n    container.innerHTML = '';\n    \n    \/\/ Pokazanie za\u0142adowanego obrazka\n    img.style.display = \"block\";\n    container.appendChild(img);\n    \n    \/\/ Dodaj ostrze\u017cenie o dacie je\u015bli u\u017cywamy wczorajszej prognozy\n    const dateWarning = document.getElementById(\"dateWarning\");\n    if (forecastBaseDateString && forecastBaseDateString !== getDateString(0)) {\n      dateWarning.textContent = \"\u26a0 Prognoza z dnia \" + forecastBaseDateString + \" (najnowsza dost\u0119pna)\";\n      dateWarning.style.display = \"block\";\n    } else {\n      dateWarning.textContent = \"\";\n      dateWarning.style.display = \"none\";\n    }\n    \n    \/\/ Zapisz stan ostatniego wy\u015bwietlonego obrazu\n    lastDisplayedImage = {\n      model: selectedModel,\n      term: selectedTerm,\n      param: selectedParam,\n      hour: selectedHour,\n      stat: selectedStat,\n      pressureLevel: selectedPressureLevel,\n      imageId: imageId\n    };\n    \n    \/\/ Zapisanie obrazu do bufora (zachowujemy maksymalnie 20 obraz\u00f3w)\n    if (Object.keys(imageCache).length >= 20) {\n      \/\/ Usuwamy pierwszy element (najstarszy)\n      const firstKey = Object.keys(imageCache)[0];\n      delete imageCache[firstKey];\n    }\n    imageCache[imageId] = img.cloneNode(true);\n    \n    \/\/ Oznacz godzin\u0119 jako za\u0142adowan\u0105\n    markHourAsLoaded(selectedModel, selectedTerm, selectedParam, selectedHour, selectedPressureLevel);\n  };\n  \n  \/\/ Dodanie obs\u0142ugi b\u0142\u0119d\u00f3w dla obrazka\n  img.onerror = function() {\n    console.log(\"B\u0142\u0105d \u0142adowania obrazka:\", fileName);\n    \n    \/\/ Czyszczenie timeoutu je\u015bli istnieje\n    if (imageLoadTimeout) {\n      clearTimeout(imageLoadTimeout);\n      imageLoadTimeout = null;\n    }\n    \n    \/\/ Sprawd\u017a, czy to jest ostatnie \u017c\u0105danie (czy kontener nie zosta\u0142 zast\u0105piony przez nowsze \u017c\u0105danie)\n    if (container.dataset.lastRequestId !== this.dataset.requestId) {\n      console.log(\"Pomijam obs\u0142ug\u0119 b\u0142\u0119du - jest ju\u017c nowsze \u017c\u0105danie\");\n      return;\n    }\n    \n    \/\/ Resetowanie wszystkich flag wykonywania w przypadku b\u0142\u0119du\n    isShowImagesRunning = false;\n    isShowImagesRunningTimestamp = 0;\n    isRenderingForecastHours = false;\n    isRenderingForecastHoursTimestamp = 0;\n    isPreloadingImages = false;\n    isPreloadingImagesTimestamp = 0;\n    isLoadingImages = false;\n    \n    \/\/ Natychmiastowe czyszczenie kontenera\n    container.innerHTML = '';\n    \n    \/\/ Reset stanu ostatnio wy\u015bwietlanego obrazu w przypadku b\u0142\u0119du - ustawiamy pusty obiekt zamiast null\n    lastDisplayedImage = {\n      model: \"\",\n      term: \"\",\n      param: \"\",\n      hour: \"\",\n      stat: \"\",\n      pressureLevel: \"\"\n    };\n    \n    try {\n      \/\/ Tworzenie obrazka zast\u0119pczego\n      const errorImg = document.createElement(\"img\");\n      errorImg.src = \"\/wp-content\/uploads\/production\/prognozy_wiazkowe\/configuration_not.png\";\n      errorImg.alt = \"Konfiguracja niedost\u0119pna\";\n      errorImg.style.maxWidth = \"100%\";\n      errorImg.style.display = \"block\";\n      container.appendChild(errorImg);\n      \/\/ Dodanie komunikatu o b\u0142\u0119dzie\n      const errorMsg = document.createElement(\"p\");\n      errorMsg.textContent = \"Brak dost\u0119pnych prognoz dla wybranej konfiguracji.\";\n      errorMsg.style.color = \"red\";\n      errorMsg.style.textAlign = \"center\";\n      container.appendChild(errorMsg);\n      \/\/ Dodanie obs\u0142ugi b\u0142\u0119du dla obrazka zast\u0119pczego\n      errorImg.onerror = function() {\n        console.log(\"Nie mo\u017cna za\u0142adowa\u0107 obrazka zast\u0119pczego\");\n        this.style.display = \"none\";\n      };\n    } catch (e) {\n      console.error(\"B\u0142\u0105d podczas obs\u0142ugi b\u0142\u0119du \u0142adowania obrazka:\", e);\n      \/\/ Ostateczne rozwi\u0105zanie\n      container.innerHTML = '<div style=\"color: red; text-align: center; padding: 20px;\"><h3>Brak dost\u0119pnej prognozy<\/h3><p>Nie znaleziono prognozy dla wybranych parametr\u00f3w.<\/p><\/div>';\n    }\n  }\n  \n  \/\/ Ustawienie timeoutu dla obrazka\n  if (imageLoadTimeout) {\n    clearTimeout(imageLoadTimeout);\n  }\n  \n  \/\/ Ustawienie nowego timeoutu na 20 sekund\n  imageLoadTimeout = setTimeout(function() {\n    console.log(\"Timeout dla obrazka:\", fileName);\n    \/\/ Je\u015bli obrazek jeszcze si\u0119 \u0142aduje, wywo\u0142ujemy b\u0142\u0105d\n    if (!img.complete) {\n      img.onerror(); \/\/ Wywo\u0142ujemy funkcj\u0119 obs\u0142ugi b\u0142\u0119du r\u0119cznie\n    }\n  }, 20000); \/\/ 20 sekund na za\u0142adowanie\n  \n  \/\/ Ustawienie src na ko\u0144cu\n  const fullImagePath = `\/wp-content\/uploads\/production\/mapyGorne\/${selectedTerm}\/${fileName}`;\n  img.src = fullImagePath;\n  img.alt = fileName;\n  } finally {\n    \/\/ Zawsze resetuj flag\u0119 wykonywania na ko\u0144cu, nawet w przypadku b\u0142\u0119du\n    setTimeout(() => {\n      isShowImagesRunning = false;\n      isShowImagesRunningTimestamp = 0;\n      \n      \/\/ Sprawd\u017a, czy pojawi\u0142o si\u0119 nowe \u017c\u0105danie podczas wykonywania tej funkcji\n      if (pendingImageRequest) {\n        pendingImageRequest = false;\n        \n        \/\/ Natychmiast wczytaj najnowszy \u017c\u0105dany obraz\n        setTimeout(() => {\n          showImages();\n        }, 10); \/\/ Bardzo kr\u00f3tki timeout\n      }\n    }, 10); \/\/ Bardzo kr\u00f3tki timeout, aby szybko reagowa\u0107 na zmiany\n  }\n}\n\n\/\/ Funkcja do wst\u0119pnego \u0142adowania danych dla modelu\nfunction preloadModelData(model) {\n  return new Promise((resolve) => {\n    \n    \/\/ Zapisz, \u017ce model jest preloadowany\n    preloadedModels[model] = true;\n    \n    \/\/ Ustal domy\u015blne warto\u015bci dla tego modelu\n    const term = defaultSettings[model].term;\n    const param = defaultSettings[model].param;\n    const stat = defaultSettings[model].stat;\n    \n    \/\/ Dla wszystkich modeli standardowych, preloaduj pierwszy obrazek\n    const hours = forecastHours[model]?.[term] || forecastHours[\"default\"];\n    if (hours && hours.length > 0) {\n      const firstHour = String(hours[0]).padStart(2, \"0\");\n      const fileName = generateFileName(model, term, param, firstHour, stat, null);\n      \n      \/\/ Wst\u0119pnie za\u0142aduj tylko pierwszy obrazek\n      const img = new Image();\n      const imagePath = `\/wp-content\/uploads\/production\/mapyGorne\/${term}\/${fileName}`;\n      \n      img.onload = () => setTimeout(resolve, 100);\n      img.onerror = () => {\n        \/\/ Spr\u00f3buj z wczorajsz\u0105 dat\u0105\n        if (!img.dataset || !img.dataset.triedYesterday) {\n          img.dataset = img.dataset || {};\n          img.dataset.triedYesterday = \"true\";\n          const yesterdayFileName = generateFileName(model, term, param, firstHour, stat, null, true);\n          img.src = `\/wp-content\/uploads\/production\/mapyGorne\/${term}\/${yesterdayFileName}`;\n        } else {\n          setTimeout(resolve, 100);\n        }\n      };\n      \n      img.src = imagePath;\n    } else {\n      resolve();\n    }\n  });\n}\n\n\/\/ Funkcja do aktualizacji wszystkich aktywnych przycisk\u00f3w\nfunction updateAllActiveButtons() {\n  \n  \/\/ Usuni\u0119cie starych przycisk\u00f3w i renderowanie nowych z aktywnym zaznaczeniem\n  renderButtons(\"models\", models, (model) => {\n    const previousModel = selectedModel;\n    \n    \/\/ Zapisz poprzednie warto\u015bci\n    let prevParam = isParameterWithPressureLevel(selectedParam) ? getBaseParam(selectedParam) : selectedParam;\n    const prevStat = selectedStat;\n    const prevHour = selectedHour;\n    const prevTerm = selectedTerm;\n    \n    selectedModel = model;\n    \n    \/\/ Zachowaj termin dla zwyk\u0142ych modeli\n    if (!false) {\n      if (prevTerm && termsDefault.includes(prevTerm)) {\n        selectedTerm = prevTerm;\n      } else {\n        selectedTerm = defaultSettings[model].term;\n      }\n    } else {\n      selectedTerm = defaultSettings[model].term;\n    }\n    \n    \/\/ Sprawd\u017a czy poprzedni parametr jest dost\u0119pny w nowym modelu\n    \/\/ Pobierz list\u0119 parametr\u00f3w dla nowego modelu\n    let availableParams = paramsForModels[model] || [];\n    \n    \/\/ Filtruj parametry (usu\u0144 te z poziomem ci\u015bnienia)\n    let filteredParams = [];\n    let baseParamsAdded = new Set();\n    availableParams.forEach(param => {\n      if (\/^[A-Z]$\/.test(param)) {\n        if (!baseParamsAdded.has(param)) {\n          filteredParams.push(param);\n          baseParamsAdded.add(param);\n        }\n      } else if (isParameterWithPressureLevel(param)) {\n        const baseParam = getBaseParam(param);\n        if (!baseParamsAdded.has(baseParam)) {\n          filteredParams.push(baseParam);\n          baseParamsAdded.add(baseParam);\n        }\n      } else {\n        filteredParams.push(param);\n      }\n    });\n    \n    \/\/ Je\u015bli poprzedni parametr jest dost\u0119pny w nowym modelu, zachowaj go\n    if (prevParam && filteredParams.includes(prevParam)) {\n      selectedParam = prevParam;\n    } else {\n      \/\/ W przeciwnym razie u\u017cyj domy\u015blnego parametru\n      selectedParam = defaultSettings[model].param;\n    }\n    \n    \/\/ Okre\u015bl dost\u0119pne godziny dla nowego modelu\n    let availableHours = [];\n    if (forecastHours[model] && forecastHours[model][selectedTerm]) {\n      availableHours = forecastHours[model][selectedTerm].map(h => String(h).padStart(2, \"0\"));\n    } else {\n      availableHours = forecastHours[\"default\"].map(h => String(h).padStart(2, \"0\"));\n    }\n    \n    \/\/ Zachowaj godzin\u0119 je\u015bli jest dost\u0119pna w nowym modelu\n    if (prevHour) {\n      const prevHourNormalized = String(prevHour).padStart(2, \"0\");\n      const prevHourInt = parseInt(String(prevHour), 10);\n      \n      \/\/ Sprawd\u017a czy poprzednia godzina jest dost\u0119pna\n      const isAvailable = availableHours.some(h => {\n        const hNormalized = String(h).padStart(2, \"0\");\n        const hInt = parseInt(String(h), 10);\n        return hNormalized === prevHourNormalized || hInt === prevHourInt;\n      });\n      \n      if (isAvailable) {\n        selectedHour = prevHourNormalized;\n      } else {\n        selectedHour = String(defaultSettings[model].hour).padStart(2, \"0\");\n      }\n    } else {\n      selectedHour = String(defaultSettings[model].hour).padStart(2, \"0\");\n    }\n    \n    \/\/ Zachowaj statystyk\u0119 dla zwyk\u0142ych modeli\n    if (!false) {\n      const availableStats = suffixConfig[selectedParam] || suffixConfig[\"default\"];\n      \n      if (prevStat && availableStats.includes(prevStat)) {\n        selectedStat = prevStat;\n      } else {\n        selectedStat = defaultSettings[model].stat;\n      }\n    } else {\n      selectedStat = null; \/\/ Modele podsumowania nie maj\u0105 statystyk\n    }\n    \n    updateTermLabel();\n    \n    \/\/ Wyczy\u015b\u0107 obrazy i poka\u017c animacj\u0119 \u0142adowania, je\u015bli jest to zmiana modelu\n    if (previousModel !== model) {\n      resetStateForModelChange();\n      \n      const imagesContainer = document.getElementById(\"images\");\n      if (imagesContainer) {\n        imagesContainer.innerHTML = \"\";\n        if (!preloadedModels[model]) {\n          const loaderContainer = document.createElement(\"div\");\n          loaderContainer.className = \"loader-container\";\n          const loader = document.createElement(\"div\");\n          loader.className = \"loader\";\n          loaderContainer.appendChild(loader);\n          const loadingText = document.createElement(\"p\");\n          loadingText.textContent = \"\u0141adowanie prognozy...\";\n          loadingText.style.marginTop = \"10px\";\n          loaderContainer.appendChild(loadingText);\n          imagesContainer.appendChild(loaderContainer);\n        }\n      }\n    }\n\n    \/\/ Wszystkie modele standardowe - aktualizacja interfejsu\n    updateTermButtons();\n    renderForecastHours(); \/\/ WA\u017bNE: Renderuj godziny aby pokaza\u0107 zachowan\u0105 godzin\u0119!\n    \n    \/\/ Od\u015bwie\u017c widok pe\u0142noekranowy je\u015bli jest aktywny\n    const overlay = document.getElementById(\"fullscreenOverlay\");\n    if (overlay && overlay.classList.contains(\"active\")) {\n      \/\/ U\u017cywamy wi\u0119kszego timeoutu i wielokrotnych pr\u00f3b\n      setTimeout(() => refreshFullscreen(), 300);\n      setTimeout(() => refreshFullscreen(), 600);\n      setTimeout(() => refreshFullscreen(), 1000);\n    }\n    \n    \/\/ Dodajemy model do listy preloadowanych\n    preloadedModels[model] = true;\n  }, selectedModel, modelDisplayNames);\n  \n  \/\/ Je\u015bli mamy slider z poziomami ci\u015bnienia i ustawiony poziom, aktualizuje jego warto\u015b\u0107\n  if (document.getElementById(\"pressureLevelSliderSection\").style.display !== \"none\" && selectedPressureLevel) {\n    const levelIndex = pressureLevels.indexOf(selectedPressureLevel);\n    if (levelIndex !== -1) {\n      document.getElementById(\"pressureLevelSlider\").value = levelIndex;\n      \n      if (selectedParam === \"SHEAR\") {\n        const displayValue = selectedPressureLevel.replace(\/^0_\/, '');\n        document.getElementById(\"pressureValue\").textContent = displayValue + \" m\";\n      } else if (selectedParam === \"TW1000\") {\n        const displayValue = selectedPressureLevel.replace(\/^1000_\/, '');\n        document.getElementById(\"pressureValue\").textContent = displayValue + \" hPa\";\n      } else {\n        document.getElementById(\"pressureValue\").textContent = selectedPressureLevel + \" hPa\";\n      }\n    }\n  }\n}\n\n\/\/ Funkcja do parsowania parametr\u00f3w URL\nfunction parseURLParams() {\n  const urlParams = new URLSearchParams(window.location.search);\n  const params = {};\n  \n  \/\/ Pobierz wszystkie parametry z URL\n  for (const [key, value] of urlParams) {\n    params[key] = value;\n  }\n  \n  return params;\n}\n\n\/\/ Funkcja do walidacji i zastosowania parametr\u00f3w z URL\nfunction validateAndApplyURLParams() {\n  const params = parseURLParams();\n  \n  \/\/ W przypadku braku parametr\u00f3w w URL, stosowane s\u0105 domy\u015blne warto\u015bci\n  if (Object.keys(params).length === 0) {\n    return false;\n  }\n  \n  \/\/ Walidacja modelu\n  if (params.model && isValidModel(params.model)) {\n    selectedModel = params.model;\n  } else if (params.model) {\n    console.warn(`Nieprawid\u0142owy model: ${params.model}, u\u017cywam domy\u015blnego.`);\n  }\n  \n  \/\/ Walidacja terminu startu\n  if (params.term && isValidTerm(params.term, selectedModel)) {\n    selectedTerm = params.term;\n  } else if (params.term) {\n    console.warn(`Nieprawid\u0142owy termin startu: ${params.term}, u\u017cywam domy\u015blnego dla modelu ${selectedModel}.`);\n    selectedTerm = defaultSettings[selectedModel].term;\n  } else {\n    selectedTerm = defaultSettings[selectedModel].term;\n  }\n  \n  \/\/ Walidacja parametru\n  if (params.param && isValidParam(params.param, selectedModel, selectedTerm)) {\n    selectedParam = params.param;\n  } else if (params.param) {\n    console.warn(`Nieprawid\u0142owy parametr: ${params.param}, u\u017cywam domy\u015blnego dla modelu ${selectedModel}.`);\n    selectedParam = defaultSettings[selectedModel].param;\n  } else {\n    selectedParam = defaultSettings[selectedModel].param;\n  }\n  \n  \/\/ Walidacja statystyki (nowe modele nie maj\u0105 statystyk)\n  if (false) {\n    if (params.stat && isValidStat(params.stat, selectedParam)) {\n      selectedStat = params.stat;\n    } else if (params.stat) {\n      console.warn(`Nieprawid\u0142owa statystyka: ${params.stat}, u\u017cywam domy\u015blnej dla parametru ${selectedParam}.`);\n      selectedStat = defaultSettings[selectedModel].stat;\n    } else {\n      selectedStat = defaultSettings[selectedModel].stat;\n    }\n  }\n  \n  \/\/ Walidacja terminu prognozy\n  if (params.hour && isValidHour(params.hour, selectedModel, selectedTerm)) {\n    selectedHour = params.hour;\n  } else if (params.hour) {\n    console.warn(`Nieprawid\u0142owy termin prognozy: ${params.hour}, u\u017cywam domy\u015blnego.`);\n    selectedHour = defaultSettings[selectedModel].hour;\n  } else {\n    selectedHour = defaultSettings[selectedModel].hour;\n  }\n  \n  \/\/ Poziom ci\u015bnienia (opcjonalny)\n  if (params.level && pressureLevels.includes(params.level)) {\n    selectedPressureLevel = params.level;\n  }\n  \n  return true;\n}\n\n\/\/ Funkcja sprawdzaj\u0105ca czy model jest prawid\u0142owy\nfunction isValidModel(model) {\n  return models.includes(model);\n}\n\n\/\/ Funkcja sprawdzaj\u0105ca czy termin jest prawid\u0142owy dla wybranego modelu\nfunction isValidTerm(term, model) {\n  return termsDefault.includes(term);\n}\n\n\/\/ Funkcja sprawdzaj\u0105ca czy parametr jest prawid\u0142owy dla wybranego modelu\nfunction isValidParam(param, model, term) {\n  return (paramsForModels[model] && paramsForModels[model].includes(param));\n}\n\n\/\/ Funkcja sprawdzaj\u0105ca czy statystyka jest prawid\u0142owa dla wybranego parametru\nfunction isValidStat(stat, param) {\n  const stats = suffixConfig[param] || suffixConfig[\"default\"];\n  return stats.includes(stat);\n}\n\n\/\/ Funkcja sprawdzaj\u0105ca czy termin prognozy jest prawid\u0142owy dla wybranego modelu\nfunction isValidHour(hour, model, term) {\n  const hours = forecastHours[\"default\"];\n  return hours.map(String).includes(hour);\n}\n\n\/\/ Zmienne do \u015bledzenia stanu gotowo\u015bci aplikacji\nlet isAppReady = false;\nlet preloadedModels = {};\nlet initialLoadDone = false;\n\n\/\/ Zmienne do kontroli op\u00f3\u017anienia skr\u00f3t\u00f3w klawiszowych\nlet lastKeyPressTime = 0;\nconst keyPressDelay = 130; \/\/ Op\u00f3\u017anienie w milisekundach (130ms)\n\n\/\/ Funkcja od\u015bwie\u017caj\u0105ca widok pe\u0142noekranowy\nfunction refreshFullscreen() {\n  const overlay = document.getElementById(\"fullscreenOverlay\");\n  if (!overlay || !overlay.classList.contains(\"active\")) {\n    return; \/\/ Nie jeste\u015bmy w trybie pe\u0142noekranowym\n  }\n  \n  const fullscreenImages = document.getElementById(\"fullscreenImages\");\n  const fullscreenTitle = document.getElementById(\"fullscreenTitle\");\n  const compareBtn = document.getElementById(\"compareBtn\");\n  const isCompareMode = compareBtn && compareBtn.classList.contains(\"active\");\n  \n  \/\/ Pobierz nazwy do wy\u015bwietlenia\n  const modelName = modelDisplayNames[selectedModel] || selectedModel;\n  const paramName = parameterDisplayNames[selectedParam] || selectedParam;\n  const termText = selectedTerm ? selectedTerm.substring(0, 8) + \" \" + selectedTerm.substring(8) + \" UTC\" : \"\";\n  const hourText = selectedHour ? `+${selectedHour}h` : \"\";\n  \n  \/\/ Aktualizuj tytu\u0142\n  if (isCompareMode) {\n    fullscreenTitle.textContent = `Por\u00f3wnanie modeli - ${paramName} - Termin: ${termText} - ${hourText}`;\n  } else {\n    fullscreenTitle.textContent = `${modelName} - ${paramName} - Termin: ${termText} - ${hourText}`;\n  }\n  \n  \/\/ Wyczy\u015b\u0107 zawarto\u015b\u0107\n  fullscreenImages.innerHTML = \"\";\n  \n  if (isCompareMode) {\n    \/\/ Tryb por\u00f3wnania - poka\u017c wszystkie 4 modele w siatce 2x2\n    fullscreenImages.style.display = \"grid\";\n    fullscreenImages.style.gridTemplateColumns = \"1fr 1fr\";\n    fullscreenImages.style.gap = \"15px\";\n    fullscreenImages.style.padding = \"20px\";\n    fullscreenImages.style.justifyContent = \"center\";\n    fullscreenImages.style.alignItems = \"center\";\n    \n    models.forEach((model) => {\n      const modelContainer = document.createElement(\"div\");\n      modelContainer.style.position = \"relative\";\n      modelContainer.style.display = \"flex\";\n      modelContainer.style.flexDirection = \"column\";\n      modelContainer.style.alignItems = \"center\";\n      \n      const modelLabel = document.createElement(\"div\");\n      modelLabel.textContent = modelDisplayNames[model] || model;\n      modelLabel.style.cssText = `\n        padding: 8px 15px;\n        background: rgba(0, 0, 0, 0.8);\n        color: white;\n        border-radius: 6px;\n        font-weight: bold;\n        margin-bottom: 10px;\n        font-size: 1.1em;\n      `;\n      \n      \/\/ Sprawd\u017a czy wybrana godzina jest dost\u0119pna dla tego modelu\n      const modelHours = forecastHours[model]?.[selectedTerm] || forecastHours[\"default\"];\n      const selectedHourNum = parseInt(selectedHour, 10);\n      const isHourAvailable = modelHours.includes(selectedHourNum);\n      \n      if (!isHourAvailable) {\n        \/\/ Je\u015bli godzina nie jest dost\u0119pna dla tego modelu, wy\u015bwietl informacj\u0119\n        const message = document.createElement(\"div\");\n        message.textContent = `Prognoza niedost\u0119pna dla ${selectedHour}h`;\n        message.style.cssText = `\n          padding: 40px;\n          text-align: center;\n          color: rgba(255, 255, 255, 0.7);\n          font-size: 1.1em;\n          background: rgba(0, 0, 0, 0.3);\n          border-radius: 8px;\n          border: 2px dashed rgba(255, 255, 255, 0.3);\n        `;\n        modelContainer.appendChild(modelLabel);\n        modelContainer.appendChild(message);\n        fullscreenImages.appendChild(modelContainer);\n        return;\n      }\n      \n      const img = document.createElement(\"img\");\n      const fileName = generateFileName(model, selectedTerm, selectedParam, selectedHour, selectedStat, selectedPressureLevel);\n      const imagePath = `\/wp-content\/uploads\/production\/mapyGorne\/${selectedTerm}\/${fileName}`;\n      \n      img.src = imagePath;\n      img.alt = `${model} - ${selectedParam}`;\n      img.style.cssText = `\n        width: 100%;\n        max-height: 50vh;\n        object-fit: contain;\n        border: 3px solid ${model === selectedModel ? 'rgba(86, 221, 208, 1)' : 'rgba(255, 255, 255, 0.3)'};\n        border-radius: 8px;\n      `;\n      \n      img.onerror = function() {\n        if (!this.dataset.triedYesterday) {\n          this.dataset.triedYesterday = \"true\";\n          const yesterdayFileName = generateFileName(model, selectedTerm, selectedParam, selectedHour, selectedStat, selectedPressureLevel, true);\n          this.src = `\/wp-content\/uploads\/production\/mapyGorne\/${selectedTerm}\/${yesterdayFileName}`;\n        } else {\n          this.src = \"\/wp-content\/uploads\/production\/configuration_not.png\";\n        }\n      };\n      \n      modelContainer.appendChild(modelLabel);\n      modelContainer.appendChild(img);\n      fullscreenImages.appendChild(modelContainer);\n    });\n  } else {\n    \/\/ Tryb pojedynczego modelu - poka\u017c jeden du\u017cy obraz\n    fullscreenImages.style.display = \"flex\";\n    fullscreenImages.style.gridTemplateColumns = \"\";\n    fullscreenImages.style.justifyContent = \"center\";\n    fullscreenImages.style.alignItems = \"center\";\n    fullscreenImages.style.padding = \"20px\";\n    \n    const img = document.createElement(\"img\");\n    const fileName = generateFileName(selectedModel, selectedTerm, selectedParam, selectedHour, selectedStat, selectedPressureLevel);\n    const imagePath = `\/wp-content\/uploads\/production\/mapyGorne\/${selectedTerm}\/${fileName}`;\n    \n    img.src = imagePath;\n    img.alt = `${selectedModel} - ${selectedParam}`;\n    img.style.cssText = `\n      max-width: 95%;\n      max-height: 90vh;\n      object-fit: contain;\n      border: 3px solid rgba(86, 221, 208, 1);\n      border-radius: 8px;\n    `;\n    \n    img.onerror = function() {\n      if (!this.dataset.triedYesterday) {\n        this.dataset.triedYesterday = \"true\";\n        const yesterdayFileName = generateFileName(selectedModel, selectedTerm, selectedParam, selectedHour, selectedStat, selectedPressureLevel, true);\n        this.src = `\/wp-content\/uploads\/production\/mapyGorne\/${selectedTerm}\/${yesterdayFileName}`;\n      } else {\n        this.src = \"\/wp-content\/uploads\/production\/configuration_not.png\";\n      }\n    };\n    \n    fullscreenImages.appendChild(img);\n  }\n}\n\n\/\/ Inicjalizacja po za\u0142adowaniu strony\ndocument.addEventListener(\"DOMContentLoaded\", function() {\n  \/\/ Obs\u0142uga klawiszy strza\u0142ek dla nawigacji\n  document.addEventListener(\"keydown\", function(event) {\n    \/\/ Ignoruj tylko je\u015bli u\u017cytkownik pisze w polu tekstowym lub textarea\n    if (event.target.tagName === 'INPUT' || event.target.tagName === 'TEXTAREA') {\n      return;\n    }\n    \n    \/\/ Najpierw zablokuj domy\u015blne zachowanie dla klawiszy nawigacyjnych\n    const paramsSelect = document.getElementById('paramsSelect');\n    const isParamsSelectActive = document.activeElement === paramsSelect;\n    \n    \/\/ Blokuj przewijanie strony dla strza\u0142ek (chyba \u017ce edytujemy list\u0119 parametr\u00f3w)\n    if ((event.key === \"ArrowLeft\" || event.key === \"ArrowRight\" || \n         event.key === \"ArrowUp\" || event.key === \"ArrowDown\") && !isParamsSelectActive) {\n      event.preventDefault();\n    }\n    \n    \/\/ Sprawd\u017a op\u00f3\u017anienie dla wszystkich skr\u00f3t\u00f3w klawiszowych\n    const currentTime = Date.now();\n    if (currentTime - lastKeyPressTime < keyPressDelay) {\n      return; \/\/ Ignoruj je\u015bli min\u0119\u0142o za ma\u0142o czasu\n    }\n    lastKeyPressTime = currentTime;\n    \n    \/\/ Strza\u0142ki lewo\/prawo - zmiana godzin prognozy\n    if (event.key === \"ArrowLeft\" || event.key === \"ArrowRight\") {\n      \n      \/\/ Pobierz dost\u0119pne godziny\n      let hours = [];\n      \n      \/\/ Sprawd\u017a czy jeste\u015bmy w trybie por\u00f3wnywania modeli\n      const compareBtn = document.getElementById(\"compareBtn\");\n      const isCompareMode = compareBtn && compareBtn.classList.contains(\"active\");\n      \n      if (isCompareMode) {\n        \/\/ W trybie por\u00f3wnywania u\u017cywamy najd\u0142u\u017cszego zakresu prognozy\n        hours = getMaxForecastHours(selectedTerm);\n      } else {\n        if (forecastHours[selectedModel] && forecastHours[selectedModel][selectedTerm]) {\n          hours = forecastHours[selectedModel][selectedTerm];\n        } else {\n          hours = forecastHours[\"default\"];\n        }\n      }\n      \n      if (hours.length === 0) return;\n      \n      \/\/ Znajd\u017a aktualny indeks\n      const currentIndex = hours.findIndex(h => String(h).padStart(2, \"0\") === String(selectedHour).padStart(2, \"0\"));\n      \n      let newIndex;\n      if (event.key === \"ArrowLeft\") {\n        \/\/ Strza\u0142ka w lewo - poprzednia godzina (zatrzymaj si\u0119 na pocz\u0105tku)\n        newIndex = currentIndex > 0 ? currentIndex - 1 : currentIndex;\n      } else {\n        \/\/ Strza\u0142ka w prawo - nast\u0119pna godzina (zatrzymaj si\u0119 na ko\u0144cu)\n        newIndex = currentIndex < hours.length - 1 ? currentIndex + 1 : currentIndex;\n      }\n      \n      \/\/ Je\u015bli indeks si\u0119 nie zmieni\u0142, nie r\u00f3b nic\n      if (newIndex === currentIndex) {\n        return;\n      }\n      \n      \/\/ Ustaw now\u0105 godzin\u0119\n      const newHour = String(hours[newIndex]).padStart(2, \"0\");\n      selectedHour = newHour;\n      \n      \/\/ Zaktualizuj interfejs\n      setActiveButton(\"hours\", newHour);\n      \n      \/\/ Zaktualizuj slider\n      const slider = document.getElementById(\"forecastHourSlider\");\n      if (slider) {\n        slider.value = newIndex;\n        document.getElementById(\"hourValue\").textContent = newHour + \"h\";\n      }\n      \n      \/\/ Za\u0142aduj obrazek\n      showImages();\n      preloadForecastImages();\n      \n      \/\/ Od\u015bwie\u017c widok pe\u0142noekranowy je\u015bli jest aktywny\n      refreshFullscreen();\n    }\n    \n    \/\/ Strza\u0142ki g\u00f3ra\/d\u00f3\u0142 - zmiana poziom\u00f3w ci\u015bnienia LUB nawigacja po li\u015bcie parametr\u00f3w\n    if (event.key === \"ArrowUp\" || event.key === \"ArrowDown\") {\n      \/\/ Je\u015bli lista parametr\u00f3w ma focus, pozw\u00f3l domy\u015blnej akcji (nawigacja po li\u015bcie)\n      if (isParamsSelectActive) {\n        return; \/\/ Nie blokuj domy\u015blnego zachowania\n      }\n      \n      \/\/ W przeciwnym razie - steruj suwakiem ci\u015bnienia\n      \/\/ Sprawd\u017a czy suwak poziom\u00f3w jest widoczny\n      const sliderSection = document.getElementById(\"pressureLevelSliderSection\");\n      if (!sliderSection || sliderSection.style.display === \"none\") {\n        return;\n      }\n      \n      const availableLevels = getAvailablePressureLevels(selectedParam);\n      if (availableLevels.length === 0) return;\n      \n      \/\/ Znajd\u017a aktualny indeks\n      const currentIndex = availableLevels.indexOf(selectedPressureLevel);\n      if (currentIndex === -1) return;\n      \n      let newIndex;\n      if (event.key === \"ArrowUp\") {\n        \/\/ Strza\u0142ka w g\u00f3r\u0119 - ni\u017csze ci\u015bnienie (wy\u017cszy indeks w tablicy posortowanej malej\u0105co)\n        \/\/ Zatrzymaj si\u0119 na ko\u0144cu\n        newIndex = currentIndex < availableLevels.length - 1 ? currentIndex + 1 : currentIndex;\n      } else {\n        \/\/ Strza\u0142ka w d\u00f3\u0142 - wy\u017csze ci\u015bnienie (ni\u017cszy indeks)\n        \/\/ Zatrzymaj si\u0119 na pocz\u0105tku\n        newIndex = currentIndex > 0 ? currentIndex - 1 : currentIndex;\n      }\n      \n      \/\/ Je\u015bli indeks si\u0119 nie zmieni\u0142, nie r\u00f3b nic\n      if (newIndex === currentIndex) {\n        return;\n      }\n      \n      \/\/ Ustaw nowy poziom\n      selectedPressureLevel = availableLevels[newIndex];\n      \n      \/\/ Zaktualizuj slider\n      const slider = document.getElementById(\"pressureLevelSlider\");\n      if (slider) {\n        slider.value = newIndex;\n        \n        if (selectedParam === \"SHEAR\") {\n          const displayValue = selectedPressureLevel.replace(\/^0_\/, '');\n          document.getElementById(\"pressureValue\").textContent = `${displayValue} m`;\n        } else if (selectedParam === \"TW1000\") {\n          const displayValue = selectedPressureLevel.replace(\/^1000_\/, '');\n          document.getElementById(\"pressureValue\").textContent = `${displayValue} hPa`;\n        } else {\n          document.getElementById(\"pressureValue\").textContent = `${selectedPressureLevel} hPa`;\n        }\n      }\n      \n      \/\/ Za\u0142aduj obrazek\n      showImages();\n      preloadForecastImages();\n      \n      \/\/ Od\u015bwie\u017c widok pe\u0142noekranowy je\u015bli jest aktywny\n      refreshFullscreen();\n    }\n    \n    \/\/ Cyfry 1-9 - szybki wyb\u00f3r modelu\n    if (event.key >= '1' && event.key <= '9') {\n      event.preventDefault();\n      \n      const modelIndex = parseInt(event.key) - 1;\n      if (modelIndex < models.length) {\n        const newModel = models[modelIndex];\n        \n        \/\/ Symuluj klikni\u0119cie w przycisk modelu\n        const modelButtons = document.querySelectorAll('#models button');\n        modelButtons.forEach(btn => {\n          if (btn.textContent.trim() === modelDisplayNames[newModel] || btn.textContent.trim() === newModel) {\n            btn.click();\n          }\n        });\n      }\n    }\n    \n    \/\/ Tab - rozwini\u0119cie listy parametr\u00f3w\n    if (event.key === 'Tab') {\n      event.preventDefault();\n      const paramsSelect = document.getElementById('paramsSelect');\n      if (paramsSelect) {\n        paramsSelect.focus();\n        \/\/ Symuluj klikni\u0119cie aby rozwin\u0105\u0107 list\u0119\n        paramsSelect.click();\n        \/\/ Alternatywnie: programowo rozwi\u0144 select\n        paramsSelect.size = Math.min(paramsSelect.options.length, 10);\n      }\n    }\n    \n    \/\/ , i . - zmiana terminu inicjacji (, = lewo\/wcze\u015bniej, . = prawo\/p\u00f3\u017aniej)\n    if (event.key === ',' || event.key === '<' || event.key === '.' || event.key === '>') {\n      event.preventDefault();\n      \n      \/\/ Pobierz dost\u0119pne terminy\n      const availableTerms = termsDefault;\n      if (availableTerms.length === 0) return;\n      \n      \/\/ Znajd\u017a aktualny indeks\n      const currentIndex = availableTerms.indexOf(selectedTerm);\n      if (currentIndex === -1) return;\n      \n      let newIndex;\n      if (event.key === ',' || event.key === '<') {\n        \/\/ , lub < - poprzedni termin (zatrzymaj si\u0119 na pocz\u0105tku)\n        newIndex = currentIndex > 0 ? currentIndex - 1 : currentIndex;\n      } else {\n        \/\/ . lub > - nast\u0119pny termin (zatrzymaj si\u0119 na ko\u0144cu)\n        newIndex = currentIndex < availableTerms.length - 1 ? currentIndex + 1 : currentIndex;\n      }\n      \n      \/\/ Je\u015bli indeks si\u0119 nie zmieni\u0142, nie r\u00f3b nic\n      if (newIndex === currentIndex) {\n        return;\n      }\n      \n      \/\/ Ustaw nowy termin\n      const newTerm = availableTerms[newIndex];\n      \n      \/\/ Symuluj klikni\u0119cie w przycisk terminu\n      const termButtons = document.querySelectorAll('#terms button');\n      termButtons.forEach(btn => {\n        if (btn.dataset.value === newTerm) {\n          btn.click();\n        }\n      });\n    }\n    \n    \/\/ A - w\u0142\u0105cz\/wy\u0142\u0105cz animacj\u0119\n    if (event.key === 'a' || event.key === 'A') {\n      event.preventDefault();\n      const animateBtn = document.getElementById(\"animateBtn\");\n      if (animateBtn) {\n        animateBtn.click();\n      }\n    }\n    \n    \/\/ C - w\u0142\u0105cz\/wy\u0142\u0105cz por\u00f3wnanie modeli\n    if (event.key === 'c' || event.key === 'C') {\n      event.preventDefault();\n      const compareBtn = document.getElementById(\"compareBtn\");\n      if (compareBtn) {\n        compareBtn.click();\n      }\n    }\n    \n    \/\/ F - w\u0142\u0105cz\/wy\u0142\u0105cz pe\u0142ny ekran\n    if (event.key === 'f' || event.key === 'F') {\n      event.preventDefault();\n      const overlay = document.getElementById(\"fullscreenOverlay\");\n      \n      \/\/ Je\u015bli fullscreen jest aktywny, zamknij go\n      if (overlay && overlay.classList.contains(\"active\")) {\n        overlay.classList.remove(\"active\");\n        \n        \/\/ Upewnij si\u0119, \u017ce przycisk fullscreen pozostaje widoczny\n        const fullscreenBtn = document.getElementById(\"fullscreenBtn\");\n        if (fullscreenBtn) {\n          fullscreenBtn.style.display = \"flex\";\n        }\n      } else {\n        \/\/ W przeciwnym razie otw\u00f3rz fullscreen\n        const fullscreenBtn = document.getElementById(\"fullscreenBtn\");\n        if (fullscreenBtn) {\n          fullscreenBtn.click();\n        }\n      }\n    }\n    \n    \/\/ Escape - zamknij pe\u0142ny ekran je\u015bli jest aktywny\n    if (event.key === \"Escape\") {\n      const overlay = document.getElementById(\"fullscreenOverlay\");\n      if (overlay && overlay.classList.contains(\"active\")) {\n        event.preventDefault();\n        overlay.classList.remove(\"active\");\n        \n        \/\/ Upewnij si\u0119, \u017ce przycisk fullscreen pozostaje widoczny\n        const fullscreenBtn = document.getElementById(\"fullscreenBtn\");\n        if (fullscreenBtn) {\n          fullscreenBtn.style.display = \"flex\";\n        }\n      }\n    }\n  });\n  \n  \/\/ Dodanie wska\u017anika \u0142adowania na pocz\u0105tku\n  const mainContent = document.querySelector(\".container\");\n  const initialLoader = document.createElement(\"div\");\n  initialLoader.id = \"initialLoader\";\n  initialLoader.className = \"loader-container\";\n  initialLoader.style.position = \"fixed\";\n  initialLoader.style.top = \"0\";\n  initialLoader.style.left = \"0\";\n  initialLoader.style.width = \"100%\";\n  initialLoader.style.height = \"100%\";\n  initialLoader.style.backgroundColor = \"rgba(255, 255, 255, 0.8)\";\n  initialLoader.style.zIndex = \"9999\";\n  initialLoader.innerHTML = `\n    <div class=\"loader\"><\/div>\n    <p style=\"margin-top: 10px; font-weight: bold;\">\u0141adowanie aplikacji...<\/p>\n  `;\n  document.body.appendChild(initialLoader);\n  \n  \/\/ Dodanie mo\u017cliwo\u015bci resetowania wszystkich flag przez podw\u00f3jne klikni\u0119cie na nag\u0142\u00f3wek\n  document.querySelector(\"h1\")?.addEventListener(\"dblclick\", function() {\n    resetAllExecutionFlags();\n    alert(\"Flagi wykonywania zosta\u0142y zresetowane\");\n  });\n  \n  \/\/ Obs\u0142uga przycisku animacji\n  let animationInterval = null;\n  let isAnimating = false;\n  \n  document.getElementById(\"animateBtn\")?.addEventListener(\"click\", function() {\n    const animateBtn = this;\n    \n    if (isAnimating) {\n      \/\/ Zatrzymaj animacj\u0119\n      clearInterval(animationInterval);\n      animationInterval = null;\n      isAnimating = false;\n      animateBtn.classList.remove(\"active\");\n      animateBtn.textContent = \"\u25b6\";\n      animateBtn.title = \"Animuj godziny prognozy\";\n    } else {\n      \/\/ Rozpocznij animacj\u0119\n      isAnimating = true;\n      animateBtn.classList.add(\"active\");\n      animateBtn.textContent = \"\u23f8\";\n      animateBtn.title = \"Zatrzymaj animacj\u0119\";\n      \n      \/\/ Pobierz dost\u0119pne godziny\n      let hours = [];\n      \n      \/\/ Sprawd\u017a czy jeste\u015bmy w trybie por\u00f3wnywania modeli\n      const compareBtn = document.getElementById(\"compareBtn\");\n      const isCompareMode = compareBtn && compareBtn.classList.contains(\"active\");\n      \n      if (isCompareMode) {\n        \/\/ W trybie por\u00f3wnywania u\u017cywamy najd\u0142u\u017cszego zakresu prognozy\n        hours = getMaxForecastHours(selectedTerm);\n      } else {\n        if (forecastHours[selectedModel] && forecastHours[selectedModel][selectedTerm]) {\n          hours = forecastHours[selectedModel][selectedTerm];\n        } else {\n          hours = forecastHours[\"default\"];\n        }\n      }\n      \n      if (hours.length === 0) {\n        isAnimating = false;\n        animateBtn.classList.remove(\"active\");\n        animateBtn.textContent = \"\u25b6\";\n        return;\n      }\n      \n      animationInterval = setInterval(() => {\n        \/\/ Znajd\u017a aktualny indeks\n        const currentIndex = hours.findIndex(h => String(h).padStart(2, \"0\") === String(selectedHour).padStart(2, \"0\"));\n        \n        \/\/ Przejd\u017a do nast\u0119pnej godziny (zap\u0119tlenie)\n        const nextIndex = (currentIndex + 1) % hours.length;\n        const newHour = String(hours[nextIndex]).padStart(2, \"0\");\n        selectedHour = newHour;\n        \n        \/\/ Zaktualizuj interfejs\n        setActiveButton(\"hours\", newHour);\n        \n        \/\/ Zaktualizuj slider\n        const slider = document.getElementById(\"forecastHourSlider\");\n        if (slider) {\n          slider.value = nextIndex;\n          document.getElementById(\"hourValue\").textContent = newHour + \"h\";\n        }\n        \n        \/\/ Za\u0142aduj obrazek\n        showImages();\n        \n        \/\/ Od\u015bwie\u017c widok pe\u0142noekranowy je\u015bli jest aktywny\n        refreshFullscreen();\n      }, 1000); \/\/ Zmiana co 1 sekund\u0119\n    }\n  });\n  \n  \/\/ Obs\u0142uga przycisku por\u00f3wnania modeli\n  document.getElementById(\"compareBtn\")?.addEventListener(\"click\", function() {\n    \/\/ Zatrzymaj animacj\u0119 je\u015bli jest aktywna\n    if (isAnimating) {\n      document.getElementById(\"animateBtn\").click();\n    }\n    \n    const imagesContainer = document.getElementById(\"images\");\n    const isCompareMode = this.classList.contains(\"active\");\n    \n    if (isCompareMode) {\n      \/\/ Wy\u0142\u0105cz tryb por\u00f3wnania\n      this.classList.remove(\"active\");\n      this.title = \"Por\u00f3wnaj 4 modele\";\n      \n      \/\/ Przycisk pe\u0142nego ekranu pozostaje widoczny\n      const fullscreenBtn = document.getElementById(\"fullscreenBtn\");\n      if (fullscreenBtn) {\n        fullscreenBtn.style.display = \"flex\";\n      }\n      \n      \/\/ Odznacz wszystkie modele opr\u00f3cz wybranego\n      const modelButtons = document.querySelectorAll('#models button');\n      modelButtons.forEach(btn => {\n        btn.classList.remove('active');\n      });\n      setActiveButton(\"models\", selectedModel);\n      \n      \/\/ Wyczy\u015b\u0107 kontener\n      imagesContainer.innerHTML = \"\";\n      \n      \/\/ Przywr\u00f3\u0107 normalny widok\n      imagesContainer.style.display = \"block\";\n      imagesContainer.style.gridTemplateColumns = \"\";\n      imagesContainer.style.gap = \"\";\n      \n      \/\/ Przerenderuj przyciski godzin i slider dla pojedynczego modelu\n      renderForecastHours();\n      \n      \/\/ Za\u0142aduj aktualny obraz\n      showImages();\n      \n      \/\/ Od\u015bwie\u017c widok pe\u0142noekranowy je\u015bli jest aktywny\n      refreshFullscreen();\n    } else {\n      \/\/ W\u0142\u0105cz tryb por\u00f3wnania\n      this.classList.add(\"active\");\n      this.title = \"Wy\u0142\u0105cz por\u00f3wnanie\";\n      \n      \/\/ Poka\u017c przycisk pe\u0142nego ekranu\n      const fullscreenBtn = document.getElementById(\"fullscreenBtn\");\n      if (fullscreenBtn) {\n        fullscreenBtn.style.display = \"flex\";\n      }\n      \n      \/\/ Przerenderuj przyciski godzin i slider dla najd\u0142u\u017cszego zakresu\n      renderForecastHours();\n      \n      \/\/ Od\u015bwie\u017c widok por\u00f3wnania\n      refreshCompareView();\n      \n      \/\/ Od\u015bwie\u017c widok pe\u0142noekranowy je\u015bli jest aktywny\n      refreshFullscreen();\n    }\n  });\n  \n  \/\/ Obs\u0142uga przycisku pe\u0142nego ekranu\n  document.getElementById(\"fullscreenBtn\")?.addEventListener(\"click\", function() {\n    const overlay = document.getElementById(\"fullscreenOverlay\");\n    const fullscreenImages = document.getElementById(\"fullscreenImages\");\n    const fullscreenTitle = document.getElementById(\"fullscreenTitle\");\n    const compareBtn = document.getElementById(\"compareBtn\");\n    const isCompareMode = compareBtn && compareBtn.classList.contains(\"active\");\n    \n    \/\/ Ustaw tytu\u0142\n    const paramName = parameterDisplayNames[selectedParam] || selectedParam;\n    const hourText = selectedHour + \"h\";\n    const termText = selectedTerm;\n    \n    if (isCompareMode) {\n      fullscreenTitle.textContent = `Por\u00f3wnanie modeli - ${paramName} - Termin: ${termText} - ${hourText}`;\n    } else {\n      const modelName = modelDisplayNames[selectedModel] || selectedModel;\n      fullscreenTitle.textContent = `${modelName} - ${paramName} - Termin: ${termText} - ${hourText}`;\n    }\n    \n    \/\/ Wyczy\u015b\u0107 kontener\n    fullscreenImages.innerHTML = \"\";\n    \n    if (isCompareMode) {\n      \/\/ Tryb por\u00f3wnania - poka\u017c wszystkie modele w siatce 2x2\n      fullscreenImages.style.display = \"grid\";\n      fullscreenImages.style.gridTemplateColumns = \"1fr 1fr\";\n      fullscreenImages.style.gap = \"15px\";\n      fullscreenImages.style.padding = \"20px\";\n      fullscreenImages.style.justifyContent = \"center\";\n      fullscreenImages.style.alignItems = \"center\";\n      \n      \/\/ Dla ka\u017cdego modelu utw\u00f3rz kontener z obrazem\n      models.forEach((model) => {\n        const modelContainer = document.createElement(\"div\");\n        modelContainer.style.position = \"relative\";\n        modelContainer.style.display = \"flex\";\n        modelContainer.style.flexDirection = \"column\";\n        modelContainer.style.alignItems = \"center\";\n        \n        \/\/ Dodaj etykiet\u0119 modelu\n        const label = document.createElement(\"div\");\n        label.textContent = modelDisplayNames[model];\n        label.style.cssText = `\n          padding: 8px 15px;\n          background: rgba(0, 0, 0, 0.8);\n          color: white;\n          border-radius: 6px;\n          font-weight: bold;\n          margin-bottom: 10px;\n          font-size: 1.1em;\n        `;\n        \n        \/\/ Utw\u00f3rz element obrazu\n        const img = document.createElement(\"img\");\n        img.style.cssText = `\n          width: 100%;\n          max-height: 50vh;\n          object-fit: contain;\n          display: block;\n          border: 3px solid ${model === selectedModel ? 'rgba(86, 221, 208, 1)' : 'rgba(255, 255, 255, 0.3)'};\n          border-radius: 8px;\n        `;\n        \n        \/\/ Generuj nazw\u0119 pliku dla tego modelu\n        const fileName = generateFileName(model, selectedTerm, selectedParam, selectedHour, selectedStat, selectedPressureLevel);\n        const imagePath = `\/wp-content\/uploads\/production\/mapyGorne\/${selectedTerm}\/${fileName}`;\n        \n        img.src = imagePath;\n        img.alt = modelDisplayNames[model];\n        \n        \/\/ Obs\u0142uga b\u0142\u0119d\u00f3w \u0142adowania\n        img.onerror = function() {\n          if (!this.dataset.triedYesterday) {\n            this.dataset.triedYesterday = \"true\";\n            const yesterdayFileName = generateFileName(model, selectedTerm, selectedParam, selectedHour, selectedStat, selectedPressureLevel, true);\n            this.src = `\/wp-content\/uploads\/production\/mapyGorne\/${selectedTerm}\/${yesterdayFileName}`;\n          } else {\n            this.src = \"\/wp-content\/uploads\/production\/mapyGorne\/configuration_not.png\";\n          }\n        };\n        \n        modelContainer.appendChild(label);\n        modelContainer.appendChild(img);\n        fullscreenImages.appendChild(modelContainer);\n      });\n    } else {\n      \/\/ Tryb pojedynczego modelu - poka\u017c jeden du\u017cy obraz\n      fullscreenImages.style.display = \"flex\";\n      fullscreenImages.style.gridTemplateColumns = \"\";\n      \n      const img = document.createElement(\"img\");\n      img.style.width = \"100%\";\n      img.style.height = \"auto\";\n      img.style.maxHeight = \"90vh\";\n      img.style.objectFit = \"contain\";\n      img.style.display = \"block\";\n      img.style.borderRadius = \"8px\";\n      \n      \/\/ Generuj nazw\u0119 pliku\n      const fileName = generateFileName(selectedModel, selectedTerm, selectedParam, selectedHour, selectedStat, selectedPressureLevel);\n      const imagePath = `\/wp-content\/uploads\/production\/mapyGorne\/${selectedTerm}\/${fileName}`;\n      \n      img.src = imagePath;\n      img.alt = modelDisplayNames[selectedModel];\n      \n      \/\/ Obs\u0142uga b\u0142\u0119d\u00f3w \u0142adowania\n      img.onerror = function() {\n        if (!this.dataset.triedYesterday) {\n          this.dataset.triedYesterday = \"true\";\n          const yesterdayFileName = generateFileName(selectedModel, selectedTerm, selectedParam, selectedHour, selectedStat, selectedPressureLevel, true);\n          this.src = `\/wp-content\/uploads\/production\/mapyGorne\/${selectedTerm}\/${yesterdayFileName}`;\n        } else {\n          this.src = \"\/wp-content\/uploads\/production\/mapyGorne\/configuration_not.png\";\n        }\n      };\n      \n      fullscreenImages.appendChild(img);\n    }\n    \n    \/\/ Poka\u017c overlay\n    overlay.classList.add(\"active\");\n  });\n  \n  \/\/ Obs\u0142uga zamykania pe\u0142nego ekranu\n  document.getElementById(\"fullscreenClose\")?.addEventListener(\"click\", function() {\n    const overlay = document.getElementById(\"fullscreenOverlay\");\n    overlay.classList.remove(\"active\");\n    \n    \/\/ Upewnij si\u0119, \u017ce przycisk fullscreen pozostaje widoczny\n    const fullscreenBtn = document.getElementById(\"fullscreenBtn\");\n    if (fullscreenBtn) {\n      fullscreenBtn.style.display = \"flex\";\n    }\n  });\n  \n  \/\/ Ustawienie bezpiecznika czasowego dla resetowania flag\n  \/\/ Sprawdza co 30 sekund, czy flagi s\u0105 aktywne d\u0142u\u017cej ni\u017c 60 sekund (prawdziwy deadlock)\n  setInterval(() => {\n    const now = Date.now();\n    const DEADLOCK_TIMEOUT = 60000; \/\/ 60 sekund\n    \n    let shouldReset = false;\n    \n    \/\/ Sprawd\u017a ka\u017cd\u0105 flag\u0119 osobno z jej timestampem\n    if (isShowImagesRunning && isShowImagesRunningTimestamp > 0 && (now - isShowImagesRunningTimestamp) > DEADLOCK_TIMEOUT) {\n      shouldReset = true;\n    }\n    \n    if (isRenderingForecastHours && isRenderingForecastHoursTimestamp > 0 && (now - isRenderingForecastHoursTimestamp) > DEADLOCK_TIMEOUT) {\n      shouldReset = true;\n    }\n    \n    if (isPreloadingImages && isPreloadingImagesTimestamp > 0 && (now - isPreloadingImagesTimestamp) > DEADLOCK_TIMEOUT) {\n      shouldReset = true;\n    }\n    \n    if (shouldReset) {\n      resetAllExecutionFlags();\n    }\n  }, 30000);\n  \n  \/\/ Dodanie mechanizmu automatycznego naprawiania interfejsu\n  setInterval(function() {\n    const imagesContainer = document.getElementById(\"images\");\n    \n    \/\/ Sprawd\u017a czy kontener obraz\u00f3w jest pusty, a powinien zawiera\u0107 obrazy\n    if (imagesContainer && \n        isAppReady && \n        selectedModel && \n        selectedTerm && \n        selectedParam && \n        selectedHour && \n        imagesContainer.innerHTML.trim() === \"\") {\n      \n      \/\/ Dodaj animacj\u0119 \u0142adowania\n      const loaderContainer = document.createElement(\"div\");\n      loaderContainer.className = \"loader-container\";\n      const loader = document.createElement(\"div\");\n      loader.className = \"loader\";\n      loaderContainer.appendChild(loader);\n      const loadingText = document.createElement(\"p\");\n      loadingText.textContent = \"Od\u015bwie\u017canie prognozy...\";\n      loadingText.style.marginTop = \"10px\";\n      loaderContainer.appendChild(loadingText);\n      imagesContainer.appendChild(loaderContainer);\n      \n      \/\/ Po kr\u00f3tkim op\u00f3\u017anieniu spr\u00f3buj za\u0142adowa\u0107 obrazy ponownie\n      setTimeout(() => {\n        showImages();\n      }, 100);\n    }\n  }, 1000); \/\/ Sprawdzaj co 1 sekund\u0119\n\n  \/\/ Pr\u00f3ba za\u0142adowania parametr\u00f3w z URL\n  if (validateAndApplyURLParams()) {\n    updateTermButtons();\n  } else {\n    initializeInterface();\n  }\n  \n  \/\/ Rozpocznij preload domy\u015blnego modelu i kilku alternatywnych\n  Promise.all([\n    preloadModelData(selectedModel),\n    \/\/ Preload najcz\u0119\u015bciej u\u017cywanych modeli w tle\n    preloadModelData(\"ALARO\")\n  ]).then(() => {\n    initialLoadDone = true;\n    \n    \/\/ Usu\u0144 wska\u017anik \u0142adowania\n    const loader = document.getElementById(\"initialLoader\");\n    if (loader) {\n      loader.style.opacity = \"0\";\n      loader.style.transition = \"opacity 0.5s\";\n      setTimeout(() => {\n        loader.remove();\n      }, 500);\n      console.log(\"Autor Bart\u0142omiej Sobczyk 2025\");\n    }\n    \n    \/\/ Oznacz aplikacj\u0119 jako gotow\u0105\n    isAppReady = true;\n  });\n  \n  \/\/ Dodatkowa weryfikacja poprawnego pod\u015bwietlenia przycisk\u00f3w i za\u0142adowania obrazka\n  setTimeout(function() {\n    updateAllActiveButtons();\n    \/\/ Zapewnienie poprawnego za\u0142adowania obrazka\n    showImages();\n  }, 1000);\n});\n<\/script>\n\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t\t\t\t<\/div>\n\t\t<\/div>\n\t\t\t\t\t\t\t<\/div>\n\t\t<\/section>\n\t\t\t\t\t\t\t<\/div>\n\t\t","protected":false},"excerpt":{"rendered":"<p>Model: Termin startu: Parametr: Statystyka: Godziny prognozy: *Je\u015bli przy formacie czasu nie widnieje adnotacja UTC to znaczy, \u017ce obraz generowany jest na wskazany termin wg czasu urz\u0119dowego. \ud83d\udca1 Skr\u00f3ty klawiszowe: 1-4 Wyb\u00f3r modelu &lt; &gt; Zmiana terminu startu \u2190 \u2192 Zmiana godzin prognozy \u2191 \u2193 Zmiana poziom\u00f3w ci\u015bnienia Tab Rozwini\u0119cie parametr\u00f3w A Animacja godzin prognozy [&hellip;]<\/p>\n","protected":false},"author":3,"featured_media":0,"parent":0,"menu_order":0,"comment_status":"closed","ping_status":"closed","template":"elementor_header_footer","meta":{"ocean_post_layout":"","ocean_both_sidebars_style":"","ocean_both_sidebars_content_width":0,"ocean_both_sidebars_sidebars_width":0,"ocean_sidebar":"0","ocean_second_sidebar":"0","ocean_disable_margins":"enable","ocean_add_body_class":"","ocean_shortcode_before_top_bar":"","ocean_shortcode_after_top_bar":"","ocean_shortcode_before_header":"","ocean_shortcode_after_header":"","ocean_has_shortcode":"","ocean_shortcode_after_title":"","ocean_shortcode_before_footer_widgets":"","ocean_shortcode_after_footer_widgets":"","ocean_shortcode_before_footer_bottom":"","ocean_shortcode_after_footer_bottom":"","ocean_display_top_bar":"default","ocean_display_header":"default","ocean_header_style":"","ocean_center_header_left_menu":"0","ocean_custom_header_template":"0","ocean_custom_logo":0,"ocean_custom_retina_logo":0,"ocean_custom_logo_max_width":0,"ocean_custom_logo_tablet_max_width":0,"ocean_custom_logo_mobile_max_width":0,"ocean_custom_logo_max_height":0,"ocean_custom_logo_tablet_max_height":0,"ocean_custom_logo_mobile_max_height":0,"ocean_header_custom_menu":"0","ocean_menu_typo_font_family":"0","ocean_menu_typo_font_subset":"","ocean_menu_typo_font_size":0,"ocean_menu_typo_font_size_tablet":0,"ocean_menu_typo_font_size_mobile":0,"ocean_menu_typo_font_size_unit":"px","ocean_menu_typo_font_weight":"","ocean_menu_typo_font_weight_tablet":"","ocean_menu_typo_font_weight_mobile":"","ocean_menu_typo_transform":"","ocean_menu_typo_transform_tablet":"","ocean_menu_typo_transform_mobile":"","ocean_menu_typo_line_height":0,"ocean_menu_typo_line_height_tablet":0,"ocean_menu_typo_line_height_mobile":0,"ocean_menu_typo_line_height_unit":"","ocean_menu_typo_spacing":0,"ocean_menu_typo_spacing_tablet":0,"ocean_menu_typo_spacing_mobile":0,"ocean_menu_typo_spacing_unit":"","ocean_menu_link_color":"","ocean_menu_link_color_hover":"","ocean_menu_link_color_active":"","ocean_menu_link_background":"","ocean_menu_link_hover_background":"","ocean_menu_link_active_background":"","ocean_menu_social_links_bg":"","ocean_menu_social_hover_links_bg":"","ocean_menu_social_links_color":"","ocean_menu_social_hover_links_color":"","ocean_disable_title":"default","ocean_disable_heading":"default","ocean_post_title":"","ocean_post_subheading":"","ocean_post_title_style":"","ocean_post_title_background_color":"","ocean_post_title_background":0,"ocean_post_title_bg_image_position":"","ocean_post_title_bg_image_attachment":"","ocean_post_title_bg_image_repeat":"","ocean_post_title_bg_image_size":"","ocean_post_title_height":0,"ocean_post_title_bg_overlay":0.5,"ocean_post_title_bg_overlay_color":"","ocean_disable_breadcrumbs":"default","ocean_breadcrumbs_color":"","ocean_breadcrumbs_separator_color":"","ocean_breadcrumbs_links_color":"","ocean_breadcrumbs_links_hover_color":"","ocean_display_footer_widgets":"default","ocean_display_footer_bottom":"default","ocean_custom_footer_template":"0"},"yoast_head":"<!-- This site is optimized with the Yoast SEO plugin v19.5.1 - https:\/\/yoast.com\/wordpress\/plugins\/seo\/ -->\n<title>IMGW-PIB CMM: Mapy g\u00f3rne - Laboratorium Modelowania Meteorologicznego CMOK IMGW-PIB<\/title>\n<meta name=\"description\" content=\"Mapy g\u00f3rne pozwalaj\u0105 na ocen\u0119 i \u015bledzenie dynamiki zmian w atmosferze na wy\u017cszych jej poziomach, co jest niezb\u0119dne w prognozowaniu stanu pogody przez synoptyk\u00f3w.\" \/>\n<meta name=\"robots\" content=\"index, follow, max-snippet:-1, max-image-preview:large, max-video-preview:-1\" \/>\n<link rel=\"canonical\" href=\"https:\/\/cmm.imgw.pl\/?page_id=39786\" \/>\n<meta property=\"og:locale\" content=\"pl_PL\" \/>\n<meta property=\"og:type\" content=\"article\" \/>\n<meta property=\"og:title\" content=\"IMGW PIB mapy g\u00f3rne\" \/>\n<meta property=\"og:description\" content=\"Mapy g\u00f3rne pozwalaj\u0105 na ocen\u0119 i \u015bledzenie dynamiki zmian w atmosferze na wy\u017cszych jej poziomach, co jest niezb\u0119dne w prognozowaniu stanu pogody przez synoptyk\u00f3w.\" \/>\n<meta property=\"og:url\" content=\"https:\/\/cmm.imgw.pl\/?page_id=39786\" \/>\n<meta property=\"og:site_name\" content=\"Laboratorium Modelowania Meteorologicznego CMOK IMGW-PIB\" \/>\n<meta property=\"article:publisher\" content=\"https:\/\/www.facebook.com\/Meteoimgw\/\" \/>\n<meta property=\"article:modified_time\" content=\"2026-02-06T11:36:27+00:00\" \/>\n<meta property=\"og:image\" content=\"https:\/\/cmm.imgw.pl\/wp-content\/uploads\/2025\/10\/MODELE_LOGO_UNIFIKACJA_v2.png\" \/>\n\t<meta property=\"og:image:width\" content=\"1356\" \/>\n\t<meta property=\"og:image:height\" content=\"365\" \/>\n\t<meta property=\"og:image:type\" content=\"image\/png\" \/>\n<meta name=\"twitter:card\" content=\"summary_large_image\" \/>\n<meta name=\"twitter:title\" content=\"IMGW PIB mapy g\u00f3rne\" \/>\n<meta name=\"twitter:description\" content=\"Mapy g\u00f3rne pozwalaj\u0105 na ocen\u0119 i \u015bledzenie dynamiki zmian w atmosferze na wy\u017cszych jej poziomach, co jest niezb\u0119dne w prognozowaniu stanu pogody przez synoptyk\u00f3w.\" \/>\n<meta name=\"twitter:site\" content=\"@IMGW_CMM\" \/>\n<script type=\"application\/ld+json\" class=\"yoast-schema-graph\">{\"@context\":\"https:\/\/schema.org\",\"@graph\":[{\"@type\":\"WebPage\",\"@id\":\"https:\/\/cmm.imgw.pl\/?page_id=39786\",\"url\":\"https:\/\/cmm.imgw.pl\/?page_id=39786\",\"name\":\"IMGW-PIB CMM: Mapy g\u00f3rne - Laboratorium Modelowania Meteorologicznego CMOK IMGW-PIB\",\"isPartOf\":{\"@id\":\"https:\/\/cmm.imgw.pl\/#website\"},\"datePublished\":\"2025-06-27T18:20:20+00:00\",\"dateModified\":\"2026-02-06T11:36:27+00:00\",\"description\":\"Mapy g\u00f3rne pozwalaj\u0105 na ocen\u0119 i \u015bledzenie dynamiki zmian w atmosferze na wy\u017cszych jej poziomach, co jest niezb\u0119dne w prognozowaniu stanu pogody przez synoptyk\u00f3w.\",\"breadcrumb\":{\"@id\":\"https:\/\/cmm.imgw.pl\/?page_id=39786#breadcrumb\"},\"inLanguage\":\"pl-PL\",\"potentialAction\":[{\"@type\":\"ReadAction\",\"target\":[\"https:\/\/cmm.imgw.pl\/?page_id=39786\"]}]},{\"@type\":\"BreadcrumbList\",\"@id\":\"https:\/\/cmm.imgw.pl\/?page_id=39786#breadcrumb\",\"itemListElement\":[{\"@type\":\"ListItem\",\"position\":1,\"name\":\"Home\",\"item\":\"https:\/\/cmm.imgw.pl\/\"},{\"@type\":\"ListItem\",\"position\":2,\"name\":\"IMGW-PIB CMM: Mapy g\u00f3rne\"}]},{\"@type\":\"WebSite\",\"@id\":\"https:\/\/cmm.imgw.pl\/#website\",\"url\":\"https:\/\/cmm.imgw.pl\/\",\"name\":\"Laboratorium Modelowania Meteorologicznego CMOK IMGW-PIB\",\"description\":\"CMOK-LMM Laboratorium pe\u0142ni pa\u0144stwow\u0105 s\u0142u\u017cb\u0119 hydrologiczno-meteorologiczn\u0105 w zakresie numerycznych prognoz pogody, kt\u00f3rego zadaniem jest konsolidacja kompetencji w obszarze modelowania zjawisk pogodowych oraz dalszego rozwoju numerycznych modeli pogody (NMP).\",\"publisher\":{\"@id\":\"https:\/\/cmm.imgw.pl\/#organization\"},\"potentialAction\":[{\"@type\":\"SearchAction\",\"target\":{\"@type\":\"EntryPoint\",\"urlTemplate\":\"https:\/\/cmm.imgw.pl\/?s={search_term_string}\"},\"query-input\":\"required name=search_term_string\"}],\"inLanguage\":\"pl-PL\"},{\"@type\":\"Organization\",\"@id\":\"https:\/\/cmm.imgw.pl\/#organization\",\"name\":\"Laboratorium Modelowania Meteorologicznego CMOK IMGW-PIB\",\"url\":\"https:\/\/cmm.imgw.pl\/\",\"sameAs\":[\"https:\/\/www.facebook.com\/Meteoimgw\/\",\"https:\/\/twitter.com\/IMGW_CMM\"],\"logo\":{\"@type\":\"ImageObject\",\"inLanguage\":\"pl-PL\",\"@id\":\"https:\/\/cmm.imgw.pl\/#\/schema\/logo\/image\/\",\"url\":\"https:\/\/cmm.imgw.pl\/wp-content\/uploads\/2025\/10\/MODELE_LOGO_UNIFIKACJA_v2.png\",\"contentUrl\":\"https:\/\/cmm.imgw.pl\/wp-content\/uploads\/2025\/10\/MODELE_LOGO_UNIFIKACJA_v2.png\",\"width\":1356,\"height\":365,\"caption\":\"Laboratorium Modelowania Meteorologicznego CMOK IMGW-PIB\"},\"image\":{\"@id\":\"https:\/\/cmm.imgw.pl\/#\/schema\/logo\/image\/\"}}]}<\/script>\n<!-- \/ Yoast SEO plugin. -->","yoast_head_json":{"title":"IMGW-PIB CMM: Mapy g\u00f3rne - Laboratorium Modelowania Meteorologicznego CMOK IMGW-PIB","description":"Mapy g\u00f3rne pozwalaj\u0105 na ocen\u0119 i \u015bledzenie dynamiki zmian w atmosferze na wy\u017cszych jej poziomach, co jest niezb\u0119dne w prognozowaniu stanu pogody przez synoptyk\u00f3w.","robots":{"index":"index","follow":"follow","max-snippet":"max-snippet:-1","max-image-preview":"max-image-preview:large","max-video-preview":"max-video-preview:-1"},"canonical":"https:\/\/cmm.imgw.pl\/?page_id=39786","og_locale":"pl_PL","og_type":"article","og_title":"IMGW PIB mapy g\u00f3rne","og_description":"Mapy g\u00f3rne pozwalaj\u0105 na ocen\u0119 i \u015bledzenie dynamiki zmian w atmosferze na wy\u017cszych jej poziomach, co jest niezb\u0119dne w prognozowaniu stanu pogody przez synoptyk\u00f3w.","og_url":"https:\/\/cmm.imgw.pl\/?page_id=39786","og_site_name":"Laboratorium Modelowania Meteorologicznego CMOK IMGW-PIB","article_publisher":"https:\/\/www.facebook.com\/Meteoimgw\/","article_modified_time":"2026-02-06T11:36:27+00:00","og_image":[{"width":1356,"height":365,"url":"https:\/\/cmm.imgw.pl\/wp-content\/uploads\/2025\/10\/MODELE_LOGO_UNIFIKACJA_v2.png","type":"image\/png"}],"twitter_card":"summary_large_image","twitter_title":"IMGW PIB mapy g\u00f3rne","twitter_description":"Mapy g\u00f3rne pozwalaj\u0105 na ocen\u0119 i \u015bledzenie dynamiki zmian w atmosferze na wy\u017cszych jej poziomach, co jest niezb\u0119dne w prognozowaniu stanu pogody przez synoptyk\u00f3w.","twitter_site":"@IMGW_CMM","schema":{"@context":"https:\/\/schema.org","@graph":[{"@type":"WebPage","@id":"https:\/\/cmm.imgw.pl\/?page_id=39786","url":"https:\/\/cmm.imgw.pl\/?page_id=39786","name":"IMGW-PIB CMM: Mapy g\u00f3rne - Laboratorium Modelowania Meteorologicznego CMOK IMGW-PIB","isPartOf":{"@id":"https:\/\/cmm.imgw.pl\/#website"},"datePublished":"2025-06-27T18:20:20+00:00","dateModified":"2026-02-06T11:36:27+00:00","description":"Mapy g\u00f3rne pozwalaj\u0105 na ocen\u0119 i \u015bledzenie dynamiki zmian w atmosferze na wy\u017cszych jej poziomach, co jest niezb\u0119dne w prognozowaniu stanu pogody przez synoptyk\u00f3w.","breadcrumb":{"@id":"https:\/\/cmm.imgw.pl\/?page_id=39786#breadcrumb"},"inLanguage":"pl-PL","potentialAction":[{"@type":"ReadAction","target":["https:\/\/cmm.imgw.pl\/?page_id=39786"]}]},{"@type":"BreadcrumbList","@id":"https:\/\/cmm.imgw.pl\/?page_id=39786#breadcrumb","itemListElement":[{"@type":"ListItem","position":1,"name":"Home","item":"https:\/\/cmm.imgw.pl\/"},{"@type":"ListItem","position":2,"name":"IMGW-PIB CMM: Mapy g\u00f3rne"}]},{"@type":"WebSite","@id":"https:\/\/cmm.imgw.pl\/#website","url":"https:\/\/cmm.imgw.pl\/","name":"Laboratorium Modelowania Meteorologicznego CMOK IMGW-PIB","description":"CMOK-LMM Laboratorium pe\u0142ni pa\u0144stwow\u0105 s\u0142u\u017cb\u0119 hydrologiczno-meteorologiczn\u0105 w zakresie numerycznych prognoz pogody, kt\u00f3rego zadaniem jest konsolidacja kompetencji w obszarze modelowania zjawisk pogodowych oraz dalszego rozwoju numerycznych modeli pogody (NMP).","publisher":{"@id":"https:\/\/cmm.imgw.pl\/#organization"},"potentialAction":[{"@type":"SearchAction","target":{"@type":"EntryPoint","urlTemplate":"https:\/\/cmm.imgw.pl\/?s={search_term_string}"},"query-input":"required name=search_term_string"}],"inLanguage":"pl-PL"},{"@type":"Organization","@id":"https:\/\/cmm.imgw.pl\/#organization","name":"Laboratorium Modelowania Meteorologicznego CMOK IMGW-PIB","url":"https:\/\/cmm.imgw.pl\/","sameAs":["https:\/\/www.facebook.com\/Meteoimgw\/","https:\/\/twitter.com\/IMGW_CMM"],"logo":{"@type":"ImageObject","inLanguage":"pl-PL","@id":"https:\/\/cmm.imgw.pl\/#\/schema\/logo\/image\/","url":"https:\/\/cmm.imgw.pl\/wp-content\/uploads\/2025\/10\/MODELE_LOGO_UNIFIKACJA_v2.png","contentUrl":"https:\/\/cmm.imgw.pl\/wp-content\/uploads\/2025\/10\/MODELE_LOGO_UNIFIKACJA_v2.png","width":1356,"height":365,"caption":"Laboratorium Modelowania Meteorologicznego CMOK IMGW-PIB"},"image":{"@id":"https:\/\/cmm.imgw.pl\/#\/schema\/logo\/image\/"}}]}},"_links":{"self":[{"href":"https:\/\/cmm.imgw.pl\/index.php?rest_route=\/wp\/v2\/pages\/39786"}],"collection":[{"href":"https:\/\/cmm.imgw.pl\/index.php?rest_route=\/wp\/v2\/pages"}],"about":[{"href":"https:\/\/cmm.imgw.pl\/index.php?rest_route=\/wp\/v2\/types\/page"}],"author":[{"embeddable":true,"href":"https:\/\/cmm.imgw.pl\/index.php?rest_route=\/wp\/v2\/users\/3"}],"replies":[{"embeddable":true,"href":"https:\/\/cmm.imgw.pl\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=39786"}],"version-history":[{"count":105,"href":"https:\/\/cmm.imgw.pl\/index.php?rest_route=\/wp\/v2\/pages\/39786\/revisions"}],"predecessor-version":[{"id":49346,"href":"https:\/\/cmm.imgw.pl\/index.php?rest_route=\/wp\/v2\/pages\/39786\/revisions\/49346"}],"wp:attachment":[{"href":"https:\/\/cmm.imgw.pl\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=39786"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}