Merge pull request 'dev' (#1) from dev into main

Reviewed-on: http://sergeevrov.fvds.ru:3000/Vic/SatForecast/pulls/1
This commit is contained in:
Vic 2026-06-12 12:51:47 +03:00
commit dfea84f313
218 changed files with 552 additions and 0 deletions

10
.idea/.gitignore generated vendored Normal file
View file

@ -0,0 +1,10 @@
# Default ignored files
/shelf/
/workspace.xml
# Ignored default folder with query files
/queries/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml
# Editor-based HTTP Client requests
/httpRequests/

8
.idea/SatForecast.iml generated Normal file
View file

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="PYTHON_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$" />
<orderEntry type="jdk" jdkName="Python 3.12 (SatForecast)" jdkType="Python SDK" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

View file

@ -0,0 +1,6 @@
<component name="InspectionProjectProfileManager">
<settings>
<option name="USE_PROJECT_PROFILE" value="false" />
<version value="1.0" />
</settings>
</component>

7
.idea/misc.xml generated Normal file
View file

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="Black">
<option name="sdkName" value="Python 3.14" />
</component>
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.12 (SatForecast)" project-jdk-type="Python SDK" />
</project>

8
.idea/modules.xml generated Normal file
View file

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/SatForecast.iml" filepath="$PROJECT_DIR$/.idea/SatForecast.iml" />
</modules>
</component>
</project>

6
.idea/vcs.xml generated Normal file
View file

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" />
</component>
</project>

507
main.py Normal file
View file

@ -0,0 +1,507 @@
import tkinter as tk
from tkinter import ttk, messagebox
from PIL import Image, ImageTk
import requests
from io import BytesIO
import math
import time
from datetime import datetime
import threading
from pathlib import Path
import random
class RateLimiter:
def __init__(self, max_per_second=1):
self.min_interval = 1.0 / max_per_second
self.last_request = 0
def wait(self):
elapsed = time.time() - self.last_request
if elapsed < self.min_interval:
time.sleep(self.min_interval - elapsed)
self.last_request = time.time()
class WeatherMapApp:
def __init__(self, root):
self.root = root
self.root.title("SatForecast")
self.root.geometry("1200x800")
# API ключи
self.owm_api_key = "cc437a23f6a217cd5859bd2d4dc9b6f8"
# Rate limiter
self.rate_limiter = RateLimiter(max_per_second=2)
# Настройки карты
self.lon = 37.6173
self.lat = 55.7558
self.zoom = 8
self.dragging = False
self.drag_start_x = 0
self.drag_start_y = 0
# Кэш тайлов
self.cache_dir = Path("tile_cache")
self.cache_dir.mkdir(exist_ok=True)
# Маркеры
self.markers = [
{"name": "Москва", "lat": 55.7558, "lon": 37.6173},
{"name": "СПб", "lat": 59.9343, "lon": 30.3351},
]
# Настройка интерфейса
self.setup_ui()
# Загрузка данных
self.load_map()
self.load_forecast()
def setup_ui(self):
# Основной контейнер
main_frame = ttk.Frame(self.root)
main_frame.pack(fill='both', expand=True)
# Левая панель управления
control_panel = ttk.Frame(main_frame, width=300)
control_panel.pack(side='left', fill='y', padx=5, pady=5)
control_panel.pack_propagate(False)
# Поиск города
ttk.Label(control_panel, text="Поиск города:").pack(pady=5)
search_frame = ttk.Frame(control_panel)
search_frame.pack(fill='x', padx=5)
self.city_entry = ttk.Entry(search_frame)
self.city_entry.pack(side='left', fill='x', expand=True)
self.city_entry.bind('<Return>', lambda e: self.search_city())
ttk.Button(search_frame, text="🔍", width=3,
command=self.search_city).pack(side='left')
# Список городов
ttk.Label(control_panel, text="Избранные города:").pack(pady=5)
self.city_listbox = tk.Listbox(control_panel, height=8)
self.city_listbox.pack(fill='x', padx=5)
self.city_listbox.bind('<<ListboxSelect>>', self.on_city_select)
for marker in self.markers:
self.city_listbox.insert(tk.END, marker['name'])
# Управление маркерами
marker_frame = ttk.Frame(control_panel)
marker_frame.pack(fill='x', padx=5, pady=5)
ttk.Button(marker_frame, text="Добавить маркер",
command=self.add_current_location).pack(fill='x')
ttk.Button(marker_frame, text="Удалить маркер",
command=self.remove_marker).pack(fill='x')
# Слои карты
ttk.Label(control_panel, text="Слой карты:").pack(pady=5)
layers = [
("Спутник (ESRI)", "esri_sat"),
("Спутник (Google)", "google_sat"),
("OpenStreetMap", "osm"),
("CartoDB", "cartodb"),
("Температура", "temp"),
("Облачность", "clouds"),
("Осадки", "precipitation")
]
self.layer_var = tk.StringVar(value="esri_sat")
for text, value in layers:
ttk.Radiobutton(control_panel, text=text, variable=self.layer_var,
value=value, command=self.load_map).pack(anchor='w', padx=20)
# Прогноз погоды
ttk.Label(control_panel, text="Прогноз погоды:").pack(pady=10)
self.forecast_text = tk.Text(control_panel, height=12, width=35)
self.forecast_text.pack(fill='both', expand=True, padx=5)
# Карта
map_frame = ttk.Frame(main_frame)
map_frame.pack(side='right', fill='both', expand=True)
self.canvas = tk.Canvas(map_frame, bg='#1a1a2e')
self.canvas.pack(fill='both', expand=True)
# Привязка событий
self.canvas.bind('<Button-1>', self.start_drag)
self.canvas.bind('<B1-Motion>', self.drag_map)
self.canvas.bind('<ButtonRelease-1>', self.end_drag)
self.canvas.bind('<MouseWheel>', self.mouse_zoom)
# Кнопки масштаба
zoom_frame = ttk.Frame(map_frame)
zoom_frame.place(relx=1.0, rely=0.5, anchor='e', x=-10)
ttk.Button(zoom_frame, text="+", width=3,
command=lambda: self.change_zoom(1)).pack()
ttk.Button(zoom_frame, text="-", width=3,
command=lambda: self.change_zoom(-1)).pack()
# Статус бар
self.status_label = ttk.Label(self.root, text="Готово")
self.status_label.pack(side='bottom', fill='x')
def get_cache_path(self, layer, x, y, zoom):
cache_subdir = self.cache_dir / layer / str(zoom) / str(x)
cache_subdir.mkdir(parents=True, exist_ok=True)
return cache_subdir / f"{y}.png"
def download_tile_cached(self, url, layer, x, y, zoom):
cache_path = self.get_cache_path(layer, x, y, zoom)
# Проверяем кэш
if cache_path.exists():
max_age = 3 * 3600 if "sat" in layer else 24 * 3600
if datetime.now().timestamp() - cache_path.stat().st_mtime < max_age:
try:
return Image.open(cache_path)
except:
pass
try:
headers = {
'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',
'Accept': 'image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8',
'Accept-Language': 'ru-RU,ru;q=0.9,en-US;q=0.8,en;q=0.7',
'Referer': 'https://www.openstreetmap.org/',
'Sec-Fetch-Dest': 'image',
'Sec-Fetch-Mode': 'no-cors',
'Sec-Fetch-Site': 'cross-site',
}
self.rate_limiter.wait()
response = requests.get(url, headers=headers, timeout=15)
if response.status_code == 200:
content_type = response.headers.get('content-type', '')
if 'image' in content_type or len(response.content) > 1000:
img = Image.open(BytesIO(response.content))
img.save(cache_path)
print(f"{layer}: {x},{y} zoom={zoom}")
return img
else:
print(f"Не изображение: {url[:80]}")
else:
print(f"{response.status_code}: {url[:80]}")
except Exception as e:
print(f"✗ Ошибка: {e}")
# Возвращаем из кэша
if cache_path.exists():
try:
return Image.open(cache_path)
except:
pass
return None
def get_tile_url(self, x, y, zoom):
"""Рабочие бесплатные URL для тайлов"""
layer = self.layer_var.get()
if layer == "esri_sat":
# ESRI спутниковые снимки (бесплатно, без ключа)
return f"https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{zoom}/{y}/{x}"
elif layer == "google_sat":
# Google Maps спутник (бесплатно для некоммерческого использования)
# Используем разные поддомены для распределения нагрузки
subdomain = random.choice(['0', '1', '2', '3'])
return f"https://mt{subdomain}.google.com/vt/lyrs=s&x={x}&y={y}&z={zoom}"
elif layer == "osm":
# OpenStreetMap
subdomain = random.choice(['a', 'b', 'c'])
return f"https://{subdomain}.tile.openstreetmap.org/{zoom}/{x}/{y}.png"
elif layer == "cartodb":
# CartoDB Light (красивый стиль)
return f"https://a.basemaps.cartocdn.com/light_all/{zoom}/{x}/{y}.png"
elif layer == "temp":
return f"https://tile.openweathermap.org/map/temp_new/{zoom}/{x}/{y}.png?appid={self.owm_api_key}"
elif layer == "clouds":
return f"https://tile.openweathermap.org/map/clouds_new/{zoom}/{x}/{y}.png?appid={self.owm_api_key}"
elif layer == "precipitation":
return f"https://tile.openweathermap.org/map/precipitation_new/{zoom}/{x}/{y}.png?appid={self.owm_api_key}"
return None
def lat_lon_to_tile(self, lat, lon, zoom):
lat_rad = math.radians(lat)
n = 2.0 ** zoom
xtile = (lon + 180.0) / 360.0 * n
ytile = (1.0 - math.asinh(math.tan(lat_rad)) / math.pi) / 2.0 * n
return xtile, ytile
def load_map(self):
threading.Thread(target=self._load_map_thread, daemon=True).start()
def _load_map_thread(self):
self.root.after(0, lambda: self.status_label.config(text="Загрузка карты..."))
self.canvas.delete("tile")
center_x, center_y = self.lat_lon_to_tile(self.lat, self.lon, self.zoom)
center_x_int = int(center_x)
center_y_int = int(center_y)
tiles = {}
for dx in range(-1, 2):
for dy in range(-1, 2):
x = center_x_int + dx
y = center_y_int + dy
url = self.get_tile_url(x, y, self.zoom)
if url:
layer = self.layer_var.get()
img = self.download_tile_cached(url, layer, x, y, self.zoom)
if img:
tiles[(dx, dy)] = ImageTk.PhotoImage(img)
self.root.after(0, self._display_tiles, tiles, center_x, center_y)
self.root.after(0, lambda: self.status_label.config(
text=f"Загружено тайлов: {len(tiles)}/9"))
def _display_tiles(self, tiles, center_x, center_y):
canvas_width = self.canvas.winfo_width()
canvas_height = self.canvas.winfo_height()
if canvas_width < 100:
canvas_width = 800
canvas_height = 600
tile_size = 256
for (dx, dy), photo in tiles.items():
x_offset = (center_x - int(center_x)) * tile_size
y_offset = (center_y - int(center_y)) * tile_size
px = canvas_width / 2 + dx * tile_size - x_offset
py = canvas_height / 2 + dy * tile_size - y_offset
self.canvas.create_image(px, py, image=photo, anchor='nw', tags="tile")
self.draw_markers()
self.canvas.tile_images = tiles
def draw_markers(self):
self.canvas.delete("marker")
canvas_width = self.canvas.winfo_width()
canvas_height = self.canvas.winfo_height()
for marker in self.markers:
marker_x, marker_y = self.lat_lon_to_tile(
marker['lat'], marker['lon'], self.zoom)
center_x, center_y = self.lat_lon_to_tile(self.lat, self.lon, self.zoom)
tile_size = 256
px = canvas_width / 2 + (marker_x - center_x) * tile_size
py = canvas_height / 2 + (marker_y - center_y) * tile_size
# Красивый маркер
self.canvas.create_oval(px - 10, py - 10, px + 10, py + 10,
fill='#ff4444', outline='white', width=2, tags="marker")
self.canvas.create_oval(px - 3, py - 3, px + 3, py + 3,
fill='white', tags="marker")
self.canvas.create_text(px, py - 20, text=marker['name'],
fill='white', font=('Arial', 10, 'bold'),
tags="marker")
def start_drag(self, event):
self.dragging = True
self.drag_start_x = event.x
self.drag_start_y = event.y
def drag_map(self, event):
if not self.dragging:
return
dx = event.x - self.drag_start_x
dy = event.y - self.drag_start_y
self.canvas.move("tile", dx, dy)
self.canvas.move("marker", dx, dy)
tile_size = 256
n = 2.0 ** self.zoom
self.lon -= dx / tile_size * 360.0 / n
self.lat += dy / tile_size * 180.0 / n
self.drag_start_x = event.x
self.drag_start_y = event.y
if abs(dx) > tile_size / 2 or abs(dy) > tile_size / 2:
self.load_map()
def end_drag(self, event):
self.dragging = False
self.load_map()
def mouse_zoom(self, event):
if event.delta > 0:
self.change_zoom(1)
else:
self.change_zoom(-1)
def change_zoom(self, delta):
new_zoom = self.zoom + delta
if 2 <= new_zoom <= 18:
self.zoom = new_zoom
self.load_map()
def search_city(self):
city = self.city_entry.get().strip()
if not city:
return
threading.Thread(target=self._search_city_thread, args=(city,), daemon=True).start()
def _search_city_thread(self, city):
try:
url = "https://nominatim.openstreetmap.org/search"
params = {
"q": city,
"format": "json",
"limit": 1
}
headers = {
"User-Agent": "SatForecast/1.0"
}
response = requests.get(url, params=params, headers=headers)
data = response.json()
if data:
lat = float(data[0]['lat'])
lon = float(data[0]['lon'])
self.root.after(0, lambda: self.set_location(lat, lon, city))
else:
self.root.after(0, lambda: messagebox.showinfo("Поиск", "Город не найден"))
except Exception as e:
self.root.after(0, lambda: messagebox.showerror("Ошибка", str(e)))
def set_location(self, lat, lon, name):
self.lat = lat
self.lon = lon
if not any(m['name'] == name for m in self.markers):
self.markers.append({"name": name, "lat": lat, "lon": lon})
self.city_listbox.insert(tk.END, name)
self.load_map()
self.load_forecast()
def on_city_select(self, event):
selection = self.city_listbox.curselection()
if selection:
index = selection[0]
if index < len(self.markers):
marker = self.markers[index]
self.set_location(marker['lat'], marker['lon'], marker['name'])
def add_current_location(self):
name = f"Точка {len(self.markers) + 1}"
self.markers.append({"name": name, "lat": self.lat, "lon": self.lon})
self.city_listbox.insert(tk.END, name)
self.draw_markers()
def remove_marker(self):
selection = self.city_listbox.curselection()
if selection:
index = selection[0]
if index < len(self.markers):
del self.markers[index]
self.city_listbox.delete(index)
self.load_map()
def load_forecast(self):
threading.Thread(target=self._load_forecast_thread, daemon=True).start()
def _load_forecast_thread(self):
try:
# Open-Meteo (бесплатно, без ключа)
url = "https://api.open-meteo.com/v1/forecast"
params = {
"latitude": self.lat,
"longitude": self.lon,
"hourly": "temperature_2m,precipitation,wind_speed_10m,cloud_cover",
"daily": "temperature_2m_max,temperature_2m_min,precipitation_sum",
"timezone": "auto",
"forecast_days": 3
}
response = requests.get(url, params=params)
data = response.json()
forecast_text = f"📍 Прогноз для координат:\n{self.lat:.2f}, {self.lon:.2f}\n\n"
if 'daily' in data:
forecast_text += "📅 По дням:\n"
for i in range(3):
date = data['daily']['time'][i]
temp_max = data['daily']['temperature_2m_max'][i]
temp_min = data['daily']['temperature_2m_min'][i]
precip = data['daily']['precipitation_sum'][i]
weather_emoji = "☀️" if precip == 0 else "🌧️"
if temp_max > 30:
weather_emoji = "🔥"
elif temp_max < 0:
weather_emoji = "❄️"
forecast_text += f"{date}: {weather_emoji} {temp_min}°C...{temp_max}°C, 💧{precip}мм\n"
if 'hourly' in data:
forecast_text += "\n🕐 Ближайшие часы:\n"
current_hour = datetime.now().hour
for i in range(current_hour, min(current_hour + 8, len(data['hourly']['time']))):
time = data['hourly']['time'][i].split('T')[1]
temp = data['hourly']['temperature_2m'][i]
wind = data['hourly']['wind_speed_10m'][i]
clouds = data['hourly']['cloud_cover'][i]
cloud_emoji = "☁️" if clouds > 50 else "🌤️" if clouds > 20 else "☀️"
forecast_text += f"{time}: {cloud_emoji} {temp}°C, 💨{wind}м/с\n"
self.root.after(0, self._update_forecast_display, forecast_text)
except Exception as e:
self.root.after(0, lambda: self.status_label.config(text=f"Ошибка прогноза: {e}"))
def _update_forecast_display(self, text):
self.forecast_text.delete(1.0, tk.END)
self.forecast_text.insert(1.0, text)
def main():
root = tk.Tk()
app = WeatherMapApp(root)
style = ttk.Style()
style.theme_use('clam')
def on_closing():
if messagebox.askokcancel("Выход", "Закрыть приложение?"):
root.destroy()
root.protocol("WM_DELETE_WINDOW", on_closing)
root.mainloop()
if __name__ == "__main__":
main()

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 102 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 93 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 104 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 113 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 110 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 112 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 92 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 101 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 104 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 99 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 762 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 92 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 762 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 762 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 762 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 762 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 762 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 762 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 762 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 762 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 762 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 762 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 762 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 762 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 132 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 120 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 106 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 93 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 143 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 153 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 152 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 138 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 148 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 147 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 150 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 148 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 155 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 158 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 148 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 148 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 162 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 124 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 81 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 80 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 87 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 108 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 119 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 75 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 85 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 98 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 98 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 135 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 146 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 133 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 143 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 93 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 75 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 85 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 116 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 91 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 102 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 143 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 85 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

Some files were not shown because too many files have changed in this diff Show more