331 lines
14 KiB
Python
331 lines
14 KiB
Python
# -*- coding: utf-8 -*-
|
||
"""
|
||
Django management command: generate_map_js
|
||
|
||
Генерирует JavaScript-файлы для отрисовки карт с геоданными зданий типовых серий.
|
||
|
||
Процесс:
|
||
1. Получает все корневые серии (где id = kRoot_id)
|
||
2. Собирает геоданные всех зданий для этих серий
|
||
3. Генерирует JavaScript файл public/static/js/4maps/_ALL_seria_on_map.js
|
||
4. Файл содержит координаты всех зданий, привязанные к сериям
|
||
|
||
Структура генерируемого файла:
|
||
- Массив цветов для каждой серии (DimColor)
|
||
- Объявление переменных серий (c<ID>, s<ID>)
|
||
- Инициализация Yandex.Maps с PlaceMarks всех зданий
|
||
"""
|
||
|
||
from django.core.management.base import BaseCommand
|
||
from django.template.loader import render_to_string
|
||
from oknardia.models import Seria_Info, Building_Info
|
||
from web.add_func import sanitize_slug
|
||
from oknardia.settings import STATIC_BASE_PATH, PATH_FOR_JS_MAP, SUFFIX_FOR_JS_MAP
|
||
import os
|
||
import time
|
||
import base64
|
||
import json
|
||
|
||
try:
|
||
import rjsmin as _rjsmin
|
||
RJSMIN_AVAILABLE = True
|
||
except ImportError:
|
||
RJSMIN_AVAILABLE = False
|
||
_rjsmin = None
|
||
|
||
|
||
def seria_info_geo_code(seria_ids_str):
|
||
"""
|
||
Собирает геоданные для конкретных серий в компактном формате для обфускации.
|
||
|
||
Args:
|
||
seria_ids_str: строка с ID серий через запятую (например "1,8,12,24")
|
||
|
||
Returns:
|
||
dict с ключами:
|
||
- DATA4GEO: список точек с координатами и информацией о зданиях
|
||
- DATA4GEO_B64: Base64-закодированный JSON координат (обфускация)
|
||
- MUNICIPAL_M2, RESIDENTIAL_M2, GOVERNMENT_M2: площади
|
||
- RESIDENTS, APARTMENTS, ACCOUNTS: количества
|
||
- CONDITION_MAX, CONDITION_MIN: условия зданий
|
||
"""
|
||
seria_ids = [int(id_str.strip()) for id_str in seria_ids_str.split(',') if id_str.strip()]
|
||
|
||
data_return = {}
|
||
seria_2_geo = []
|
||
geo_compact = [] # Компактный формат для обфускации: [lat, lon, id, ser_id]
|
||
municipal_m2 = 0
|
||
residential_m2 = 0
|
||
government_m2 = 0
|
||
residents = 0
|
||
apartments = 0
|
||
accounts = 0
|
||
condition_max = 0
|
||
condition_min = 1000000
|
||
|
||
# ORM запрос вместо raw SQL
|
||
query = Building_Info.objects.filter(
|
||
kSeria_Link__kRoot_id__in=seria_ids
|
||
).select_related('kSeria_Link')
|
||
|
||
for building in query:
|
||
# Проверяем наличие координат (не нулевые)
|
||
if building.fGeoCode_Latitude and building.fGeoCode_Longitude:
|
||
if int(building.fGeoCode_Latitude) != 0 and int(building.fGeoCode_Longitude) != 0:
|
||
data_of_point = {
|
||
"LATITUDE": building.fGeoCode_Latitude,
|
||
"LONGITUDE": building.fGeoCode_Longitude,
|
||
"ADDR_ID": building.id,
|
||
"ADDR_LAT": sanitize_slug(building.sAddress),
|
||
"ADDR_RUS": building.sAddress,
|
||
"SER_ID": building.kSeria_Link.kRoot_id if building.kSeria_Link else None
|
||
}
|
||
seria_2_geo.append(data_of_point)
|
||
|
||
# Компактный формат для обфускации: [широта, долгота, ID адреса, ID серии]
|
||
geo_compact.append([
|
||
float(building.fGeoCode_Latitude),
|
||
float(building.fGeoCode_Longitude),
|
||
int(building.id),
|
||
int(building.kSeria_Link.kRoot_id) if building.kSeria_Link else 0
|
||
])
|
||
|
||
# Аккумулируем площади и статистику
|
||
if building.fMunicipal_Area and building.fMunicipal_Area > 0:
|
||
municipal_m2 += building.fMunicipal_Area
|
||
if building.fResidential_Area and building.fResidential_Area > 0:
|
||
residential_m2 += building.fResidential_Area
|
||
if building.fGovernment_Area and building.fGovernment_Area > 0:
|
||
government_m2 += building.fGovernment_Area
|
||
if building.iNum_Residents and building.iNum_Residents > 0:
|
||
residents += building.iNum_Residents
|
||
if building.iNum_Apartments and building.iNum_Apartments > 0:
|
||
apartments += building.iNum_Apartments
|
||
if building.iNum_Accounts and building.iNum_Accounts > 0:
|
||
accounts += building.iNum_Accounts
|
||
if building.fCondition_House and building.fCondition_House > 0:
|
||
if building.fCondition_House > condition_max:
|
||
condition_max = building.fCondition_House
|
||
if building.fCondition_House < condition_min:
|
||
condition_min = building.fCondition_House
|
||
|
||
# Обфускуем координаты через Base64
|
||
geo_json = json.dumps(geo_compact, separators=(',', ':'), ensure_ascii=True)
|
||
geo_b64 = base64.b64encode(geo_json.encode('utf-8')).decode('utf-8')
|
||
|
||
data_return.update({
|
||
"DATA4GEO": seria_2_geo,
|
||
"DATA4GEO_B64": geo_b64,
|
||
"MUNICIPAL_M2": municipal_m2,
|
||
"RESIDENTIAL_M2": residential_m2,
|
||
"GOVERNMENT_M2": government_m2,
|
||
"RESIDENTS": residents,
|
||
"APARTMENTS": apartments,
|
||
"ACCOUNTS": accounts,
|
||
"CONDITION_MAX": condition_max,
|
||
"CONDITION_MIN": condition_min
|
||
})
|
||
|
||
return data_return
|
||
|
||
|
||
def seria_nav(root_series_ids):
|
||
"""
|
||
Возвращает информацию для построения навигации по всем корневым сериям.
|
||
|
||
Args:
|
||
root_series_ids: список ID корневых серий
|
||
|
||
Returns:
|
||
dict с информацией о всех корневых сериях для шаблона
|
||
"""
|
||
# Получаем информацию о всех корневых сериях для навигации
|
||
all_root_series = Seria_Info.objects.filter(
|
||
id__in=root_series_ids
|
||
).order_by('id')
|
||
|
||
seria_nav_dim = []
|
||
for seria in all_root_series:
|
||
seria_nav_dim.append({
|
||
"SERIA_R": seria.sName,
|
||
"ID2URL": seria.id,
|
||
"SERIA_L": sanitize_slug(seria.sName)
|
||
})
|
||
|
||
return {"SERIA_NAV_DIM": seria_nav_dim}
|
||
|
||
|
||
def minify_and_obfuscate_js(input_file_path, output_file_path, verbose=0):
|
||
"""
|
||
Минифицирует JavaScript файл используя rjsmin (чистый Python, без Node.js).
|
||
|
||
Координаты внутри шаблона уже обфускированы через Base64, поэтому основной
|
||
минификатор просто сжимает синтаксис для экономии трафика.
|
||
|
||
Args:
|
||
input_file_path: путь к исходному файлу
|
||
output_file_path: путь к результирующему файлу
|
||
verbose: уровень подробности вывода
|
||
|
||
Returns:
|
||
tuple (успешность, размер_исходного, размер_минифицированного)
|
||
"""
|
||
if not RJSMIN_AVAILABLE:
|
||
if verbose >= 1:
|
||
print('[!!!] rjsmin не установлен. Минификация пропущена.')
|
||
return False, os.path.getsize(input_file_path) / 1024, 0
|
||
|
||
try:
|
||
# Читаем исходный файл
|
||
with open(input_file_path, 'r', encoding='utf-8') as f:
|
||
js_content = f.read()
|
||
|
||
# Минифицируем через rjsmin
|
||
minified_content = _rjsmin.jsmin(js_content)
|
||
|
||
# Пишем результат
|
||
with open(output_file_path, 'w', encoding='utf-8') as f:
|
||
f.write(minified_content)
|
||
|
||
original_size = os.path.getsize(input_file_path) / 1024
|
||
minified_size = os.path.getsize(output_file_path) / 1024
|
||
|
||
return True, original_size, minified_size
|
||
|
||
except Exception as e:
|
||
if verbose >= 1:
|
||
print(f'⚠ Ошибка при минификации: {e}')
|
||
return False, os.path.getsize(input_file_path) / 1024, 0
|
||
|
||
|
||
class Command(BaseCommand):
|
||
help = 'Генерирует JavaScript-файлы для карт с геоданными зданий серий'
|
||
|
||
def add_arguments(self, parser):
|
||
parser.add_argument(
|
||
'--force',
|
||
action='store_true',
|
||
help='Перегенерировать файлы, даже если они существуют'
|
||
)
|
||
|
||
def handle(self, *args, **options):
|
||
verbose = int(options.get('verbosity', 1))
|
||
force = options.get('force', False)
|
||
|
||
self.stdout.write(self.style.SUCCESS('=== ГЕНЕРАЦИЯ JAVASCRIPT ДЛЯ КАРТ ===\n'))
|
||
|
||
time_start = time.perf_counter()
|
||
|
||
# ========== ПОДГОТОВКА ==========
|
||
path_name = f"{STATIC_BASE_PATH}/{PATH_FOR_JS_MAP}"
|
||
|
||
# Проверяем наличие папки
|
||
if not os.path.exists(path_name):
|
||
os.makedirs(path_name)
|
||
if verbose >= 1:
|
||
self.stdout.write(f'✓ Создана папка: {path_name}\n')
|
||
|
||
# ========== ПОЛУЧАЕМ ВСЕ КОРНЕВЫЕ СЕРИИ ==========
|
||
if verbose >= 1:
|
||
self.stdout.write('Этап 1: Сбор информации о корневых сериях...\n')
|
||
|
||
root_series = Seria_Info.objects.filter(
|
||
id__in=Seria_Info.objects.all().values_list('kRoot_id', flat=True).distinct()
|
||
).order_by('id')
|
||
|
||
root_series_ids = [seria.id for seria in root_series]
|
||
|
||
if verbose >= 1:
|
||
self.stdout.write(f'✓ Найдено корневых серий: {len(root_series_ids)}\n')
|
||
|
||
# ========== ГЕНЕРИРУЕМ ЕДИНЫЙ JS ДЛЯ ВСЕХ СЕРИЙ ==========
|
||
if verbose >= 1:
|
||
self.stdout.write('\nЭтап 2: Генерация единого JS-файла для ВСЕ серий...\n')
|
||
|
||
time_start_js = time.perf_counter()
|
||
|
||
# Собираем ID в строку
|
||
seria_ids_string = ','.join(str(id) for id in root_series_ids)
|
||
|
||
# Получаем геоданные для всех серий
|
||
to_template = seria_info_geo_code(seria_ids_string)
|
||
|
||
# Получаем навигацию для всех корневых серий
|
||
for_seria_nav = seria_nav(root_series_ids)
|
||
to_template.update(for_seria_nav)
|
||
|
||
# Рендерим шаблон
|
||
js_content = render_to_string("service/js_4all_seria_map_js.html", to_template)
|
||
|
||
# Пишем исходный файл
|
||
js_file_path = f"{path_name}/_ALL{SUFFIX_FOR_JS_MAP}"
|
||
js_mini_file_path = f"{path_name}/_ALL{SUFFIX_FOR_JS_MAP}".replace(".js", ".mini.js")
|
||
|
||
try:
|
||
# Сохраняем исходный файл
|
||
with open(js_file_path, 'w', encoding='utf-8') as js_file:
|
||
js_file.write(js_content)
|
||
|
||
file_size_kb = os.path.getsize(js_file_path) / 1024
|
||
time_elapsed = time.perf_counter() - time_start_js
|
||
|
||
if verbose >= 1:
|
||
self.stdout.write(
|
||
f'✓ Написан исходный файл: _ALL{SUFFIX_FOR_JS_MAP}\n'
|
||
f' Размер: {file_size_kb:.1f} KB\n'
|
||
)
|
||
|
||
# Минифицируем через rjsmin (чистый Python)
|
||
if verbose >= 1:
|
||
self.stdout.write('\nЭтап 3: Минификация JavaScript (rjsmin)...\n')
|
||
|
||
time_start_minify = time.perf_counter()
|
||
success, orig_size, mini_size = minify_and_obfuscate_js(js_file_path, js_mini_file_path, verbose)
|
||
time_minify_elapsed = time.perf_counter() - time_start_minify
|
||
|
||
if success and mini_size > 0:
|
||
compression_ratio = (1 - mini_size / orig_size) * 100
|
||
if verbose >= 1:
|
||
self.stdout.write(
|
||
f'[*] Минификация успешна!\n'
|
||
f' Исходный файл: {orig_size:.3f} KB\n'
|
||
f' Минифицированный: {mini_size:.3f} KB\n'
|
||
f' Сжатие: {compression_ratio:.2f}%\n'
|
||
f' Время: {time_minify_elapsed:.4f}с\n'
|
||
)
|
||
time_elapsed += time_minify_elapsed
|
||
else:
|
||
if verbose >= 1:
|
||
self.stdout.write(f'[!!!] Минификация не применена. Используется исходный файл.\n')
|
||
|
||
if verbose >= 2:
|
||
self.stdout.write(
|
||
f'[i] Полная статистика по сериям:\n'
|
||
f' - Жилых м²: {to_template.get("RESIDENTIAL_M2", 0):,.0f}\n'
|
||
f' - Муниципальных м²: {to_template.get("MUNICIPAL_M2", 0):,.0f}\n'
|
||
f' - Жильцов: {to_template.get("RESIDENTS", 0):,}\n'
|
||
f' - Квартир: {to_template.get("APARTMENTS", 0):,}\n'
|
||
)
|
||
|
||
except Exception as e:
|
||
self.stdout.write(
|
||
self.style.ERROR(f'✗ ОШИБКА при записи файла: {e}')
|
||
)
|
||
return
|
||
|
||
# ========== РЕЗУЛЬТАТЫ ==========
|
||
time_total = time.perf_counter() - time_start
|
||
|
||
self.stdout.write(self.style.SUCCESS('\n=== РЕЗУЛЬТАТЫ ==='))
|
||
self.stdout.write(f'✓ Серий обработано: {len(root_series_ids)}')
|
||
self.stdout.write(f'✓ Зданий на карте: {len(to_template["DATA4GEO"])}')
|
||
self.stdout.write(f'✓ JS-файлов создано: 2 (исходный + минифицированный)')
|
||
self.stdout.write(f'✓ Исходный файл: _ALL{SUFFIX_FOR_JS_MAP}')
|
||
self.stdout.write(f'✓ Минифицированный: _ALL{SUFFIX_FOR_JS_MAP.replace(".js", ".mini.js")}')
|
||
self.stdout.write(f'✓ Обфускация: Base64 кодирование координат')
|
||
|
||
self.stdout.write(
|
||
self.style.SUCCESS(f'\n[OK] Генерация завершена! Время: {time_total:.2f}с')
|
||
)
|
||
|