Files
2022_oknardia/oknardia/web/management/commands/generate_map_js.py

331 lines
14 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# -*- 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}с')
)