477 lines
18 KiB
Python
477 lines
18 KiB
Python
#!/usr/bin/env python3
|
||
# -*- coding: utf-8 -*-
|
||
"""
|
||
Создание статической HTML карты с использованием Leaflet и OpenStreetMap
|
||
Отображает все найденные учебные заведения Челябинской области
|
||
"""
|
||
|
||
import json
|
||
import os
|
||
from pathlib import Path
|
||
|
||
|
||
def create_static_map(json_file: str = "chelyabinsk_educational_institutions.json"):
|
||
"""Создает статическую HTML карту с данными из JSON"""
|
||
|
||
# Загружаем данные
|
||
try:
|
||
with open(json_file, 'r', encoding='utf-8') as f:
|
||
data = json.load(f)
|
||
except FileNotFoundError:
|
||
print(f"❌ Файл {json_file} не найден!")
|
||
return None
|
||
|
||
institutions = data.get('institutions', [])
|
||
total_count = len(institutions)
|
||
|
||
print(f"Создание карты для {total_count} учебных заведений...")
|
||
|
||
# Определяем центр карты (центр Челябинской области)
|
||
center_lat = 55.1644
|
||
center_lng = 61.4368
|
||
|
||
# Создаем HTML с картой
|
||
html_content = f"""<!DOCTYPE html>
|
||
<html lang="ru">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>Карта учебных заведений Челябинской области</title>
|
||
|
||
<!-- Leaflet CSS -->
|
||
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css"
|
||
integrity="sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY="
|
||
crossorigin=""/>
|
||
|
||
<!-- Leaflet JavaScript -->
|
||
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"
|
||
integrity="sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo="
|
||
crossorigin=""></script>
|
||
|
||
<style>
|
||
body {{
|
||
margin: 0;
|
||
padding: 0;
|
||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||
background-color: #f5f5f5;
|
||
}}
|
||
|
||
.header {{
|
||
background: linear-gradient(135deg, #4CAF50 0%, #2E7D32 100%);
|
||
color: white;
|
||
padding: 20px;
|
||
text-align: center;
|
||
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
||
}}
|
||
|
||
.header h1 {{
|
||
margin: 0;
|
||
font-size: 2.2em;
|
||
font-weight: 300;
|
||
}}
|
||
|
||
.header p {{
|
||
margin: 10px 0 0 0;
|
||
opacity: 0.9;
|
||
font-size: 1.1em;
|
||
}}
|
||
|
||
.controls {{
|
||
background: white;
|
||
padding: 15px;
|
||
box-shadow: 0 2px 5px rgba(0,0,0,0.1);
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 10px;
|
||
justify-content: center;
|
||
align-items: center;
|
||
}}
|
||
|
||
.filter-btn {{
|
||
padding: 8px 16px;
|
||
border: 2px solid #4CAF50;
|
||
background: white;
|
||
color: #4CAF50;
|
||
border-radius: 25px;
|
||
cursor: pointer;
|
||
transition: all 0.3s ease;
|
||
font-size: 14px;
|
||
font-weight: 500;
|
||
}}
|
||
|
||
.filter-btn:hover {{
|
||
background: #4CAF50;
|
||
color: white;
|
||
transform: translateY(-2px);
|
||
box-shadow: 0 4px 8px rgba(76, 175, 80, 0.3);
|
||
}}
|
||
|
||
.filter-btn.active {{
|
||
background: #4CAF50;
|
||
color: white;
|
||
}}
|
||
|
||
.stats {{
|
||
background: #e8f5e8;
|
||
padding: 10px 15px;
|
||
border-radius: 20px;
|
||
color: #2E7D32;
|
||
font-weight: bold;
|
||
font-size: 14px;
|
||
}}
|
||
|
||
#map {{
|
||
width: 100%;
|
||
height: calc(100vh - 200px);
|
||
border: 2px solid #ddd;
|
||
}}
|
||
|
||
.legend {{
|
||
position: fixed;
|
||
bottom: 20px;
|
||
right: 20px;
|
||
background: rgba(255, 255, 255, 0.95);
|
||
padding: 15px;
|
||
border-radius: 10px;
|
||
box-shadow: 0 2px 10px rgba(0,0,0,0.2);
|
||
z-index: 1000;
|
||
max-width: 250px;
|
||
}}
|
||
|
||
.legend h4 {{
|
||
margin: 0 0 10px 0;
|
||
color: #2E7D32;
|
||
}}
|
||
|
||
.legend-item {{
|
||
display: flex;
|
||
align-items: center;
|
||
margin: 5px 0;
|
||
font-size: 13px;
|
||
}}
|
||
|
||
.legend-color {{
|
||
width: 16px;
|
||
height: 16px;
|
||
border-radius: 50%;
|
||
margin-right: 8px;
|
||
border: 2px solid white;
|
||
box-shadow: 0 1px 3px rgba(0,0,0,0.3);
|
||
}}
|
||
|
||
.university {{ background-color: #2196F3; }}
|
||
.institute {{ background-color: #FF9800; }}
|
||
.college {{ background-color: #4CAF50; }}
|
||
.technical {{ background-color: #9C27B0; }}
|
||
.other {{ background-color: #607D8B; }}
|
||
|
||
@media (max-width: 768px) {{
|
||
.header h1 {{
|
||
font-size: 1.8em;
|
||
}}
|
||
|
||
.controls {{
|
||
padding: 10px;
|
||
}}
|
||
|
||
.filter-btn {{
|
||
font-size: 12px;
|
||
padding: 6px 12px;
|
||
}}
|
||
|
||
.legend {{
|
||
bottom: 10px;
|
||
right: 10px;
|
||
max-width: 200px;
|
||
}}
|
||
}}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="header">
|
||
<h1>🎓 Карта учебных заведений Челябинской области</h1>
|
||
<p>Университеты, институты, колледжи, техникумы и училища • {total_count} учреждений</p>
|
||
</div>
|
||
|
||
<div class="controls">
|
||
<button class="filter-btn active" onclick="filterInstitutions('all')">🏛️ Все ({total_count})</button>
|
||
<button class="filter-btn" onclick="filterInstitutions('университет')">🎓 Университеты</button>
|
||
<button class="filter-btn" onclick="filterInstitutions('институт')">🏛️ Институты</button>
|
||
<button class="filter-btn" onclick="filterInstitutions('колледж')">📚 Колледжи</button>
|
||
<button class="filter-btn" onclick="filterInstitutions('техникум')">🔧 Техникумы</button>
|
||
<button class="filter-btn" onclick="filterInstitutions('училище')">⚙️ Училища</button>
|
||
<div class="stats">
|
||
<span id="showing-count">{total_count}</span> из {total_count} учреждений
|
||
</div>
|
||
</div>
|
||
|
||
<div id="map"></div>
|
||
|
||
<div class="legend">
|
||
<h4>Легенда</h4>
|
||
<div class="legend-item">
|
||
<div class="legend-color university"></div>
|
||
<span>Университеты</span>
|
||
</div>
|
||
<div class="legend-item">
|
||
<div class="legend-color institute"></div>
|
||
<span>Институты</span>
|
||
</div>
|
||
<div class="legend-item">
|
||
<div class="legend-color college"></div>
|
||
<span>Колледжи</span>
|
||
</div>
|
||
<div class="legend-item">
|
||
<div class="legend-color technical"></div>
|
||
<span>Техникумы/Училища</span>
|
||
</div>
|
||
<div class="legend-item">
|
||
<div class="legend-color other"></div>
|
||
<span>Другие</span>
|
||
</div>
|
||
</div>
|
||
|
||
<script>
|
||
// Данные учебных заведений
|
||
const institutionsData = {json.dumps(institutions, ensure_ascii=False, indent=2)};
|
||
|
||
// Инициализация карты
|
||
const map = L.map('map').setView([{center_lat}, {center_lng}], 8);
|
||
|
||
// Добавляем слой OpenStreetMap
|
||
L.tileLayer('https://{{s}}.tile.openstreetmap.org/{{z}}/{{x}}/{{y}}.png', {{
|
||
maxZoom: 19,
|
||
attribution: '© <a href="https://openstreetmap.org/copyright">OpenStreetMap</a> contributors'
|
||
}}).addTo(map);
|
||
|
||
// Массив всех маркеров
|
||
let allMarkers = [];
|
||
|
||
// Функция определения цвета маркера по типу учреждения
|
||
function getMarkerColor(institution) {{
|
||
const name = institution.name.toLowerCase();
|
||
|
||
if (name.includes('университет')) return '#2196F3';
|
||
if (name.includes('институт')) return '#FF9800';
|
||
if (name.includes('колледж')) return '#4CAF50';
|
||
if (name.includes('техникум') || name.includes('училище')) return '#9C27B0';
|
||
return '#607D8B';
|
||
}}
|
||
|
||
// Функция создания пользовательского маркера
|
||
function createCustomMarker(color) {{
|
||
return L.divIcon({{
|
||
className: 'custom-marker',
|
||
html: `<div style="
|
||
width: 20px;
|
||
height: 20px;
|
||
background-color: ${{color}};
|
||
border: 3px solid white;
|
||
border-radius: 50%;
|
||
box-shadow: 0 2px 6px rgba(0,0,0,0.3);
|
||
"></div>`,
|
||
iconSize: [20, 20],
|
||
iconAnchor: [10, 10]
|
||
}});
|
||
}}
|
||
|
||
// Добавление маркеров на карту
|
||
function addMarkersToMap() {{
|
||
institutionsData.forEach((institution, index) => {{
|
||
const lat = institution.latitude;
|
||
const lng = institution.longitude;
|
||
|
||
if (lat && lng && lat !== 0 && lng !== 0) {{
|
||
const color = getMarkerColor(institution);
|
||
const customIcon = createCustomMarker(color);
|
||
|
||
const marker = L.marker([lat, lng], {{ icon: customIcon }});
|
||
|
||
// Создаем содержимое всплывающего окна
|
||
let popupContent = `
|
||
<div style="max-width: 300px; font-family: Arial, sans-serif;">
|
||
<h3 style="margin: 0 0 10px 0; color: #2E7D32; font-size: 16px;">
|
||
${{institution.name}}
|
||
</h3>
|
||
`;
|
||
|
||
if (institution.official_name && institution.official_name !== institution.name) {{
|
||
popupContent += `
|
||
<p style="margin: 5px 0; font-size: 13px; color: #666; font-style: italic;">
|
||
${{institution.official_name}}
|
||
</p>
|
||
`;
|
||
}}
|
||
|
||
if (institution.address) {{
|
||
popupContent += `
|
||
<p style="margin: 5px 0; font-size: 14px;">
|
||
<strong>📍 Адрес:</strong> ${{institution.address}}
|
||
</p>
|
||
`;
|
||
}}
|
||
|
||
if (institution.city) {{
|
||
popupContent += `
|
||
<p style="margin: 5px 0; font-size: 14px;">
|
||
<strong>🏙️ Город:</strong> ${{institution.city}}
|
||
</p>
|
||
`;
|
||
}}
|
||
|
||
if (institution.phone) {{
|
||
popupContent += `
|
||
<p style="margin: 5px 0; font-size: 14px;">
|
||
<strong>📞 Телефон:</strong>
|
||
<a href="tel:${{institution.phone}}" style="color: #4CAF50;">
|
||
${{institution.phone}}
|
||
</a>
|
||
</p>
|
||
`;
|
||
}}
|
||
|
||
if (institution.website) {{
|
||
popupContent += `
|
||
<p style="margin: 5px 0; font-size: 14px;">
|
||
<strong>🌐 Сайт:</strong>
|
||
<a href="${{institution.website}}" target="_blank" style="color: #4CAF50;">
|
||
Перейти на сайт
|
||
</a>
|
||
</p>
|
||
`;
|
||
}}
|
||
|
||
if (institution.amenity) {{
|
||
popupContent += `
|
||
<p style="margin: 5px 0; font-size: 13px; color: #666;">
|
||
<strong>Тип:</strong> ${{institution.amenity}}
|
||
</p>
|
||
`;
|
||
}}
|
||
|
||
popupContent += `
|
||
<p style="margin: 10px 0 0 0; font-size: 11px; color: #999;">
|
||
Источник: ${{institution.source}}
|
||
</p>
|
||
</div>
|
||
`;
|
||
|
||
marker.bindPopup(popupContent);
|
||
marker.institutionData = institution;
|
||
marker.addTo(map);
|
||
allMarkers.push(marker);
|
||
}}
|
||
}});
|
||
|
||
console.log(`Добавлено ${{allMarkers.length}} маркеров на карту`);
|
||
}}
|
||
|
||
// Функция фильтрации учреждений
|
||
function filterInstitutions(filterType) {{
|
||
// Обновляем активную кнопку
|
||
document.querySelectorAll('.filter-btn').forEach(btn => btn.classList.remove('active'));
|
||
event.target.classList.add('active');
|
||
|
||
let visibleCount = 0;
|
||
|
||
allMarkers.forEach(marker => {{
|
||
const institution = marker.institutionData;
|
||
let shouldShow = false;
|
||
|
||
if (filterType === 'all') {{
|
||
shouldShow = true;
|
||
}} else {{
|
||
const name = institution.name.toLowerCase();
|
||
shouldShow = name.includes(filterType);
|
||
}}
|
||
|
||
if (shouldShow) {{
|
||
map.addLayer(marker);
|
||
visibleCount++;
|
||
}} else {{
|
||
map.removeLayer(marker);
|
||
}}
|
||
}});
|
||
|
||
// Обновляем счетчик
|
||
document.getElementById('showing-count').textContent = visibleCount;
|
||
|
||
// Подгоняем карту под видимые маркеры
|
||
if (visibleCount > 0) {{
|
||
const visibleMarkers = allMarkers.filter(marker => map.hasLayer(marker));
|
||
if (visibleMarkers.length > 1) {{
|
||
const group = new L.featureGroup(visibleMarkers);
|
||
map.fitBounds(group.getBounds(), {{padding: [20, 20]}});
|
||
}}
|
||
}}
|
||
}}
|
||
|
||
// Инициализация после загрузки страницы
|
||
document.addEventListener('DOMContentLoaded', function() {{
|
||
console.log('Загрузка карты с данными об учебных заведениях...');
|
||
addMarkersToMap();
|
||
|
||
// Подгоняем карту под все маркеры
|
||
if (allMarkers.length > 0) {{
|
||
setTimeout(() => {{
|
||
const group = new L.featureGroup(allMarkers);
|
||
map.fitBounds(group.getBounds(), {{padding: [50, 50]}});
|
||
}}, 1000);
|
||
}}
|
||
}});
|
||
</script>
|
||
</body>
|
||
</html>"""
|
||
|
||
# Сохраняем HTML файл
|
||
output_file = "chelyabinsk_educational_map_static.html"
|
||
try:
|
||
with open(output_file, 'w', encoding='utf-8') as f:
|
||
f.write(html_content)
|
||
|
||
print(f"✅ Статическая карта создана: {output_file}")
|
||
return output_file
|
||
|
||
except Exception as e:
|
||
print(f"❌ Ошибка создания карты: {e}")
|
||
return None
|
||
|
||
|
||
def main():
|
||
"""Основная функция"""
|
||
print("=" * 80)
|
||
print("СОЗДАНИЕ СТАТИЧЕСКОЙ КАРТЫ УЧЕБНЫХ ЗАВЕДЕНИЙ")
|
||
print("=" * 80)
|
||
|
||
# Проверяем наличие файла с данными
|
||
json_file = "chelyabinsk_educational_institutions.json"
|
||
|
||
if not os.path.exists(json_file):
|
||
print(f"❌ Файл с данными {json_file} не найден!")
|
||
print("Сначала запустите collect_educational_data.py для сбора данных")
|
||
return
|
||
|
||
# Создаем карту
|
||
output_file = create_static_map(json_file)
|
||
|
||
if output_file:
|
||
print(f"\n🎯 Карта готова!")
|
||
print(f"📁 Файл: {os.path.abspath(output_file)}")
|
||
print(f"🌐 Откройте файл в браузере для просмотра")
|
||
|
||
# Предлагаем открыть в браузере
|
||
open_browser = input("\nОткрыть карту в браузере? (y/N): ").strip().lower()
|
||
if open_browser in ['y', 'yes', 'да', 'д']:
|
||
import webbrowser
|
||
try:
|
||
webbrowser.open(f'file://{os.path.abspath(output_file)}')
|
||
print("🚀 Карта открыта в браузере!")
|
||
except Exception as e:
|
||
print(f"❌ Не удалось открыть браузер: {e}")
|
||
|
||
else:
|
||
print("❌ Не удалось создать карту")
|
||
|
||
|
||
if __name__ == "__main__":
|
||
main() |