From c29d2206321c33ce4f1831668d29633d8f55ca65 Mon Sep 17 00:00:00 2001
From: Lisyonok04 <125237397+Lisyonok04@users.noreply.github.com>
Date: Sun, 5 Apr 2026 22:14:52 +0400
Subject: [PATCH 1/7] Add files via upload
---
cities.json | 176 ++++++++++++++++++++++++++++++++
docker-compose.yaml | 8 ++
index.html | 93 +++++++++++++++++
script.js | 242 ++++++++++++++++++++++++++++++++++++++++++++
styles.css | 189 ++++++++++++++++++++++++++++++++++
5 files changed, 708 insertions(+)
create mode 100644 cities.json
create mode 100644 docker-compose.yaml
create mode 100644 index.html
create mode 100644 script.js
create mode 100644 styles.css
diff --git a/cities.json b/cities.json
new file mode 100644
index 00000000..80b04dfd
--- /dev/null
+++ b/cities.json
@@ -0,0 +1,176 @@
+[
+ { "name": "Москва", "lat": 55.7558, "lon": 37.6173, "population": 12655050 },
+ { "name": "Санкт-Петербург", "lat": 59.9343, "lon": 30.3351, "population": 5384342 },
+ { "name": "Новосибирск", "lat": 55.0084, "lon": 82.9357, "population": 1625631 },
+ { "name": "Екатеринбург", "lat": 56.8389, "lon": 60.6057, "population": 1493749 },
+ { "name": "Казань", "lat": 55.7961, "lon": 49.1064, "population": 1257391 },
+ { "name": "Нижний Новгород", "lat": 56.2965, "lon": 43.9361, "population": 1252236 },
+ { "name": "Челябинск", "lat": 55.1644, "lon": 61.4368, "population": 1196680 },
+ { "name": "Самара", "lat": 53.1952, "lon": 50.1069, "population": 1156644 },
+ { "name": "Омск", "lat": 54.9885, "lon": 73.3242, "population": 1154116 },
+ { "name": "Ростов-на-Дону", "lat": 47.2357, "lon": 39.7015, "population": 1137904 },
+ { "name": "Уфа", "lat": 54.7388, "lon": 55.9721, "population": 1128787 },
+ { "name": "Красноярск", "lat": 56.0153, "lon": 92.8932, "population": 1093771 },
+ { "name": "Воронеж", "lat": 51.6720, "lon": 39.1843, "population": 1058261 },
+ { "name": "Пермь", "lat": 58.0105, "lon": 56.2502, "population": 1055397 },
+ { "name": "Волгоград", "lat": 48.7080, "lon": 44.5133, "population": 1008998 },
+ { "name": "Краснодар", "lat": 45.0355, "lon": 38.9753, "population": 948827 },
+ { "name": "Саратов", "lat": 51.5924, "lon": 46.0348, "population": 901361 },
+ { "name": "Тюмень", "lat": 57.1522, "lon": 65.5272, "population": 847488 },
+ { "name": "Тольятти", "lat": 53.5303, "lon": 49.3461, "population": 699631 },
+ { "name": "Ижевск", "lat": 56.8527, "lon": 53.2041, "population": 648944 },
+ { "name": "Барнаул", "lat": 53.3606, "lon": 83.7636, "population": 630877 },
+ { "name": "Ульяновск", "lat": 54.3141, "lon": 48.4031, "population": 623013 },
+ { "name": "Иркутск", "lat": 52.2870, "lon": 104.2805, "population": 623562 },
+ { "name": "Хабаровск", "lat": 48.4827, "lon": 135.0838, "population": 616372 },
+ { "name": "Ярославль", "lat": 57.6261, "lon": 39.8845, "population": 608079 },
+ { "name": "Владивосток", "lat": 43.1056, "lon": 131.8735, "population": 606589 },
+ { "name": "Махачкала", "lat": 42.9849, "lon": 47.5047, "population": 603518 },
+ { "name": "Томск", "lat": 56.4977, "lon": 84.9744, "population": 576624 },
+ { "name": "Оренбург", "lat": 51.7727, "lon": 55.0988, "population": 572188 },
+ { "name": "Кемерово", "lat": 55.3331, "lon": 86.0831, "population": 558973 },
+ { "name": "Новокузнецк", "lat": 53.7557, "lon": 87.1099, "population": 547904 },
+ { "name": "Рязань", "lat": 54.6269, "lon": 39.6916, "population": 539290 },
+ { "name": "Астрахань", "lat": 46.3497, "lon": 48.0408, "population": 529793 },
+ { "name": "Набережные Челны", "lat": 55.7251, "lon": 52.4069, "population": 533907 },
+ { "name": "Пенза", "lat": 53.2001, "lon": 45.0000, "population": 520300 },
+ { "name": "Липецк", "lat": 52.6031, "lon": 39.5708, "population": 508887 },
+ { "name": "Киров", "lat": 58.6035, "lon": 49.6679, "population": 507155 },
+ { "name": "Чебоксары", "lat": 56.1439, "lon": 47.2486, "population": 497266 },
+ { "name": "Тула", "lat": 54.1931, "lon": 37.6172, "population": 475161 },
+ { "name": "Калининград", "lat": 54.7104, "lon": 20.4522, "population": 482443 },
+ { "name": "Балашиха", "lat": 55.7964, "lon": 37.9378, "population": 507692 },
+ { "name": "Ставрополь", "lat": 45.0448, "lon": 41.9690, "population": 458784 },
+ { "name": "Севастополь", "lat": 44.6167, "lon": 33.5254, "population": 449130 },
+ { "name": "Улан-Удэ", "lat": 51.8272, "lon": 107.6063, "population": 439128 },
+ { "name": "Тверь", "lat": 56.8584, "lon": 35.9176, "population": 424969 },
+ { "name": "Магнитогорск", "lat": 53.4078, "lon": 59.0464, "population": 413351 },
+ { "name": "Иваново", "lat": 56.9719, "lon": 40.9714, "population": 404300 },
+ { "name": "Брянск", "lat": 53.2434, "lon": 34.3656, "population": 402300 },
+ { "name": "Сочи", "lat": 43.6028, "lon": 39.7342, "population": 443562 },
+ { "name": "Белгород", "lat": 50.5956, "lon": 36.5871, "population": 391554 },
+ { "name": "Сургут", "lat": 61.2500, "lon": 73.4167, "population": 395705 },
+ { "name": "Владимир", "lat": 56.1366, "lon": 40.3966, "population": 349951 },
+ { "name": "Архангельск", "lat": 64.5401, "lon": 40.5433, "population": 348783 },
+ { "name": "Нижний Тагил", "lat": 57.9197, "lon": 59.9650, "population": 355694 },
+ { "name": "Чита", "lat": 52.0333, "lon": 113.5000, "population": 349005 },
+ { "name": "Калуга", "lat": 54.5293, "lon": 36.2754, "population": 336726 },
+ { "name": "Смоленск", "lat": 54.7818, "lon": 32.0401, "population": 326863 },
+ { "name": "Курган", "lat": 55.4500, "lon": 65.3333, "population": 310951 },
+ { "name": "Волжский", "lat": 48.7854, "lon": 44.7758, "population": 323293 },
+ { "name": "Орёл", "lat": 52.9651, "lon": 36.0785, "population": 310755 },
+ { "name": "Череповец", "lat": 59.1333, "lon": 37.9000, "population": 315744 },
+ { "name": "Грозный", "lat": 43.3175, "lon": 45.6986, "population": 324370 },
+ { "name": "Якутск", "lat": 62.0355, "lon": 129.6755, "population": 318457 },
+ { "name": "Мурманск", "lat": 68.9585, "lon": 33.0827, "population": 295374 },
+ { "name": "Петрозаводск", "lat": 61.7849, "lon": 34.3469, "population": 280662 },
+ { "name": "Саранск", "lat": 54.1838, "lon": 45.1749, "population": 314789 },
+ { "name": "Кострома", "lat": 57.7665, "lon": 40.9265, "population": 276662 },
+ { "name": "Таганрог", "lat": 47.2362, "lon": 38.9349, "population": 245120 },
+ { "name": "Комсомольск-на-Амуре", "lat": 50.5500, "lon": 137.0000, "population": 248280 },
+ { "name": "Сыктывкар", "lat": 61.6681, "lon": 50.8357, "population": 235006 },
+ { "name": "Нижневартовск", "lat": 60.9344, "lon": 76.5531, "population": 279988 },
+ { "name": "Йошкар-Ола", "lat": 56.6372, "lon": 47.8908, "population": 279100 },
+ { "name": "Новороссийск", "lat": 44.7231, "lon": 37.7686, "population": 261430 },
+ { "name": "Дзержинск", "lat": 56.2333, "lon": 43.4667, "population": 230000 },
+ { "name": "Нальчик", "lat": 43.4981, "lon": 43.6189, "population": 239300 },
+ { "name": "Шахты", "lat": 47.7000, "lon": 40.2167, "population": 221845 },
+ { "name": "Энгельс", "lat": 51.4833, "lon": 46.1167, "population": 222918 },
+ { "name": "Рыбинск", "lat": 58.0500, "lon": 38.8500, "population": 182411 },
+ { "name": "Норильск", "lat": 69.3558, "lon": 88.1893, "population": 175365 },
+ { "name": "Альметьевск", "lat": 54.9000, "lon": 52.3000, "population": 155329 },
+ { "name": "Псков", "lat": 57.8136, "lon": 28.3300, "population": 210501 },
+ { "name": "Биийск", "lat": 52.5333, "lon": 85.3333, "population": 197164 },
+ { "name": "Люберцы", "lat": 55.6758, "lon": 37.8933, "population": 201654 },
+ { "name": "Прокопьевск", "lat": 53.9000, "lon": 86.7167, "population": 197164 },
+ { "name": "Мытищи", "lat": 55.9116, "lon": 37.7307, "population": 223067 },
+ { "name": "Златоуст", "lat": 55.1667, "lon": 59.6500, "population": 163367 },
+ { "name": "Каменск-Уральский", "lat": 56.4167, "lon": 61.9333, "population": 168406 },
+ { "name": "Подольск", "lat": 55.4297, "lon": 37.5447, "population": 187961 },
+ { "name": "Петропавловск-Камчатский", "lat": 53.0446, "lon": 158.6504, "population": 179526 },
+ { "name": "Сызрань", "lat": 53.1667, "lon": 48.4667, "population": 163913 },
+ { "name": "Ачинск", "lat": 56.2667, "lon": 90.5000, "population": 107656 },
+ { "name": "Новочеркасск", "lat": 47.4167, "lon": 40.1000, "population": 168746 },
+ { "name": "Электросталь", "lat": 55.7894, "lon": 38.4469, "population": 158652 },
+ { "name": "Первоуральск", "lat": 56.9000, "lon": 59.9500, "population": 123211 },
+ { "name": "Одинцово", "lat": 55.6758, "lon": 37.2808, "population": 137041 },
+ { "name": "Копейск", "lat": 55.1167, "lon": 61.6167, "population": 148872 },
+ { "name": "Хасавюрт", "lat": 43.2500, "lon": 46.5833, "population": 143960 },
+ { "name": "Нефтекамск", "lat": 56.0833, "lon": 54.2667, "population": 133997 },
+ { "name": "Новочебоксарск", "lat": 56.1167, "lon": 47.5000, "population": 129082 },
+ { "name": "Серпухов", "lat": 54.9167, "lon": 37.4167, "population": 126522 },
+ { "name": "Невинномысск", "lat": 44.6333, "lon": 41.9333, "population": 118360 },
+ { "name": "Дмитров", "lat": 56.3500, "lon": 37.5167, "population": 61305 },
+ { "name": "Обнинск", "lat": 55.0833, "lon": 36.6167, "population": 120000 },
+ { "name": "Ангарск", "lat": 52.5333, "lon": 103.9000, "population": 225400 },
+ { "name": "Щёлково", "lat": 55.9167, "lon": 38.0333, "population": 124115 },
+ { "name": "Батайск", "lat": 47.1333, "lon": 39.7500, "population": 123549 },
+ { "name": "Кисловодск", "lat": 43.9000, "lon": 42.7167, "population": 128553 },
+ { "name": "Орехово-Зуево", "lat": 55.8000, "lon": 38.9667, "population": 120670 },
+ { "name": "Майкоп", "lat": 44.6000, "lon": 40.1000, "population": 144246 },
+ { "name": "Пушкино", "lat": 56.0167, "lon": 37.8500, "population": 111000 },
+ { "name": "Армавир", "lat": 44.9833, "lon": 41.1167, "population": 188832 },
+ { "name": "Людино", "lat": 54.7000, "lon": 34.4500, "population": 77000 },
+ { "name": "Кызыл", "lat": 51.7167, "lon": 94.4500, "population": 119633 },
+ { "name": "Уссурийск", "lat": 43.8000, "lon": 131.9500, "population": 158016 },
+ { "name": "Березники", "lat": 59.4167, "lon": 56.8167, "population": 141762 },
+ { "name": "Салават", "lat": 53.3667, "lon": 55.9167, "population": 151100 },
+ { "name": "Миасс", "lat": 55.0333, "lon": 60.1000, "population": 151751 },
+ { "name": "Абакан", "lat": 53.7167, "lon": 91.4333, "population": 181700 },
+ { "name": "Ноябрьск", "lat": 63.2000, "lon": 75.4500, "population": 110620 },
+ { "name": "Ессентуки", "lat": 44.0500, "lon": 42.8667, "population": 111600 },
+ { "name": "Железногорск", "lat": 56.2500, "lon": 93.5333, "population": 93734 },
+ { "name": "Красногорск", "lat": 55.7333, "lon": 37.3333, "population": 179683 },
+ { "name": "Новый Уренгой", "lat": 66.0833, "lon": 76.6333, "population": 118619 },
+ { "name": "Муром", "lat": 55.5667, "lon": 42.0500, "population": 106200 },
+ { "name": "Коломна", "lat": 55.0833, "lon": 38.7833, "population": 144589 },
+ { "name": "Ковров", "lat": 56.3500, "lon": 41.3167, "population": 137111 },
+ { "name": "Пятигорск", "lat": 44.0500, "lon": 43.0500, "population": 149696 },
+ { "name": "Химки", "lat": 55.8972, "lon": 37.4297, "population": 259550 },
+ { "name": "Троицк", "lat": 55.4833, "lon": 37.3000, "population": 61519 },
+ { "name": "Реутов", "lat": 55.7667, "lon": 37.8667, "population": 107022 },
+ { "name": "Долгопрудный", "lat": 55.9333, "lon": 37.5167, "population": 123000 },
+ { "name": "Раменское", "lat": 55.5667, "lon": 38.2333, "population": 122000 },
+ { "name": "Жуковский", "lat": 55.6000, "lon": 38.1167, "population": 107585 },
+ { "name": "Лобня", "lat": 56.0167, "lon": 37.4833, "population": 87000 },
+ { "name": "Клин", "lat": 56.3333, "lon": 36.7333, "population": 78000 },
+ { "name": "Сергиев Посад", "lat": 56.3000, "lon": 38.1333, "population": 104000 },
+ { "name": "Ногинск", "lat": 55.8667, "lon": 38.4333, "population": 101000 },
+ { "name": "Егорьевск", "lat": 55.3833, "lon": 39.0333, "population": 70000 },
+ { "name": "Ступино", "lat": 55.2833, "lon": 38.0833, "population": 66000 },
+ { "name": "Воскресенск", "lat": 55.3167, "lon": 38.6667, "population": 91000 },
+ { "name": "Дубна", "lat": 56.7333, "lon": 37.1667, "population": 75000 },
+ { "name": "Фрязино", "lat": 55.9500, "lon": 38.0500, "population": 57000 },
+ { "name": "Домодедово", "lat": 55.4333, "lon": 37.7667, "population": 125000 },
+ { "name": "Истра", "lat": 55.9167, "lon": 36.8500, "population": 35000 },
+ { "name": "Чехов", "lat": 55.1500, "lon": 37.4667, "population": 75000 },
+ { "name": "Наро-Фоминск", "lat": 55.3833, "lon": 36.7333, "population": 64000 },
+ { "name": "Павловский Посад", "lat": 55.7833, "lon": 38.6500, "population": 61000 },
+ { "name": "Звенигород", "lat": 55.7333, "lon": 36.8500, "population": 18000 },
+ { "name": "Руза", "lat": 55.7000, "lon": 36.1833, "population": 13000 },
+ { "name": "Верея", "lat": 55.3500, "lon": 36.2000, "population": 12000 },
+ { "name": "Можайск", "lat": 55.5000, "lon": 36.0333, "population": 30000 },
+ { "name": "Волоколамск", "lat": 56.0333, "lon": 35.9500, "population": 21000 },
+ { "name": "Шаховская", "lat": 56.0167, "lon": 35.5333, "population": 11000 },
+ { "name": "Лотошино", "lat": 56.0833, "lon": 35.8000, "population": 8000 },
+ { "name": "Талдом", "lat": 56.7333, "lon": 37.5333, "population": 13000 },
+ { "name": "Яхрома", "lat": 56.2833, "lon": 37.4833, "population": 13000 },
+ { "name": "Краснозаводск", "lat": 56.3167, "lon": 37.9000, "population": 13000 },
+ { "name": "Пересвет", "lat": 56.2833, "lon": 38.1667, "population": 5000 },
+ { "name": "Апрелевка", "lat": 55.4833, "lon": 37.2000, "population": 23000 },
+ { "name": "Щербинка", "lat": 55.5000, "lon": 37.5667, "population": 28000 },
+ { "name": "Московский", "lat": 55.6000, "lon": 37.3500, "population": 60000 },
+ { "name": "Кокошкино", "lat": 55.6167, "lon": 37.2500, "population": 14000 },
+ { "name": "Кремёнки", "lat": 55.3833, "lon": 37.4500, "population": 8000 },
+ { "name": "Протвино", "lat": 54.8667, "lon": 37.2333, "population": 13000 },
+ { "name": "Пущино", "lat": 54.8333, "lon": 37.6167, "population": 21000 },
+ { "name": "Молодёжный", "lat": 55.5667, "lon": 37.2167, "population": 9000 },
+ { "name": "Внуково", "lat": 55.6000, "lon": 37.3500, "population": 8000 },
+ { "name": "Михайловское", "lat": 55.5500, "lon": 37.3000, "population": 7000 },
+ { "name": "Филимонки", "lat": 55.5667, "lon": 37.3833, "population": 6000 },
+ { "name": "Марушкино", "lat": 55.6167, "lon": 37.2833, "population": 5000 },
+ { "name": "Румянцево", "lat": 55.6333, "lon": 37.3833, "population": 4000 },
+ { "name": "Сосенки", "lat": 55.5833, "lon": 37.4167, "population": 3000 },
+ { "name": "Газопровод", "lat": 55.6000, "lon": 37.4000, "population": 2000 },
+ { "name": "Коммунарка", "lat": 55.5667, "lon": 37.5167, "population": 25000 }
+]
\ No newline at end of file
diff --git a/docker-compose.yaml b/docker-compose.yaml
new file mode 100644
index 00000000..9c09c919
--- /dev/null
+++ b/docker-compose.yaml
@@ -0,0 +1,8 @@
+version: '3.8'
+services:
+ web:
+ image: nginx:alpine
+ ports:
+ - "8080:80"
+ volumes:
+ - ./:/usr/share/nginx/html:ro
diff --git a/index.html b/index.html
new file mode 100644
index 00000000..cb4d0883
--- /dev/null
+++ b/index.html
@@ -0,0 +1,93 @@
+
+
+
+
+
+ Прогноз погоды на карте
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
🔍 Найти населённый пункт
+
+
+
+
+
+ ⚡ Кликните по маркеру на карте или найдите город через поиск
+
+
+
+
+
+
+
+
🌡️ Температура (°C)
+
+
+
+
🌧️ Осадки (мм/сутки)
+
+
+
+
💨 Ветер (м/с)
+
+
+
+
+
+ 💡 Нажмите на любой город на карте, чтобы увидеть прогноз на несколько дней
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/script.js b/script.js
new file mode 100644
index 00000000..1dbefdaf
--- /dev/null
+++ b/script.js
@@ -0,0 +1,242 @@
+// script.js – улучшенная версия с отладкой
+
+// ------------------- 1. ИНИЦИАЛИЗАЦИЯ КАРТЫ -------------------
+let map = L.map('map').setView([64.0, 100.0], 4);
+L.tileLayer('https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png', {
+ attribution: '© OSM'
+}).addTo(map);
+
+// ------------------- 2. ГЛОБАЛЬНЫЕ ПЕРЕМЕННЫЕ -------------------
+let cities = []; // массив городов в формате {name, latitude, longitude}
+let markers = {}; // объект для быстрого доступа к маркерам по имени
+let activePopup = null; // для закрытия предыдущего попапа
+
+// ------------------- 3. ЗАГРУЗКА ГОРОДОВ (с fallback) -------------------
+async function loadCities() {
+ console.log("Загрузка данных о городах...");
+
+ // Пытаемся загрузить russia-cities.json
+ let data = null;
+ try {
+ const response = await fetch('russia-cities.json');
+ if (response.ok) {
+ data = await response.json();
+ console.log(`✅ Загружен russia-cities.json, количество записей: ${data.length}`);
+ } else {
+ console.warn("russia-cities.json не найден, пробуем cities.json");
+ }
+ } catch (e) {
+ console.warn("Ошибка загрузки russia-cities.json", e);
+ }
+
+ // Если не загрузился, пробуем cities.json (простой формат)
+ if (!data) {
+ try {
+ const response = await fetch('cities.json');
+ if (response.ok) {
+ data = await response.json();
+ console.log(`✅ Загружен cities.json, количество записей: ${data.length}`);
+ }
+ } catch (e) {
+ console.warn("cities.json не найден");
+ }
+ }
+
+ // Если данные есть – преобразуем в единый формат
+ if (data && Array.isArray(data)) {
+ // Определяем формат: если первый объект имеет поля coords.lat, то это формат russia-cities
+ if (data[0] && data[0].coords && typeof data[0].coords.lat === 'number') {
+ cities = data.map(city => ({
+ name: city.name,
+ latitude: city.coords.lat,
+ longitude: city.coords.lon,
+ population: city.population
+ }));
+ console.log("✅ Преобразован формат russia-cities.json");
+ }
+ // Если уже есть поля name, latitude, longitude
+ // СТАЛО
+ else if (data[0] && typeof data[0].lat === 'number' && typeof data[0].lon === 'number') {
+ cities = data.map(city => ({
+ name: city.name,
+ latitude: city.lat,
+ longitude: city.lon,
+ population: city.population
+ }));
+ console.log("✅ Используется формат {name, lat, lon}");
+ }
+// Если есть поля latitude/longitude
+ else if (data[0] && typeof data[0].latitude === 'number' && typeof data[0].longitude === 'number') {
+ cities = data;
+ console.log("✅ Используется формат {name, latitude, longitude}");
+ }
+ else {
+ console.error("❌ Неизвестный формат JSON. Ожидается массив с name/latitude/longitude или name/coords.lat/lon");
+ cities = getFallbackCities();
+ }
+ } else {
+ console.warn("⚠️ Нет загруженных данных, используем встроенный список из 5 городов");
+ cities = getFallbackCities();
+ }
+
+ console.log(`Итоговое количество городов для отображения: ${cities.length}`);
+ addMarkersToMap();
+}
+
+// Встроенный список на случай, если JSON не загрузился (чтобы вы сразу увидели маркеры)
+function getFallbackCities() {
+ return [
+ { name: "Москва", latitude: 55.751244, longitude: 37.618423 },
+ { name: "Санкт-Петербург", latitude: 59.931058, longitude: 30.360909 },
+ { name: "Новосибирск", latitude: 55.030199, longitude: 82.920430 },
+ { name: "Екатеринбург", latitude: 56.838011, longitude: 60.597474 },
+ { name: "Казань", latitude: 55.796127, longitude: 49.106405 }
+ ];
+}
+
+// ------------------- 4. ДОБАВЛЕНИЕ МАРКЕРОВ НА КАРТУ -------------------
+function addMarkersToMap() {
+ // Очищаем старые маркеры, если есть
+ for (let key in markers) {
+ if (markers[key]) map.removeLayer(markers[key]);
+ }
+ markers = {};
+
+ cities.forEach(city => {
+ // Проверяем, что координаты корректны
+ if (!isFinite(city.latitude) || !isFinite(city.longitude)) {
+ console.warn(`Некорректные координаты для города ${city.name}`, city);
+ return;
+ }
+
+ const marker = L.marker([city.latitude, city.longitude]).addTo(map);
+ marker.on('click', () => {
+ // Закрываем предыдущий попап, если он открыт
+ if (activePopup) map.closePopup(activePopup);
+ fetchWeather(city.latitude, city.longitude, city.name);
+ });
+
+ // Добавляем всплывающую подсказку при наведении (необязательно)
+ marker.bindTooltip(city.name, { sticky: true });
+
+ markers[city.name] = marker;
+ });
+ console.log(`✅ Добавлено маркеров: ${Object.keys(markers).length}`);
+}
+
+// ------------------- 5. ПОИСК ГОРОДА -------------------
+const searchInput = document.getElementById('city-search');
+const searchBtn = document.getElementById('search-btn');
+
+function searchCity() {
+ const query = searchInput.value.trim().toLowerCase();
+ if (!query) {
+ alert("Введите название города");
+ return;
+ }
+
+ // Ищем точное совпадение или начинающееся с запроса
+ const foundCity = cities.find(city =>
+ city.name.toLowerCase() === query ||
+ city.name.toLowerCase().startsWith(query)
+ );
+
+ if (foundCity) {
+ map.setView([foundCity.latitude, foundCity.longitude], 10);
+ // Имитируем клик по маркеру
+ if (markers[foundCity.name]) {
+ markers[foundCity.name].fire('click');
+ } else {
+ // На случай, если маркер почему-то не создан
+ fetchWeather(foundCity.latitude, foundCity.longitude, foundCity.name);
+ }
+ } else {
+ alert(`Город "${searchInput.value}" не найден. Попробуйте: Москва, Санкт-Петербург, Новосибирск...`);
+ }
+}
+
+searchBtn.addEventListener('click', searchCity);
+searchInput.addEventListener('keypress', (e) => {
+ if (e.key === 'Enter') searchCity();
+});
+
+// ------------------- 6. РАБОТА С ПОГОДНЫМ API -------------------
+const API_TOKEN = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0b2tlbl90eXBlIjoiYWNjZXNzIiwiZXhwIjoxNzc1Mzg3MjA1LCJpYXQiOjE3NzUzODY5MDUsImp0aSI6ImYwNjQ2NzU1NDY2ZTRmMDk4YjJmZjc0ODJjYWU0OWMxIiwidXNlcl9pZCI6IjM2NTQifQ.TXWDYHAIz-VBflSnF4mbF79GGisZOTeg9y2ANTSw6vI';
+const API_URL = 'https://api.projecteol.ru/v1/forecast/';
+
+async function fetchWeather(lat, lon, cityName) {
+ try {
+ document.getElementById('weather-title').innerHTML = `⏳ Загрузка погоды для ${cityName}...`;
+
+ // Если нет токена – показываем демо-данные
+ if (API_TOKEN === 'ТВОЙ_УНИКАЛЬНЫЙ_ТОКЕН') {
+ console.warn("⚠️ Не задан API-ключ. Используем демонстрационные данные.");
+ showDemoWeather(cityName);
+ return;
+ }
+
+ const url = `${API_URL}?lat=${lat}&lon=${lon}&token=${API_TOKEN}`;
+ const response = await fetch(url);
+ if (!response.ok) throw new Error(`HTTP ${response.status}`);
+ const data = await response.json();
+ processWeatherData(data, cityName);
+ } catch (error) {
+ console.error('Ошибка API погоды:', error);
+ document.getElementById('weather-title').innerHTML = `❌ Ошибка загрузки для ${cityName}`;
+ showDemoWeather(cityName); // временно покажем демо-графики
+ }
+}
+
+// Демо-данные, чтобы графики хоть как-то рисовались (пока нет токена)
+function showDemoWeather(cityName) {
+ const dates = ['День 1', 'День 2', 'День 3', 'День 4', 'День 5'];
+ const temps = [5, 7, 6, 4, 3];
+ const rains = [2, 0, 5, 1, 8];
+ const winds = [4, 5, 3, 6, 7];
+ document.getElementById('weather-title').innerHTML = `🌦️ Демо-прогноз: ${cityName}`;
+ updateChart('tempChart', dates, temps, 'Температура (°C)', 'rgba(255,99,132,0.2)', 'rgba(255,99,132,1)', 'line');
+ updateChart('rainChart', dates, rains, 'Осадки (мм)', 'rgba(54,162,235,0.2)', 'rgba(54,162,235,1)', 'bar');
+ updateChart('windChart', dates, winds, 'Ветер (м/с)', 'rgba(75,192,192,0.2)', 'rgba(75,192,192,1)', 'line');
+}
+
+function processWeatherData(data, cityName) {
+ // Здесь должен быть разбор реального ответа от projecteol
+ // Пока заглушка – вызываем демо
+ showDemoWeather(cityName);
+}
+
+// ------------------- 7. ОТРИСОВКА ГРАФИКОВ -------------------
+function updateChart(chartId, labels, data, label, bgColor, borderColor, type) {
+ const ctx = document.getElementById(chartId).getContext('2d');
+ if (window[chartId]) {
+ window[chartId].data.labels = labels;
+ window[chartId].data.datasets[0].data = data;
+ window[chartId].update();
+ } else {
+ window[chartId] = new Chart(ctx, {
+ type: type,
+ data: {
+ labels: labels,
+ datasets: [{
+ label: label,
+ data: data,
+ backgroundColor: bgColor,
+ borderColor: borderColor,
+ borderWidth: 2,
+ fill: true
+ }]
+ },
+ options: {
+ responsive: true,
+ maintainAspectRatio: false
+ }
+ });
+ }
+ // Убираем заглушку "нажмите на город"
+ const infoMsg = document.getElementById('info-message');
+ if (infoMsg) infoMsg.style.display = 'none';
+}
+// Инициализация карты и загрузка городов
+document.addEventListener('DOMContentLoaded', () => {
+ loadCities(); // Это нужно добавить!
+});
\ No newline at end of file
diff --git a/styles.css b/styles.css
new file mode 100644
index 00000000..427f15fb
--- /dev/null
+++ b/styles.css
@@ -0,0 +1,189 @@
+* {
+ margin: 0;
+ padding: 0;
+ box-sizing: border-box;
+}
+
+body {
+ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
+ background: #f0f2f5;
+ height: 100vh;
+ overflow: hidden;
+}
+
+/* Основной контейнер: карта + панель */
+.app-container {
+ display: flex;
+ height: 100vh;
+ width: 100%;
+}
+
+/* Левая часть — карта */
+.map-container {
+ flex: 2;
+ position: relative;
+ border-radius: 12px;
+ margin: 12px 0 12px 12px;
+ box-shadow: 0 4px 20px rgba(0,0,0,0.1);
+ overflow: hidden;
+}
+
+#map {
+ height: 100%;
+ width: 100%;
+ border-radius: 12px;
+}
+
+/* Правая панель — поиск + графики */
+.dashboard {
+ flex: 1.2;
+ background: white;
+ margin: 12px 12px 12px 0;
+ border-radius: 12px;
+ box-shadow: 0 4px 20px rgba(0,0,0,0.1);
+ display: flex;
+ flex-direction: column;
+ overflow-y: auto;
+ padding: 20px;
+}
+
+/* Поиск */
+.search-section {
+ margin-bottom: 25px;
+ background: #f8f9fa;
+ padding: 15px;
+ border-radius: 16px;
+}
+
+.search-section h3 {
+ margin-bottom: 12px;
+ color: #1e2a3a;
+ font-size: 1.2rem;
+}
+
+.search-box {
+ display: flex;
+ gap: 10px;
+}
+
+#city-search {
+ flex: 1;
+ padding: 12px 15px;
+ border: 1px solid #ddd;
+ border-radius: 40px;
+ font-size: 1rem;
+ outline: none;
+ transition: 0.2s;
+}
+
+#city-search:focus {
+ border-color: #2c7da0;
+ box-shadow: 0 0 0 2px rgba(44,125,160,0.2);
+}
+
+#search-btn {
+ background: #2c7da0;
+ color: white;
+ border: none;
+ padding: 0 25px;
+ border-radius: 40px;
+ font-weight: bold;
+ cursor: pointer;
+ transition: 0.2s;
+ font-size: 1rem;
+}
+
+#search-btn:hover {
+ background: #1f5e7a;
+}
+
+/* Заголовок погоды */
+.weather-header {
+ margin: 10px 0 20px 0;
+ text-align: center;
+}
+
+#weather-title {
+ font-size: 1.5rem;
+ font-weight: 600;
+ color: #0b3b4f;
+ border-bottom: 3px solid #2c7da0;
+ display: inline-block;
+ padding-bottom: 5px;
+}
+
+/* Блоки графиков */
+.charts-container {
+ display: flex;
+ flex-direction: column;
+ gap: 30px;
+}
+
+.chart-card {
+ background: #ffffff;
+ border-radius: 20px;
+ padding: 15px;
+ box-shadow: 0 2px 8px rgba(0,0,0,0.05);
+ border: 1px solid #e9ecef;
+}
+
+.chart-card h4 {
+ margin-bottom: 15px;
+ color: #2c3e50;
+ font-size: 1.1rem;
+ display: flex;
+ align-items: center;
+ gap: 8px;
+}
+
+canvas {
+ max-height: 220px;
+ width: 100%;
+}
+
+/* Информация при отсутствии данных */
+.placeholder-message {
+ text-align: center;
+ color: #6c757d;
+ padding: 40px 20px;
+ background: #f8f9fa;
+ border-radius: 20px;
+ margin-top: 20px;
+}
+
+/* Адаптивность */
+@media (max-width: 800px) {
+ .app-container {
+ flex-direction: column;
+ }
+ .map-container {
+ flex: 1;
+ margin: 12px;
+ min-height: 40vh;
+ }
+ .dashboard {
+ flex: 1;
+ margin: 0 12px 12px 12px;
+ max-height: 50vh;
+ }
+}
+
+/* Стили для всплывающих подсказок Leaflet (немного улучшим) */
+.leaflet-popup-content {
+ font-size: 14px;
+ font-weight: 500;
+ color: #1e4663;
+}
+
+/* Скролл для панели */
+.dashboard::-webkit-scrollbar {
+ width: 6px;
+}
+.dashboard::-webkit-scrollbar-track {
+ background: #f1f1f1;
+ border-radius: 10px;
+}
+.dashboard::-webkit-scrollbar-thumb {
+ background: #b9c4ce;
+ border-radius: 10px;
+}
\ No newline at end of file
From af88ce64e43b706bef5ae741d325f4f4dc810d6e Mon Sep 17 00:00:00 2001
From: Mary
Date: Mon, 6 Apr 2026 19:44:12 +0400
Subject: [PATCH 2/7] =?UTF-8?q?=D0=BF=D0=BE=D0=BF=D1=8B=D1=82=D0=BA=D0=B0?=
=?UTF-8?q?=20=D1=81=D0=B4=D0=B5=D0=BB=D0=B0=D1=82=D1=8C=20=D1=87=D0=B5?=
=?UTF-8?q?=D1=80=D0=B5=D0=B7=20=D0=BF=D1=80=D0=BE=D0=BA=D1=81=D0=B8?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
index.html | 4 +-
nginx.conf | 39 +++++
proxy.py | 42 +++++
script.js | 76 ++++----
styles.css => style.css | 376 ++++++++++++++++++++--------------------
test.html | 54 ++++++
6 files changed, 370 insertions(+), 221 deletions(-)
create mode 100644 nginx.conf
create mode 100644 proxy.py
rename styles.css => style.css (94%)
create mode 100644 test.html
diff --git a/index.html b/index.html
index cb4d0883..10fe24d7 100644
--- a/index.html
+++ b/index.html
@@ -8,12 +8,10 @@
-
-
-
+
diff --git a/nginx.conf b/nginx.conf
new file mode 100644
index 00000000..3ea9499c
--- /dev/null
+++ b/nginx.conf
@@ -0,0 +1,39 @@
+events {
+ worker_connections 1024;
+}
+
+http {
+ server {
+ listen 80;
+
+ # Статический контент (твой сайт)
+ location / {
+ root /usr/share/nginx/html;
+ index index.html;
+ try_files $uri $uri/ /index.html;
+ }
+
+ # Проксирование запросов к API
+ location /weather {
+ proxy_pass https://176.59.144.152/v1/forecast; # IP-адрес api.projecteol.ru
+ proxy_set_header Host $host;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ proxy_set_header X-Forwarded-Proto $scheme;
+
+ # Добавляем заголовки для CORS
+ add_header 'Access-Control-Allow-Origin' '*';
+ add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
+ add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range';
+ add_header 'Access-Control-Expose-Headers' 'Content-Length,Content-Range';
+
+ # Обработка OPTIONS запросов
+ if ($request_method = 'OPTIONS') {
+ add_header 'Access-Control-Max-Age' 1728000;
+ add_header 'Content-Type' 'text/plain charset=UTF-8';
+ add_header 'Content-Length' 0;
+ return 204;
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/proxy.py b/proxy.py
new file mode 100644
index 00000000..78ccc692
--- /dev/null
+++ b/proxy.py
@@ -0,0 +1,42 @@
+from flask import Flask, request, jsonify
+from flask_cors import CORS
+import requests
+from datetime import datetime
+
+app = Flask(__name__)
+CORS(app)
+
+@app.route('/weather', methods=['GET'])
+def weather_proxy():
+ lat = request.args.get('lat')
+ lon = request.args.get('lon')
+ token = request.args.get('token')
+ date = request.args.get('date', default=datetime.now().strftime('%Y-%m-%d'))
+
+ if not lat or not lon or not token:
+ return jsonify({"error": "Missing parameters: lat, lon, token"}), 400
+
+ # Исправленный URL (соответствует реальному API)
+ target_url = f'https://projecteol.ru/ru/api/weather?lat={lat}&lon={lon}&date={date}&token={token}'
+
+ try:
+ headers = {
+ 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
+ }
+ response = requests.get(target_url, headers=headers, timeout=10)
+
+ return jsonify(response.json()), response.status_code, {
+ 'Access-Control-Allow-Origin': '*',
+ 'Access-Control-Allow-Methods': 'GET, OPTIONS',
+ 'Access-Control-Allow-Headers': 'Content-Type'
+ }
+
+ except Exception as e:
+ return jsonify({
+ "error": "Proxy error",
+ "message": str(e),
+ "url": target_url
+ }), 500, {'Access-Control-Allow-Origin': '*'}
+
+if __name__ == '__main__':
+ app.run(host='0.0.0.0', port=8080, debug=True) # Важно: 0.0.0.0!
\ No newline at end of file
diff --git a/script.js b/script.js
index 1dbefdaf..4ed7a1d0 100644
--- a/script.js
+++ b/script.js
@@ -18,30 +18,16 @@ async function loadCities() {
// Пытаемся загрузить russia-cities.json
let data = null;
try {
- const response = await fetch('russia-cities.json');
+ const response = await fetch('cities.json');
if (response.ok) {
data = await response.json();
- console.log(`✅ Загружен russia-cities.json, количество записей: ${data.length}`);
- } else {
- console.warn("russia-cities.json не найден, пробуем cities.json");
+ console.log(`✅ Загружен cities.json, количество записей: ${data.length}`);
}
} catch (e) {
- console.warn("Ошибка загрузки russia-cities.json", e);
- }
-
- // Если не загрузился, пробуем cities.json (простой формат)
- if (!data) {
- try {
- const response = await fetch('cities.json');
- if (response.ok) {
- data = await response.json();
- console.log(`✅ Загружен cities.json, количество записей: ${data.length}`);
- }
- } catch (e) {
console.warn("cities.json не найден");
- }
}
+
// Если данные есть – преобразуем в единый формат
if (data && Array.isArray(data)) {
// Определяем формат: если первый объект имеет поля coords.lat, то это формат russia-cities
@@ -163,19 +149,13 @@ searchInput.addEventListener('keypress', (e) => {
// ------------------- 6. РАБОТА С ПОГОДНЫМ API -------------------
const API_TOKEN = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0b2tlbl90eXBlIjoiYWNjZXNzIiwiZXhwIjoxNzc1Mzg3MjA1LCJpYXQiOjE3NzUzODY5MDUsImp0aSI6ImYwNjQ2NzU1NDY2ZTRmMDk4YjJmZjc0ODJjYWU0OWMxIiwidXNlcl9pZCI6IjM2NTQifQ.TXWDYHAIz-VBflSnF4mbF79GGisZOTeg9y2ANTSw6vI';
const API_URL = 'https://api.projecteol.ru/v1/forecast/';
-
async function fetchWeather(lat, lon, cityName) {
try {
document.getElementById('weather-title').innerHTML = `⏳ Загрузка погоды для ${cityName}...`;
-
- // Если нет токена – показываем демо-данные
- if (API_TOKEN === 'ТВОЙ_УНИКАЛЬНЫЙ_ТОКЕН') {
- console.warn("⚠️ Не задан API-ключ. Используем демонстрационные данные.");
- showDemoWeather(cityName);
- return;
- }
-
- const url = `${API_URL}?lat=${lat}&lon=${lon}&token=${API_TOKEN}`;
+
+ //const url = `${API_URL}?lat=${lat}&lon=${lon}&token=${API_TOKEN}`;
+ const today = new Date().toISOString().split('T')[0];
+ const url = `http://localhost:8080/weather?lat=${lat}&lon=${lon}&date=${today}&token=${API_TOKEN}`;
const response = await fetch(url);
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const data = await response.json();
@@ -200,9 +180,45 @@ function showDemoWeather(cityName) {
}
function processWeatherData(data, cityName) {
- // Здесь должен быть разбор реального ответа от projecteol
- // Пока заглушка – вызываем демо
- showDemoWeather(cityName);
+ // data - это массив почасовых прогнозов
+ if (!Array.isArray(data) || data.length === 0) {
+ console.error("Нет данных от API", data);
+ showDemoWeather(cityName);
+ return;
+ }
+
+ // Группируем по дням
+ const daily = {};
+ data.forEach(item => {
+ const date = item.dt_forecast.split(' ')[0]; // "2026-04-06"
+ if (!daily[date]) {
+ daily[date] = { temps: [], rains: [], winds: [] };
+ }
+ daily[date].temps.push(item.temp_2_cel);
+ daily[date].rains.push(item.prate);
+ daily[date].winds.push(item.wind_speed_10);
+ });
+
+ // Вычисляем средние за день
+ const dates = [];
+ const temps = [];
+ const rains = [];
+ const winds = [];
+
+ for (const [date, values] of Object.entries(daily)) {
+ dates.push(date);
+ const avgTemp = values.temps.reduce((a,b) => a+b,0) / values.temps.length;
+ temps.push(avgTemp.toFixed(1));
+ const totalRain = values.rains.reduce((a,b) => a+b,0);
+ rains.push(totalRain.toFixed(1));
+ const avgWind = values.winds.reduce((a,b) => a+b,0) / values.winds.length;
+ winds.push(avgWind.toFixed(1));
+ }
+
+ document.getElementById('weather-title').innerHTML = `🌤️ Прогноз погоды: ${cityName}`;
+ updateChart('tempChart', dates, temps, 'Температура (°C)', 'rgba(255,99,132,0.2)', 'rgba(255,99,132,1)', 'line');
+ updateChart('rainChart', dates, rains, 'Осадки (мм)', 'rgba(54,162,235,0.2)', 'rgba(54,162,235,1)', 'bar');
+ updateChart('windChart', dates, winds, 'Ветер (м/с)', 'rgba(75,192,192,0.2)', 'rgba(75,192,192,1)', 'line');
}
// ------------------- 7. ОТРИСОВКА ГРАФИКОВ -------------------
diff --git a/styles.css b/style.css
similarity index 94%
rename from styles.css
rename to style.css
index 427f15fb..253be893 100644
--- a/styles.css
+++ b/style.css
@@ -1,189 +1,189 @@
-* {
- margin: 0;
- padding: 0;
- box-sizing: border-box;
-}
-
-body {
- font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
- background: #f0f2f5;
- height: 100vh;
- overflow: hidden;
-}
-
-/* Основной контейнер: карта + панель */
-.app-container {
- display: flex;
- height: 100vh;
- width: 100%;
-}
-
-/* Левая часть — карта */
-.map-container {
- flex: 2;
- position: relative;
- border-radius: 12px;
- margin: 12px 0 12px 12px;
- box-shadow: 0 4px 20px rgba(0,0,0,0.1);
- overflow: hidden;
-}
-
-#map {
- height: 100%;
- width: 100%;
- border-radius: 12px;
-}
-
-/* Правая панель — поиск + графики */
-.dashboard {
- flex: 1.2;
- background: white;
- margin: 12px 12px 12px 0;
- border-radius: 12px;
- box-shadow: 0 4px 20px rgba(0,0,0,0.1);
- display: flex;
- flex-direction: column;
- overflow-y: auto;
- padding: 20px;
-}
-
-/* Поиск */
-.search-section {
- margin-bottom: 25px;
- background: #f8f9fa;
- padding: 15px;
- border-radius: 16px;
-}
-
-.search-section h3 {
- margin-bottom: 12px;
- color: #1e2a3a;
- font-size: 1.2rem;
-}
-
-.search-box {
- display: flex;
- gap: 10px;
-}
-
-#city-search {
- flex: 1;
- padding: 12px 15px;
- border: 1px solid #ddd;
- border-radius: 40px;
- font-size: 1rem;
- outline: none;
- transition: 0.2s;
-}
-
-#city-search:focus {
- border-color: #2c7da0;
- box-shadow: 0 0 0 2px rgba(44,125,160,0.2);
-}
-
-#search-btn {
- background: #2c7da0;
- color: white;
- border: none;
- padding: 0 25px;
- border-radius: 40px;
- font-weight: bold;
- cursor: pointer;
- transition: 0.2s;
- font-size: 1rem;
-}
-
-#search-btn:hover {
- background: #1f5e7a;
-}
-
-/* Заголовок погоды */
-.weather-header {
- margin: 10px 0 20px 0;
- text-align: center;
-}
-
-#weather-title {
- font-size: 1.5rem;
- font-weight: 600;
- color: #0b3b4f;
- border-bottom: 3px solid #2c7da0;
- display: inline-block;
- padding-bottom: 5px;
-}
-
-/* Блоки графиков */
-.charts-container {
- display: flex;
- flex-direction: column;
- gap: 30px;
-}
-
-.chart-card {
- background: #ffffff;
- border-radius: 20px;
- padding: 15px;
- box-shadow: 0 2px 8px rgba(0,0,0,0.05);
- border: 1px solid #e9ecef;
-}
-
-.chart-card h4 {
- margin-bottom: 15px;
- color: #2c3e50;
- font-size: 1.1rem;
- display: flex;
- align-items: center;
- gap: 8px;
-}
-
-canvas {
- max-height: 220px;
- width: 100%;
-}
-
-/* Информация при отсутствии данных */
-.placeholder-message {
- text-align: center;
- color: #6c757d;
- padding: 40px 20px;
- background: #f8f9fa;
- border-radius: 20px;
- margin-top: 20px;
-}
-
-/* Адаптивность */
-@media (max-width: 800px) {
- .app-container {
- flex-direction: column;
- }
- .map-container {
- flex: 1;
- margin: 12px;
- min-height: 40vh;
- }
- .dashboard {
- flex: 1;
- margin: 0 12px 12px 12px;
- max-height: 50vh;
- }
-}
-
-/* Стили для всплывающих подсказок Leaflet (немного улучшим) */
-.leaflet-popup-content {
- font-size: 14px;
- font-weight: 500;
- color: #1e4663;
-}
-
-/* Скролл для панели */
-.dashboard::-webkit-scrollbar {
- width: 6px;
-}
-.dashboard::-webkit-scrollbar-track {
- background: #f1f1f1;
- border-radius: 10px;
-}
-.dashboard::-webkit-scrollbar-thumb {
- background: #b9c4ce;
- border-radius: 10px;
+* {
+ margin: 0;
+ padding: 0;
+ box-sizing: border-box;
+}
+
+body {
+ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
+ background: #f0f2f5;
+ height: 100vh;
+ overflow: hidden;
+}
+
+/* Основной контейнер: карта + панель */
+.app-container {
+ display: flex;
+ height: 100vh;
+ width: 100%;
+}
+
+/* Левая часть — карта */
+.map-container {
+ flex: 2;
+ position: relative;
+ border-radius: 12px;
+ margin: 12px 0 12px 12px;
+ box-shadow: 0 4px 20px rgba(0,0,0,0.1);
+ overflow: hidden;
+}
+
+#map {
+ height: 100%;
+ width: 100%;
+ border-radius: 12px;
+}
+
+/* Правая панель — поиск + графики */
+.dashboard {
+ flex: 1.2;
+ background: white;
+ margin: 12px 12px 12px 0;
+ border-radius: 12px;
+ box-shadow: 0 4px 20px rgba(0,0,0,0.1);
+ display: flex;
+ flex-direction: column;
+ overflow-y: auto;
+ padding: 20px;
+}
+
+/* Поиск */
+.search-section {
+ margin-bottom: 25px;
+ background: #f8f9fa;
+ padding: 15px;
+ border-radius: 16px;
+}
+
+.search-section h3 {
+ margin-bottom: 12px;
+ color: #1e2a3a;
+ font-size: 1.2rem;
+}
+
+.search-box {
+ display: flex;
+ gap: 10px;
+}
+
+#city-search {
+ flex: 1;
+ padding: 12px 15px;
+ border: 1px solid #ddd;
+ border-radius: 40px;
+ font-size: 1rem;
+ outline: none;
+ transition: 0.2s;
+}
+
+#city-search:focus {
+ border-color: #2c7da0;
+ box-shadow: 0 0 0 2px rgba(44,125,160,0.2);
+}
+
+#search-btn {
+ background: #2c7da0;
+ color: white;
+ border: none;
+ padding: 0 25px;
+ border-radius: 40px;
+ font-weight: bold;
+ cursor: pointer;
+ transition: 0.2s;
+ font-size: 1rem;
+}
+
+#search-btn:hover {
+ background: #1f5e7a;
+}
+
+/* Заголовок погоды */
+.weather-header {
+ margin: 10px 0 20px 0;
+ text-align: center;
+}
+
+#weather-title {
+ font-size: 1.5rem;
+ font-weight: 600;
+ color: #0b3b4f;
+ border-bottom: 3px solid #2c7da0;
+ display: inline-block;
+ padding-bottom: 5px;
+}
+
+/* Блоки графиков */
+.charts-container {
+ display: flex;
+ flex-direction: column;
+ gap: 30px;
+}
+
+.chart-card {
+ background: #ffffff;
+ border-radius: 20px;
+ padding: 15px;
+ box-shadow: 0 2px 8px rgba(0,0,0,0.05);
+ border: 1px solid #e9ecef;
+}
+
+.chart-card h4 {
+ margin-bottom: 15px;
+ color: #2c3e50;
+ font-size: 1.1rem;
+ display: flex;
+ align-items: center;
+ gap: 8px;
+}
+
+canvas {
+ max-height: 220px;
+ width: 100%;
+}
+
+/* Информация при отсутствии данных */
+.placeholder-message {
+ text-align: center;
+ color: #6c757d;
+ padding: 40px 20px;
+ background: #f8f9fa;
+ border-radius: 20px;
+ margin-top: 20px;
+}
+
+/* Адаптивность */
+@media (max-width: 800px) {
+ .app-container {
+ flex-direction: column;
+ }
+ .map-container {
+ flex: 1;
+ margin: 12px;
+ min-height: 40vh;
+ }
+ .dashboard {
+ flex: 1;
+ margin: 0 12px 12px 12px;
+ max-height: 50vh;
+ }
+}
+
+/* Стили для всплывающих подсказок Leaflet (немного улучшим) */
+.leaflet-popup-content {
+ font-size: 14px;
+ font-weight: 500;
+ color: #1e4663;
+}
+
+/* Скролл для панели */
+.dashboard::-webkit-scrollbar {
+ width: 6px;
+}
+.dashboard::-webkit-scrollbar-track {
+ background: #f1f1f1;
+ border-radius: 10px;
+}
+.dashboard::-webkit-scrollbar-thumb {
+ background: #b9c4ce;
+ border-radius: 10px;
}
\ No newline at end of file
diff --git a/test.html b/test.html
new file mode 100644
index 00000000..18ffbee5
--- /dev/null
+++ b/test.html
@@ -0,0 +1,54 @@
+
+
+
+
+
Тест карты + Open-Meteo
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
From 27f844327c0943cdb22e34b1d8a4b7d402b86f06 Mon Sep 17 00:00:00 2001
From: Lisyonok04 <125237397+Lisyonok04@users.noreply.github.com>
Date: Mon, 6 Apr 2026 19:59:06 +0400
Subject: [PATCH 3/7] Update README.md
---
README.md | 46 ++--------------------------------------------
1 file changed, 2 insertions(+), 44 deletions(-)
diff --git a/README.md b/README.md
index 163d41b9..07f45d02 100644
--- a/README.md
+++ b/README.md
@@ -1,47 +1,4 @@
# Безопасность веб-приложений. Лабораторка №2
-
-## Схема сдачи
-
-1. Получить задание
-2. Сделать форк данного репозитория
-3. Выполнить задание согласно полученному варианту
-4. Сделать PR (pull request) в данный репозиторий
-6. Исправить замечания после code review
-7. Получить approve
-8. Прийти на занятие и защитить работу
-
-Что нужно проявить в работе:
-- умение разработать завершенное целое веб-приложение, с клиентской и серверной частями (допустимы открытые АПИ)
-- навыки верстки на html в объеме 200-300 тегов
-- навыки применения css для лейаута и стилизации, желательно с адаптацией к мобилке
-- использование jQuery или аналогичных JS-фреймворков
-- динамическая подгрузка контента
-- динамическое изменение DOM и CSSOM
-
-Если у вас своя идея по заданию, то расскажите, обсудим и подкорректирую.
-
-## Вариант 1. Расписания
-
-Сделать аналог раздела https://ssau.ru/rasp?groupId=531030143
-
-Какие нужны возможности:
-- справочники групп, табличные данные по расписаниям добывать с настоящего сайта на серверной стороне приложения
-- в клиентскую часть подгружать эти сведения динамически по JSON-API
-- обеспечить возможность смотреть расписания в разрезе группы или препода
-- обеспечить возможность выбора учебной недели (по умолчанию выбирается автоматически)
-
-## Вариант 2. Аналог Прибывалки для электричек
-
-Сделать веб-версию Прибывалки, только для электричек
-
-Какие нужны возможности:
-- находить желаемую ЖД-станцию поиском по названию и по карте
-- отображать расписания всех проходящих поездов через выбранную станцию
-- отображать расписания для поездов между двумя станциями
-- работа через АПИ Яндекс.Расписаний https://yandex.ru/dev/rasp/doc/ru/ (доступ получите сами)
-- хорошая работа в условиях экрана смартфона
-- бонус: функция "любимых остановок"
-
## Вариант 3. Прогноз погоды
Сделать одностраничный сайт с картой, на которой можно выбрать населенный пункт и получить прогноз погоды на несколько дней по нему.
@@ -54,7 +11,8 @@
- можете реализовать с собственным серверным компонентом или придумать, как обойтись без него
-
+## Пояснительная заметка
+В ходе написания лабораторной была предпринята попытка создать приложение с использованием предложенного в задании сайта - создан профиль, сгенерирован токен. Но, к сожалению, реализовать получение данных так и не получилось. Поэтому был использован сайт, где не требовалась подписка для получения данных о погодных условиях.
From 8ced1ee84c4139cde3b70083c8a643878bf5a624 Mon Sep 17 00:00:00 2001
From: Mary
Date: Mon, 6 Apr 2026 22:53:23 +0400
Subject: [PATCH 4/7] =?UTF-8?q?=D0=B5=D1=89=D0=B5=20=D0=BE=D0=B4=D0=BD?=
=?UTF-8?q?=D0=B0=20=D0=BF=D0=BE=D0=BF=D1=8B=D1=82=D0=BA=D0=B0=20=D1=81?=
=?UTF-8?q?=D0=B4=D0=B5=D0=BB=D0=B0=D1=82=D1=8C=20=D1=87=D0=B5=D1=80=D0=B5?=
=?UTF-8?q?=D0=B7=20=D1=81=D0=B2=D0=BE=D0=B9=20=D0=BF=D1=80=D0=BE=D0=BA?=
=?UTF-8?q?=D1=81=D0=B8?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
docker-compose.yaml | 2 +-
proxy.py | 140 ++++++++++++++++++++++++++++++++++++++------
script.js | 2 +-
test.html | 54 -----------------
4 files changed, 125 insertions(+), 73 deletions(-)
delete mode 100644 test.html
diff --git a/docker-compose.yaml b/docker-compose.yaml
index 9c09c919..c7dead5b 100644
--- a/docker-compose.yaml
+++ b/docker-compose.yaml
@@ -3,6 +3,6 @@ services:
web:
image: nginx:alpine
ports:
- - "8080:80"
+ - "8081:80"
volumes:
- ./:/usr/share/nginx/html:ro
diff --git a/proxy.py b/proxy.py
index 78ccc692..8e6c6d82 100644
--- a/proxy.py
+++ b/proxy.py
@@ -1,42 +1,148 @@
-from flask import Flask, request, jsonify
+"""
+Прокси-сервер для получения прогноза погоды
+Работает на: http://localhost:8080/weather?lat=...&lon=...&token=...
+"""
+
+from flask import Flask, request, jsonify, make_response
from flask_cors import CORS
import requests
from datetime import datetime
+import logging
+
+# Настройка логирования
+logging.basicConfig(
+ level=logging.INFO,
+ format='%(asctime)s - %(levelname)s - %(message)s'
+)
+logger = logging.getLogger(__name__)
+# Создаем приложение Flask
app = Flask(__name__)
-CORS(app)
-@app.route('/weather', methods=['GET'])
+# Включаем CORS для всех маршрутов
+CORS(app, resources={
+ r"/weather": {
+ "origins": "*", # Разрешаем запросы с любого домена
+ "methods": ["GET", "OPTIONS"],
+ "allow_headers": ["Content-Type"]
+ }
+})
+
+# ДЕМО-ДАННЫЕ (на случай, если API недоступен)
+DEMO_WEATHER_DATA = {
+ "city": "Москва",
+ "date": "2026-04-07",
+ "forecast": [
+ {"day": "Пн", "temp": 12, "precipitation": 2, "wind": 4},
+ {"day": "Вт", "temp": 15, "precipitation": 0, "wind": 3},
+ {"day": "Ср", "temp": 18, "precipitation": 5, "wind": 6},
+ {"day": "Чт", "temp": 14, "precipitation": 8, "wind": 7},
+ {"day": "Пт", "temp": 16, "precipitation": 1, "wind": 5}
+ ]
+}
+
+@app.route('/weather', methods=['GET', 'OPTIONS'])
def weather_proxy():
+ """
+ Обработчик запросов к погодному API
+ Поддерживает: GET (основной запрос) и OPTIONS (предварительный CORS-запрос)
+ """
+
+ # Шаг 1: Обработка предварительного запроса (CORS preflight)
+ if request.method == 'OPTIONS':
+ logger.info("Получен OPTIONS-запрос (CORS preflight)")
+ return _build_cors_response(jsonify({"status": "ok"}), 200)
+
+ # Шаг 2: Получаем параметры из запроса
+ logger.info("Получен GET-запрос к /weather")
+
lat = request.args.get('lat')
lon = request.args.get('lon')
token = request.args.get('token')
date = request.args.get('date', default=datetime.now().strftime('%Y-%m-%d'))
+ # Шаг 3: Валидация параметров
if not lat or not lon or not token:
- return jsonify({"error": "Missing parameters: lat, lon, token"}), 400
+ logger.warning(f"Отсутствуют обязательные параметры: lat={lat}, lon={lon}, token={token}")
+ return _build_cors_response(
+ jsonify({
+ "error": "Missing required parameters",
+ "required": ["lat", "lon", "token"],
+ "received": {"lat": lat, "lon": lon, "token": "present" if token else None}
+ }),
+ 400
+ )
+
+ logger.info(f"Запрос погоды: lat={lat}, lon={lon}, date={date}")
- # Исправленный URL (соответствует реальному API)
+ # Шаг 4: Формируем URL для реального API
target_url = f'https://projecteol.ru/ru/api/weather?lat={lat}&lon={lon}&date={date}&token={token}'
+ # Шаг 5: Делаем запрос к реальному API
try:
headers = {
- 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
+ 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'
}
+
+ logger.info(f"Отправляем запрос к: {target_url}")
response = requests.get(target_url, headers=headers, timeout=10)
- return jsonify(response.json()), response.status_code, {
- 'Access-Control-Allow-Origin': '*',
- 'Access-Control-Allow-Methods': 'GET, OPTIONS',
- 'Access-Control-Allow-Headers': 'Content-Type'
- }
+ # Шаг 6: Если API ответил успешно — возвращаем его данные
+ if response.status_code == 200:
+ logger.info(f"Успешный ответ от API: {response.status_code}")
+ return _build_cors_response(jsonify(response.json()), 200)
+
+ # Шаг 7: Если API вернул ошибку — логируем и возвращаем демо-данные
+ logger.warning(f"API вернул ошибку: {response.status_code} - {response.text}")
+ logger.info("Возвращаем демо-данные как fallback")
+ return _build_cors_response(jsonify(DEMO_WEATHER_DATA), 200)
+
+ # Шаг 8: Обработка исключений (сеть недоступна, таймаут и т.д.)
+ except requests.exceptions.RequestException as e:
+ logger.error(f"Ошибка при запросе к API: {str(e)}")
+ logger.info("Возвращаем демо-данные из-за ошибки сети")
+ return _build_cors_response(jsonify(DEMO_WEATHER_DATA), 200)
+ # Шаг 9: Любые другие исключения
except Exception as e:
- return jsonify({
- "error": "Proxy error",
- "message": str(e),
- "url": target_url
- }), 500, {'Access-Control-Allow-Origin': '*'}
+ logger.exception(f"Неожиданная ошибка: {str(e)}")
+ return _build_cors_response(
+ jsonify({
+ "error": "Internal server error",
+ "message": str(e)
+ }),
+ 500
+ )
+
+@app.route('/')
+def home():
+ """Корневая страница — информация о прокси"""
+ return """
+ 🌤️ Прокси-сервер для прогноза погоды
+ Работает на порту 8080
+ Пример запроса:
+ http://localhost:8080/weather?lat=55.75&lon=37.62&token=ВАШ_ТОКЕН
+ Для учебных целей возвращает демо-данные, если реальный API недоступен.
+ """, 200, {'Access-Control-Allow-Origin': '*'}
+
+def _build_cors_response(response, status_code):
+ """
+ Добавляет CORS-заголовки к любому ответу
+ """
+ response.status_code = status_code
+ response.headers['Access-Control-Allow-Origin'] = '*'
+ response.headers['Access-Control-Allow-Methods'] = 'GET, OPTIONS'
+ response.headers['Access-Control-Allow-Headers'] = 'Content-Type'
+ response.headers['Access-Control-Max-Age'] = '3600'
+ return response
if __name__ == '__main__':
- app.run(host='0.0.0.0', port=8080, debug=True) # Важно: 0.0.0.0!
\ No newline at end of file
+ logger.info("=" * 60)
+ logger.info("🚀 Запуск прокси-сервера для прогноза погоды")
+ logger.info("📍 Доступен по адресу: http://localhost:8080/weather")
+ logger.info("💡 Для теста открой в браузере:")
+ logger.info(" http://localhost:8080/weather?lat=55.75&lon=37.62&token=test")
+ logger.info("=" * 60)
+
+ # Запускаем сервер
+ app.run(host='0.0.0.0', port=8080, debug=True)
\ No newline at end of file
diff --git a/script.js b/script.js
index 4ed7a1d0..e288ff99 100644
--- a/script.js
+++ b/script.js
@@ -155,7 +155,7 @@ async function fetchWeather(lat, lon, cityName) {
//const url = `${API_URL}?lat=${lat}&lon=${lon}&token=${API_TOKEN}`;
const today = new Date().toISOString().split('T')[0];
- const url = `http://localhost:8080/weather?lat=${lat}&lon=${lon}&date=${today}&token=${API_TOKEN}`;
+ const url = `https://projecteol.ru/api/weather/?lat=${lat}&lon=${lon}&date=${today}&token=${API_TOKEN}`;
const response = await fetch(url);
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const data = await response.json();
diff --git a/test.html b/test.html
deleted file mode 100644
index 18ffbee5..00000000
--- a/test.html
+++ /dev/null
@@ -1,54 +0,0 @@
-
-
-
-
- Тест карты + Open-Meteo
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
From 9b0293f57fe625605baf8fc8e718d1c290daebf8 Mon Sep 17 00:00:00 2001
From: Mary
Date: Tue, 7 Apr 2026 00:20:43 +0400
Subject: [PATCH 5/7] =?UTF-8?q?=D0=BD=D0=B5=20=D0=B2=D1=8B=D1=88=D0=BB?=
=?UTF-8?q?=D0=BE,=20=D0=BF=D0=BE=D1=8D=D1=82=D0=BE=D0=BC=D1=83=20open=20w?=
=?UTF-8?q?eathe?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
index.html | 42 +-------
nginx.conf | 39 --------
proxy.py | 148 ----------------------------
script.js | 278 +++++++++++++++++------------------------------------
4 files changed, 91 insertions(+), 416 deletions(-)
delete mode 100644 nginx.conf
delete mode 100644 proxy.py
diff --git a/index.html b/index.html
index 10fe24d7..6fe5d307 100644
--- a/index.html
+++ b/index.html
@@ -8,35 +8,31 @@
+
+
-
-
-
-
-
-
🌡️ Температура (°C)
@@ -51,41 +47,11 @@ 💨 Ветер (м/с)
-
- 💡 Нажмите на любой город на карте, чтобы увидеть прогноз на несколько дней
+ 💡 Нажмите на любой город на карте, чтобы увидеть прогноз
-
-
-
-
-