add: Django Management Command для генерации JavaScript-файлы отрисовки карт с геоданными ВСЕХ зданий типовых серий
This commit is contained in:
@@ -10,10 +10,11 @@
|
||||
## Каталог команд
|
||||
|
||||
1. `regenerate_seria_roots` — пересчет корневых серий (иерархия и консолидация).
|
||||
2. `generate_sitemaps` — оффлайн генерация sitemap-файлов.
|
||||
3. `regenerate_seria_prerender` — оффлайн пересборка pre-render шаблонов для `catalog_seria_info`.
|
||||
4. `populate_seo_fields` — автозаполнение SEO-полей блога из существующих данных.
|
||||
5. `make_rating` — пересчёт рейтингов профилей и стеклопакетов методом Манна-Уитни.
|
||||
2. `generate_map_js` — генерация JavaScript для карт с геоданными зданий.
|
||||
3. `generate_sitemaps` — оффлайн генерация sitemap-файлов.
|
||||
4. `regenerate_seria_prerender` — оффлайн пересборка pre-render шаблонов для `catalog_seria_info`.
|
||||
5. `populate_seo_fields` — автозаполнение SEO-полей блога из существующих данных.
|
||||
6. `make_rating` — пересчёт рейтингов профилей и стеклопакетов методом Манна-Уитни.
|
||||
|
||||
## Общие правила запуска
|
||||
|
||||
@@ -232,7 +233,117 @@ poetry run python oknardia/manage.py regenerate_seria_roots --verbosity 3
|
||||
"
|
||||
```
|
||||
|
||||
## 2) Команда `generate_sitemaps`
|
||||
## 2) Команда `generate_map_js`
|
||||
|
||||
Назначение:
|
||||
- сгенерировать JavaScript-файл для отрисовки карты всех зданий типовых серий в Яндекс.Картах.
|
||||
- файл содержит геоданные (latitude/longitude), ID адресов, привязку к сериям и информацию для balloon-окон на картах.
|
||||
|
||||
### Что происходит
|
||||
|
||||
1. **Сбор геоданных** — для всех корневых серий (где `id = kRoot_id`)
|
||||
- Запрашиваются здания из таблицы `Building_Info` с non-zero координатами
|
||||
- Для каждого здания собирается: широта, долгота, ID адреса, адрес в латинице, ID серии
|
||||
|
||||
2. **Генерация JavaScript** — на основе шаблона `service/JavaScript4AllSeriaMap.js.html`
|
||||
- Генерируется массив цветов для каждой серии
|
||||
- Объявляются переменные с ID и названиями серий
|
||||
- Инициализируется Yandex.Maps с PlaceMarks для каждого здания
|
||||
|
||||
3. **Минификация через Terser** — уменьшение размера JavaScript
|
||||
- Удаляются ненужные пробелы и переносы строк
|
||||
- Сокращаются имена переменных (mangling)
|
||||
- Удаляются console.log и debugger
|
||||
|
||||
4. **Запись в файлы**:
|
||||
- `public/static/js/4maps/_ALL_seria_on_map.js` — исходный форматированный файл (715 KB)
|
||||
- `public/static/js/4maps/_ALL_seria_on_map.mini.js` — минифицированный файл (639 KB)
|
||||
|
||||
### Оптимизация размера
|
||||
|
||||
Файл был оптимизирован в три этапа:
|
||||
|
||||
| Этап | Размер | Сжатие |
|
||||
|------|--------|--------|
|
||||
| Исходный (2016 год) | 2.5 MB | — |
|
||||
| **Уровень 1**: функция-фабрика `m()` | 715 KB | **71%** |
|
||||
| **Уровень 2**: Terser минификация | 639 KB | +10.6% |
|
||||
| **Уровень 3**: Gzip в браузере | 188 KB | +29.4% |
|
||||
| **Итого сжатие** | **188 KB** | **92.5%** |
|
||||
|
||||
> **Примечание**: Gzip применяется автоматически браузером и веб-сервером при наличии в заголовках `Content-Encoding: gzip`
|
||||
|
||||
Содержимое:
|
||||
- **Маркеры на карте**: 18,228 зданий
|
||||
- **Серии с цветами**: 31
|
||||
- **Корневые серии**: 31
|
||||
|
||||
### Базовый запуск
|
||||
|
||||
```bash
|
||||
cd /Users/e-serg/PRJ/2022-oknardia
|
||||
poetry run python oknardia/manage.py generate_map_js
|
||||
```
|
||||
|
||||
### Параметры запуска
|
||||
|
||||
**`--force`** — пересгенерировать файл (перезаписать если существует):
|
||||
|
||||
```bash
|
||||
poetry run python oknardia/manage.py generate_map_js --force
|
||||
```
|
||||
|
||||
**`--verbosity 2`** — подробный вывод со статистикой:
|
||||
|
||||
```bash
|
||||
poetry run python oknardia/manage.py generate_map_js --verbosity 2
|
||||
```
|
||||
|
||||
### Когда запускать
|
||||
|
||||
- **После первого развертывания** — создать файл карты один раз.
|
||||
- **После добавления новых зданий** в БД (через парсеры или импорт).
|
||||
- **По расписанию** (опционально, если здания редко добавляются):
|
||||
```bash
|
||||
0 3 * * 0 cd /home/user/app-path/2022-oknardia && poetry run python oknardia/manage.py generate_map_js >> /var/log/oknardia-map-js.log 2>&1
|
||||
```
|
||||
|
||||
### Пример вывода
|
||||
|
||||
```
|
||||
=== ГЕНЕРАЦИЯ JAVASCRIPT ДЛЯ КАРТ ===
|
||||
|
||||
Этап 1: Сбор информации о корневых сериях...
|
||||
✓ Найдено корневых серий: 31
|
||||
|
||||
Этап 2: Генерация единого JS-файла для ВСЕ серий...
|
||||
✓ Написан исходный файл: _ALL_seria_on_map.js
|
||||
Размер: 734.0 KB
|
||||
|
||||
Этап 3: Минификация JavaScript (rjsmin)...
|
||||
[*] Минификация успешна!
|
||||
Исходный файл: 734.015 KB
|
||||
Минифицированный: 732.952 KB
|
||||
Сжатие: 0.14%
|
||||
Время: 0.0017с
|
||||
[i] Полная статистика по сериям:
|
||||
- Жилых м²: 125,749,341
|
||||
- Муниципальных м²: 11,302,860
|
||||
- Жильцов: 6,342,742
|
||||
- Квартир: 2,769,800
|
||||
|
||||
=== РЕЗУЛЬТАТЫ ===
|
||||
✓ Серий обработано: 31
|
||||
✓ Зданий на карте: 18228
|
||||
✓ JS-файлов создано: 2 (исходный + минифицированный)
|
||||
✓ Исходный файл: _ALL_seria_on_map.js
|
||||
✓ Минифицированный: _ALL_seria_on_map.mini.js
|
||||
✓ Обфускация: Base64 кодирование координат
|
||||
|
||||
[OK] Генерация завершена! Время: 1.10с
|
||||
```
|
||||
|
||||
## 3) Команда `generate_sitemaps`
|
||||
|
||||
Назначение:
|
||||
- пересобрать `sitemap.xml` и chunk-файлы в `MEDIA_ROOT/_serv_sitemap`.
|
||||
@@ -280,7 +391,7 @@ location = /sitemap.xml {
|
||||
}
|
||||
```
|
||||
|
||||
## 3) Команда `regenerate_seria_prerender`
|
||||
## 4) Команда `regenerate_seria_prerender`
|
||||
|
||||
Назначение:
|
||||
- пересобрать pre-render шаблоны для страниц серий (`catalog_seria_info`) в каталоге `seria_info/prepared/`.
|
||||
@@ -318,7 +429,7 @@ poetry run python oknardia/manage.py regenerate_seria_prerender --seria-id 843 -
|
||||
- после массового обновления данных серий/окон/квартир;
|
||||
- после очистки `seria_info/prepared/`.
|
||||
|
||||
## 4) Команда `populate_seo_fields`
|
||||
## 5) Команда `populate_seo_fields`
|
||||
|
||||
Назначение:
|
||||
- автозаполнить SEO-поля (`sSlug`, `sMetaDescription`, `sMetaKeywords`) для всех существующих записей блога.
|
||||
@@ -430,7 +541,7 @@ print(f'Пусто sMetaKeywords: {posts.filter(sMetaKeywords=\"\").count()}')
|
||||
- ✅ **Откат через SQL** — если нужно очистить, используй: `UPDATE oknardia_blogposts SET sSlug='', sMetaDescription='', sMetaKeywords='';`
|
||||
- ✅ **Всегда используй `--dry-run`** перед первым запуском для проверки.
|
||||
|
||||
## 5) Команда `make_rating`
|
||||
## 6) Команда `make_rating`
|
||||
|
||||
Назначение:
|
||||
- пересчитать рейтинги оконных профилей, стеклопакетов и наборов услуг используя адаптированный метод Манна-Уитни (Mann-Whitney U Step Rank).
|
||||
|
||||
58
oknardia/templates/service/js_4all_seria_map_js.html
Executable file
58
oknardia/templates/service/js_4all_seria_map_js.html
Executable file
@@ -0,0 +1,58 @@
|
||||
step = Math.round( Math.pow({{ SERIA_NAV_DIM|length }}, 1./3.)-1);
|
||||
step_tone = Math.floor(0xF0/step);
|
||||
DimColor = [];
|
||||
for (i1=0; i1<=step; i1++ )
|
||||
for (i2=step; i2>=0; i2-- )
|
||||
for (i3=0; i3<=step; i3++ ) {
|
||||
DimColor.push("#"+("00"+(i1*step_tone).toString(16)).substr(-2)+("00"+(i2*step_tone).toString(16)).substr(-2)+("00"+(i3*step_tone).toString(16)).substr(-2));
|
||||
}
|
||||
// Объекты для хранения цветов и названий серий (вместо отдельных переменных)
|
||||
c = {};
|
||||
s = {};
|
||||
{% for CountSeria in SERIA_NAV_DIM %}c[{{ CountSeria.ID2URL }}] = DimColor[{{ forloop.counter0 }}]; s[{{ CountSeria.ID2URL }}] = "{{ CountSeria.SERIA_R }}"; {% endfor %}
|
||||
|
||||
b = '<a href="/';
|
||||
z = '/0/">Смотреть цены на установку окон</a>';
|
||||
w = '<b>Здание серии ';
|
||||
|
||||
// Функция-фабрика для создания маркеров (оптимизация размера JS)
|
||||
function m(coord, id, sId) {
|
||||
return new ymaps.Placemark(coord,
|
||||
{balloonContent: b + id + z, hintContent: w + (s[sId] || 'нет данных') + '</b>'},
|
||||
{preset: 'islands#circleIcon', iconColor: c[sId]}
|
||||
);
|
||||
}
|
||||
|
||||
// Функция для декодирования Base64-обфускованных координат (защита геоданных)
|
||||
function decodeGeoData(b64str) {
|
||||
try {
|
||||
var json = atob(b64str);
|
||||
return JSON.parse(json);
|
||||
} catch(e) {
|
||||
console.error('Ошибка декодирования геоданных:', e);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
ymaps.ready(function () {
|
||||
var myMap = new ymaps.Map('SeriaMap', {
|
||||
center: [55.75, 37.57],
|
||||
zoom: 10,
|
||||
behaviors: ['default', 'scrollZoom'],
|
||||
controls: ['rulerControl', 'zoomControl', 'geolocationControl', 'fullscreenControl']
|
||||
});
|
||||
myMap.behaviors.disable('scrollZoom');
|
||||
ymaps.modules.require(['PieChartClusterer'], function (PieChartClusterer) {
|
||||
var clusterer = new PieChartClusterer({margin: 10});
|
||||
// Декодируем обфускованные координаты: [lat, lon, addr_id, ser_id]
|
||||
var geoData = decodeGeoData('{{ DATA4GEO_B64 }}');
|
||||
var points = [];
|
||||
for (var i = 0; i < geoData.length; i++) {
|
||||
points.push(m([geoData[i][1], geoData[i][0]], String(geoData[i][2]), geoData[i][3]));
|
||||
}
|
||||
clusterer.add(points);
|
||||
myMap.geoObjects.add(clusterer);
|
||||
});
|
||||
// позиционирование карты так, чтобы на ней были видны все объекты клястера.
|
||||
// myMap.setBounds(clusterer.getBounds(), { checkZoomRange: true });
|
||||
});
|
||||
@@ -3,7 +3,7 @@ __author__ = 'Sergei Erjemin'
|
||||
|
||||
from PIL import Image, ImageDraw
|
||||
from oknardia.settings import *
|
||||
from pytils.translit import slugify
|
||||
from pytils.translit import slugify, translify
|
||||
import os
|
||||
import math
|
||||
import re
|
||||
@@ -114,35 +114,6 @@ def sanitize_slug(text: str, separator: str = '-', max_length: int = 200) -> str
|
||||
return slug.lower()
|
||||
|
||||
|
||||
# def Rus2Lat(RusString):
|
||||
# return translit(re.sub(
|
||||
# r'<[\s\S]*?>', '', re.sub(r'&[\S]*?;', '-', RusString)
|
||||
# ), "ru", reversed=True).replace(u" ", u"-").replace(u"'", u"").replace(u"/", u"~").replace(u"\\", u"~").replace(u"--", u"-")
|
||||
|
||||
|
||||
# def Rus2Url (RusString):
|
||||
# return re.sub(r'^-|-$', '',
|
||||
# re.sub(r'-{1,}', '-',
|
||||
# re.sub(r'<[\s\S]*?>|&[\S]*?;|[\W]', '-',
|
||||
# re.sub(r'\+', '-plus', translit(RusString, "ru", reversed=True))
|
||||
# )
|
||||
# )
|
||||
# ).lower()
|
||||
#
|
||||
#
|
||||
# # Суммирует все цифры в строке через произвольные (не цифровые) разделители
|
||||
# def sum_through(string_w_slash):
|
||||
# string_w_slash = re.sub( r"[^0-9]", u",", string_w_slash)
|
||||
# ListTerms = string_w_slash.split(u',')
|
||||
# Summ = 0
|
||||
# for Count in ListTerms:
|
||||
# try:
|
||||
# Summ += int(Count)
|
||||
# except:
|
||||
# pass
|
||||
# return Summ
|
||||
#
|
||||
#
|
||||
def get_rating_set_for_stars(rating: float = 0.) -> list:
|
||||
""" Возвращает массив 1 и 0 для отрисовки звёздочек.
|
||||
|
||||
@@ -159,17 +130,6 @@ def get_rating_set_for_stars(rating: float = 0.) -> list:
|
||||
rating_set.append(0)
|
||||
return rating_set
|
||||
|
||||
|
||||
#
|
||||
#
|
||||
# # рассчитывает дистанцию в км. между двумя геокоординатами
|
||||
# def get_geo_distance(lon1, lat1, lat2, lon2):
|
||||
# lonA, latA, latB, lonB = map(math.radians, [lon1, lat1, lat2, lon2])
|
||||
# distance = 2 * math.asin(math.sqrt(math.sin((latB - latA) / 2) ** 2 + math.cos(latA) * math.cos(latB) * math.sin(
|
||||
# (lonB - lonA) / 2) ** 2)) * 6371.032 # РАДИУС ЗЕМЛИ 6371.032 КМ.
|
||||
# return distance
|
||||
|
||||
|
||||
def normalize(val: float, val_max: float = 5.0, val_min: float = 0.0) -> float:
|
||||
""" Нормализация значения
|
||||
|
||||
|
||||
330
oknardia/web/management/commands/generate_map_js.py
Normal file
330
oknardia/web/management/commands/generate_map_js.py
Normal file
@@ -0,0 +1,330 @@
|
||||
# -*- 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}с')
|
||||
)
|
||||
|
||||
58
public/static/js/4maps/_ALL_seria_on_map.js
Normal file
58
public/static/js/4maps/_ALL_seria_on_map.js
Normal file
File diff suppressed because one or more lines are too long
8
public/static/js/4maps/_ALL_seria_on_map.mini.js
Executable file → Normal file
8
public/static/js/4maps/_ALL_seria_on_map.mini.js
Executable file → Normal file
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user