diff --git a/dbapp/Dockerfile b/dbapp/Dockerfile index 8b95d3f..a48659e 100644 --- a/dbapp/Dockerfile +++ b/dbapp/Dockerfile @@ -18,34 +18,26 @@ RUN apt-get update && apt-get install -y \ postgresql-client \ build-essential \ libpq-dev \ - gcc \ - g++ \ && rm -rf /var/lib/apt/lists/* -# Install Python dependencies for GDAL -RUN pip install --upgrade pip && \ - pip install --no-cache-dir GDAL==$(gdal-config --version) - # Set work directory WORKDIR /app -# Copy project requirements +# Copy project files COPY pyproject.toml uv.lock ./ -# Install uv package manager -RUN pip install --upgrade pip && pip install uv +# Install uv and dependencies +RUN pip install --no-cache-dir uv && \ + uv sync --frozen --no-dev -# Install dependencies using uv -RUN uv pip install --system --no-cache-dir -r uv.lock - -# Copy project +# Copy project code (после установки зависимостей для лучшего кэширования) COPY . . # Collect static files -RUN python manage.py collectstatic --noinput +RUN uv run manage.py collectstatic --noinput # Expose port EXPOSE 8000 # Run gunicorn server -CMD ["gunicorn", "--bind", "0.0.0.0:8000", "--workers", "3", "dbapp.wsgi:application"] \ No newline at end of file +CMD [".venv/bin/gunicorn", "--bind", "0.0.0.0:8000", "--workers", "3", "dbapp.wsgi:application"] \ No newline at end of file diff --git a/dbapp/dbapp/settings/base.py b/dbapp/dbapp/settings/base.py index 5a51365..cb21ca1 100644 --- a/dbapp/dbapp/settings/base.py +++ b/dbapp/dbapp/settings/base.py @@ -72,6 +72,7 @@ INSTALLED_APPS = [ MIDDLEWARE = [ "debug_toolbar.middleware.DebugToolbarMiddleware", #Добавил + 'django.middleware.locale.LocaleMiddleware', #Добавил 'django.middleware.security.SecurityMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', 'django.contrib.messages.middleware.MessageMiddleware', #Добавил diff --git a/dbapp/gdal-3.10.2-cp313-cp313-win_amd64.whl b/dbapp/gdal-3.10.2-cp313-cp313-win_amd64.whl new file mode 100644 index 0000000..07335c9 Binary files /dev/null and b/dbapp/gdal-3.10.2-cp313-cp313-win_amd64.whl differ diff --git a/dbapp/mainapp/admin.py b/dbapp/mainapp/admin.py index 496406d..3ffcb16 100644 --- a/dbapp/mainapp/admin.py +++ b/dbapp/mainapp/admin.py @@ -6,6 +6,7 @@ from .models import ( Standard, SigmaParMark, SigmaParameter, + SourceType, Parameter, Satellite, Mirror, @@ -128,6 +129,12 @@ class ModulationAdmin(admin.ModelAdmin): search_fields = ("name",) ordering = ("name",) +@admin.register(SourceType) +class ModulationAdmin(admin.ModelAdmin): + list_display = ("name",) + search_fields = ("name",) + ordering = ("name",) + @admin.register(Standard) class StandardAdmin(admin.ModelAdmin): @@ -209,23 +216,25 @@ class ParameterAdmin(ImportExportActionModelAdmin, admin.ModelAdmin): class SigmaParameterAdmin(ImportExportActionModelAdmin, admin.ModelAdmin): list_display = ( "id_satellite", - "status", + # "status", "frequency", + "transfer_frequency", "freq_range", - "power", + # "power", + "polarization", "modulation", "bod_velocity", "snr", - "standard", + # "standard", "parameter", - "packets", + # "packets", "datetime_begin", "datetime_end", ) readonly_fields = ( "datetime_begin", - "datetime_end", - + "datetime_end", + "transfer_frequency" ) list_display_links = ("id_satellite",) list_filter = ( @@ -401,7 +410,7 @@ class ObjectAdmin(admin.ModelAdmin): ) search_fields = ( "name", - # "id_geo", + "id_geo__coords", # "id_satellite__name", # "id_vch_load__frequency", ) @@ -413,6 +422,7 @@ class ObjectAdmin(admin.ModelAdmin): "id_vch_load__modulation", "id_vch_load__id_satellite", "id_geo", + "id_source_type" ) autocomplete_fields = ("id_geo",) raw_id_fields = ("id_vch_load",) @@ -422,11 +432,13 @@ class ObjectAdmin(admin.ModelAdmin): def sat_name(self, obj): return obj.id_vch_load.id_satellite sat_name.short_description = "Спутник" + sat_name.admin_order_field = "id_vch_load__id_satellite__name" def freq(self, obj): par = obj.id_vch_load return par.frequency freq.short_description = "Частота, МГц" + freq.admin_order_field = "id_vch_load__frequency" def distance_geo_kup(self, obj): par = obj.id_geo.distance_coords_kup @@ -458,6 +470,7 @@ class ObjectAdmin(admin.ModelAdmin): par = obj.id_vch_load return par.freq_range freq_range.short_description = "Полоса, МГц" + freq_range.admin_order_field = "id_vch_load__freq_range" def bod_velocity(self, obj): par = obj.id_vch_load @@ -482,6 +495,7 @@ class ObjectAdmin(admin.ModelAdmin): lat = f"{latitude}N" if latitude > 0 else f"{abs(latitude)}S" return f"{lat} {lon}" geo_coords.short_description = "Координаты геолокации" + geo_coords.admin_order_filed = "id_geo__coords" def kupsat_coords(self, obj): obj = obj.id_geo diff --git a/dbapp/mainapp/forms.py b/dbapp/mainapp/forms.py index fb89ea2..edc4943 100644 --- a/dbapp/mainapp/forms.py +++ b/dbapp/mainapp/forms.py @@ -1,5 +1,14 @@ from django import forms -from .models import Satellite +from .models import Satellite, Polarization + +class UploadFileForm(forms.Form): + file = forms.FileField( + label="Выберите файл", + widget=forms.FileInput(attrs={ + 'class': 'form-file-input' + }) + ) + class LoadExcelData(forms.Form): file = forms.FileField( @@ -33,7 +42,7 @@ class LoadCsvData(forms.Form): }) ) -class UploadFileForm(forms.Form): +class UploadVchLoad(UploadFileForm): sat_choice = forms.ModelChoiceField( queryset=Satellite.objects.all(), label="Выберите спутник", @@ -41,12 +50,7 @@ class UploadFileForm(forms.Form): 'class': 'form-select' }) ) - file = forms.FileField( - label="Выберите текстовый файл", - widget=forms.FileInput(attrs={ - 'class': 'form-file-input' - }) - ) + class VchLinkForm(forms.Form): sat_choice = forms.ModelChoiceField( @@ -75,4 +79,28 @@ class VchLinkForm(forms.Form): 'class': 'form-control', 'placeholder': 'Введите второе число' }) + ) + + +class NewEventForm(forms.Form): + # sat_choice = forms.ModelChoiceField( + # queryset=Satellite.objects.all(), + # label="Выберите спутник", + # widget=forms.Select(attrs={ + # 'class': 'form-select' + # }) + # ) + # pol_choice = forms.ModelChoiceField( + # queryset=Polarization.objects.all(), + # label="Выберите поляризацию", + # widget=forms.Select(attrs={ + # 'class': 'form-select' + # }) + # ) + file = forms.FileField( + label="Выберите файл", + widget=forms.FileInput(attrs={ + 'class': 'form-control', + 'accept': '.xlsx,.xls' + }) ) \ No newline at end of file diff --git a/dbapp/mainapp/migrations/0018_sigmaparameter_polarization_sigmaparameter_transfer_and_more.py b/dbapp/mainapp/migrations/0018_sigmaparameter_polarization_sigmaparameter_transfer_and_more.py new file mode 100644 index 0000000..b3059f3 --- /dev/null +++ b/dbapp/mainapp/migrations/0018_sigmaparameter_polarization_sigmaparameter_transfer_and_more.py @@ -0,0 +1,31 @@ +# Generated by Django 5.2.7 on 2025-10-27 13:10 + +import django.db.models.deletion +import django.db.models.expressions +import mainapp.models +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('mainapp', '0017_alter_sigmaparameter_parameter'), + ] + + operations = [ + migrations.AddField( + model_name='sigmaparameter', + name='polarization', + field=models.ForeignKey(blank=True, default=mainapp.models.get_default_polarization, null=True, on_delete=django.db.models.deletion.SET_DEFAULT, related_name='polarizations_sigma', to='mainapp.polarization', verbose_name='Поляризация'), + ), + migrations.AddField( + model_name='sigmaparameter', + name='transfer', + field=models.FloatField(choices=[(-1.0, '-'), (9750.0, '9750 МГц'), (10750.0, '10750 МГц')], default=-1.0, verbose_name='Перенос по частоте'), + ), + migrations.AddField( + model_name='sigmaparameter', + name='transfer_frequency', + field=models.GeneratedField(db_persist=True, expression=models.ExpressionWrapper(django.db.models.expressions.CombinedExpression(models.F('frequency'), '+', models.F('transfer')), output_field=models.FloatField()), null=True, output_field=models.FloatField(), verbose_name='Частота в Ku, МГц'), + ), + ] diff --git a/dbapp/mainapp/migrations/0019_alter_satellite_name.py b/dbapp/mainapp/migrations/0019_alter_satellite_name.py new file mode 100644 index 0000000..eeec1fb --- /dev/null +++ b/dbapp/mainapp/migrations/0019_alter_satellite_name.py @@ -0,0 +1,18 @@ +# Generated by Django 5.2.7 on 2025-10-28 05:41 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('mainapp', '0018_sigmaparameter_polarization_sigmaparameter_transfer_and_more'), + ] + + operations = [ + migrations.AlterField( + model_name='satellite', + name='name', + field=models.CharField(db_index=True, max_length=100, unique=True, verbose_name='Имя спутника'), + ), + ] diff --git a/dbapp/mainapp/migrations/0020_sourcetype_objitem_id_source_type.py b/dbapp/mainapp/migrations/0020_sourcetype_objitem_id_source_type.py new file mode 100644 index 0000000..4359cb1 --- /dev/null +++ b/dbapp/mainapp/migrations/0020_sourcetype_objitem_id_source_type.py @@ -0,0 +1,30 @@ +# Generated by Django 5.2.7 on 2025-10-29 14:00 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('mainapp', '0019_alter_satellite_name'), + ] + + operations = [ + migrations.CreateModel( + name='SourceType', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=50, unique=True, verbose_name='Тип источника')), + ], + options={ + 'verbose_name': 'Тип источника', + 'verbose_name_plural': 'Типы источников', + }, + ), + migrations.AddField( + model_name='objitem', + name='id_source_type', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='objitems', to='mainapp.sourcetype', verbose_name='Тип источника'), + ), + ] diff --git a/dbapp/mainapp/models.py b/dbapp/mainapp/models.py index 7ed0266..cd2fce8 100644 --- a/dbapp/mainapp/models.py +++ b/dbapp/mainapp/models.py @@ -2,6 +2,7 @@ from django.db import models from django.contrib.auth.models import User from django.contrib.gis.db import models as gis from django.contrib.gis.db.models import functions +from django.db.models import F, ExpressionWrapper def get_default_polarization(): obj, created = Polarization.objects.get_or_create( @@ -96,7 +97,7 @@ class Standard(models.Model): class Satellite(models.Model): - name = models.CharField(max_length=30, unique=True, verbose_name="Имя спутника", db_index=True) + name = models.CharField(max_length=100, unique=True, verbose_name="Имя спутника", db_index=True) norad = models.IntegerField(blank=True, null=True, verbose_name="NORAD ID") def __str__(self): @@ -107,6 +108,40 @@ class Satellite(models.Model): verbose_name_plural = "Спутники" +class ObjItem(models.Model): + name = models.CharField(null=True, blank=True, max_length=100, verbose_name="Имя объекта", db_index=True) + # id_satellite = models.ForeignKey(Satellite, on_delete=models.PROTECT, related_name="objitems", verbose_name="Спутник") + # id_vch_load = models.ForeignKey(Parameter, on_delete=models.CASCADE, related_name="objitems", verbose_name="ВЧ загрузка") + # id_geo = models.ForeignKey(Geo, on_delete=models.CASCADE, related_name="objitems", verbose_name="Геоданные") + id_user_add = models.ForeignKey(CustomUser, on_delete=models.SET_NULL, related_name="objitems", verbose_name="Пользователь", null=True, blank=True) + # id_source_type = models.ForeignKey(SourceType, on_delete=models.SET_NULL, related_name="objitems", verbose_name='Тип источника', null=True, blank=True) + + + def __str__(self): + return f"Объект {self.name}" + + class Meta: + verbose_name = "Объект" + verbose_name_plural = "Объекты" + # constraints = [ + # models.UniqueConstraint( + # fields=['id_vch_load', 'id_geo'], + # name='unique_objitem_combination' + # ) + # ] + +class SourceType(models.Model): + name = models.CharField(max_length=50, unique=True, verbose_name="Тип источника") + objitem = models.OneToOneField(ObjItem, on_delete=models.SET_NULL, verbose_name="Гео", related_name="objitems", null=True) + + def __str__(self): + return self.name + + class Meta: + verbose_name = "Тип источника" + verbose_name_plural = 'Типы источников' + + class Parameter(models.Model): id_satellite = models.ForeignKey(Satellite, on_delete=models.PROTECT, related_name="parameters", verbose_name="Спутник", null=True) polarization = models.ForeignKey( @@ -123,6 +158,7 @@ class Parameter(models.Model): Standard, default=get_default_standard, on_delete=models.SET_DEFAULT, related_name="standards", null=True, blank=True, verbose_name="Стандарт" ) id_user_add = models.ForeignKey(CustomUser, on_delete=models.SET_NULL, related_name="parameter_added", verbose_name="Пользователь", null=True, blank=True) + objitem = models.ForeignKey(ObjItem, on_delete=models.SET_NULL, related_name="objitems", verbose_name="Источник",null=True, blank=True) # id_sigma_parameter = models.ManyToManyField(SigmaParameter, on_delete=models.SET_NULL, related_name="sigma_parameter", verbose_name="ВЧ с sigma", null=True, blank=True) # id_sigma_parameter = models.ManyToManyField(SigmaParameter, verbose_name="ВЧ с sigma", null=True, blank=True) @@ -151,12 +187,35 @@ class Parameter(models.Model): class SigmaParameter(models.Model): + TRANSFERS = [ + (-1.0, "-"), + (9750.0, "9750 МГц"), + (10750.0, "10750 МГц") + ] + id_satellite = models.ForeignKey(Satellite, on_delete=models.PROTECT, related_name="sigmapar_sat", verbose_name="Спутник") + transfer = models.FloatField( + choices=TRANSFERS, + default=-1.0, + verbose_name="Перенос по частоте" + ) status = models.CharField(max_length=20, blank=True, null=True, verbose_name="Статус") frequency = models.FloatField(default=0, null=True, blank=True, verbose_name="Частота, МГц", db_index=True) + transfer_frequency = models.GeneratedField( + expression=ExpressionWrapper( + F('frequency') + F('transfer'), + output_field=models.FloatField() + ), + output_field=models.FloatField(), + db_persist=True, + null=True, blank=True, verbose_name="Частота в Ku, МГц" + ) freq_range = models.FloatField(default=0, null=True, blank=True, verbose_name="Полоса частот, МГц") power = models.FloatField(default=0, null=True, blank=True, verbose_name="Мощность, дБм") bod_velocity = models.FloatField(default=0, null=True, blank=True, verbose_name="Символьная скорость, БОД") + polarization = models.ForeignKey( + Polarization, default=get_default_polarization, on_delete=models.SET_DEFAULT, related_name="polarizations_sigma", null=True, blank=True, verbose_name="Поляризация" + ) modulation = models.ForeignKey( Modulation, default=get_default_modulation, on_delete=models.SET_DEFAULT, related_name="modulations_sigma", null=True, blank=True, verbose_name="Модуляция" ) @@ -213,6 +272,7 @@ class Geo(models.Model): db_persist=True, null=True, blank=True, verbose_name="Расстояние между купсатом и оперативным отделом, км" ) + objitem = models.OneToOneField(ObjItem, on_delete=models.SET_NULL, verbose_name="Гео", related_name="objitems", null=True) def __str__(self): longitude = self.coords.coords[0] @@ -234,23 +294,3 @@ class Geo(models.Model): ) ] - -class ObjItem(models.Model): - name = models.CharField(null=True, blank=True, max_length=100, verbose_name="Имя объекта", db_index=True) - # id_satellite = models.ForeignKey(Satellite, on_delete=models.PROTECT, related_name="objitems", verbose_name="Спутник") - id_vch_load = models.ForeignKey(Parameter, on_delete=models.CASCADE, related_name="objitems", verbose_name="ВЧ загрузка") - id_geo = models.ForeignKey(Geo, on_delete=models.CASCADE, related_name="objitems", verbose_name="Геоданные") - id_user_add = models.ForeignKey(CustomUser, on_delete=models.SET_NULL, related_name="objitems", verbose_name="Пользователь", null=True, blank=True) - - def __str__(self): - return f"Объект {self.name}" - - class Meta: - verbose_name = "Объект" - verbose_name_plural = "Объекты" - constraints = [ - models.UniqueConstraint( - fields=['id_vch_load', 'id_geo'], - name='unique_objitem_combination' - ) - ] \ No newline at end of file diff --git a/dbapp/mainapp/templates/mainapp/home.html b/dbapp/mainapp/templates/mainapp/home.html index 7def0d8..c38adf4 100644 --- a/dbapp/mainapp/templates/mainapp/home.html +++ b/dbapp/mainapp/templates/mainapp/home.html @@ -171,6 +171,26 @@ + + +
+
+
+
+
+ + + +
+

Формирование таблицы для Кубсатов

+
+

Добавьте новое событие с помощью выбора спутника и загрузки файла данных.

+ + Добавить событие + +
+
+
{% endblock %} \ No newline at end of file diff --git a/dbapp/mainapp/templates/mainapp/link_vch.html b/dbapp/mainapp/templates/mainapp/link_vch.html index 27b5925..65de198 100644 --- a/dbapp/mainapp/templates/mainapp/link_vch.html +++ b/dbapp/mainapp/templates/mainapp/link_vch.html @@ -31,13 +31,13 @@
{{ form.sat_choice.errors }}
{% endif %} -
+ {% comment %}
{{ form.ku_range }} {% if form.ku_range.errors %}
{{ form.ku_range.errors }}
{% endif %} -
+
{% endcomment %}
{{ form.value1 }} diff --git a/dbapp/mainapp/templates/mainapp/process_kubsat.html b/dbapp/mainapp/templates/mainapp/process_kubsat.html new file mode 100644 index 0000000..1fef615 --- /dev/null +++ b/dbapp/mainapp/templates/mainapp/process_kubsat.html @@ -0,0 +1,52 @@ +{% extends 'mainapp/base.html' %} + +{% block title %}Новое событие{% endblock %} + +{% block content %} +
+
+
+
+
+

Формирование таблицы Кубсат

+
+
+
+ {% csrf_token %} + {% comment%} +
+ + {{ form.sat_choice }} + {% if form.sat_choice.errors %} +
{{ form.sat_choice.errors }}
+ {% endif %} +
{% endcomment %} + + {% comment %}
+ + {{ form.pol_choice }} + {% if form.pol_choice.errors %} +
{{ form.pol_choice.errors }}
+ {% endif %} +
{% endcomment %} + +
+ + {{ form.file }} + {% if form.file.errors %} +
{{ form.file.errors }}
+ {% endif %} +
Выберите файл для загрузки
+
+ +
+ Назад + +
+
+
+
+
+
+
+{% endblock %} \ No newline at end of file diff --git a/dbapp/mainapp/templates/mainapp/transponders_upload.html b/dbapp/mainapp/templates/mainapp/transponders_upload.html new file mode 100644 index 0000000..702b018 --- /dev/null +++ b/dbapp/mainapp/templates/mainapp/transponders_upload.html @@ -0,0 +1,53 @@ +{% extends 'mainapp/base.html' %} + +{% block title %}Загрузка данных транспондеров{% endblock %} + +{% block content %} +
+
+
+
+
+

Загрузка данных транспондеров из CellNet

+
+
+ {% if messages %} + {% for message in messages %} + + {% endfor %} + {% endif %} + +

Загрузите xml-файл и выберите спутник для загрузки данных в базу.

+ +
+ {% csrf_token %} +
+ + {{ form.file }} + {% if form.file.errors %} +
{{ form.file.errors }}
+ {% endif %} +
Загрузите xml-файл (.xml) с данными для обработки
+
+ + {% comment %}
+ + {{ form.sat_choice }} + {% if form.sat_choice.errors %} +
{{ form.sat_choice.errors }}
+ {% endif %} +
{% endcomment %} +
+ Назад + +
+
+
+
+
+
+
+{% endblock %} \ No newline at end of file diff --git a/dbapp/mainapp/urls.py b/dbapp/mainapp/urls.py index 8b62f86..51d9cec 100644 --- a/dbapp/mainapp/urls.py +++ b/dbapp/mainapp/urls.py @@ -15,6 +15,7 @@ urlpatterns = [ path('cluster/', views.ClusterTestView.as_view(), name='cluster'), path('vch-upload/', views.UploadVchLoadView.as_view(), name='vch_load'), path('vch-link/', views.LinkVchSigmaView.as_view(), name='link_vch_sigma'), + path('kubsat-excel/', views.ProcessKubsatView.as_view(), name='kubsat_excel'), # path('upload/', views.upload_file, name='upload_file'), ] \ No newline at end of file diff --git a/dbapp/mainapp/utils.py b/dbapp/mainapp/utils.py index bbcf831..1a12891 100644 --- a/dbapp/mainapp/utils.py +++ b/dbapp/mainapp/utils.py @@ -10,12 +10,18 @@ from .models import ( ObjItem, CustomUser ) +from mapsapp.models import Transponders from datetime import datetime, time import pandas as pd import numpy as np from django.contrib.gis.geos import Point import json import re +import io +from django.db.models import F, Count, Exists, OuterRef, Min, Max +from geopy.geocoders import Nominatim +import reverse_geocoder as rg +import time def get_all_constants(): sats = [sat.name for sat in Satellite.objects.all()] @@ -74,7 +80,10 @@ def fill_data_from_df(df: pd.DataFrame, sat: Satellite): freq = remove_str(stroka[1]['Частота, МГц']) freq_line = remove_str(stroka[1]['Полоса, МГц']) v = remove_str(stroka[1]['Символьная скорость, БОД']) - mod_obj, _ = Modulation.objects.get_or_create(name=stroka[1]['Модуляция'].strip()) + try: + mod_obj, _ = Modulation.objects.get_or_create(name=stroka[1]['Модуляция'].strip()) + except AttributeError: + mod_obj, _ = Modulation.objects.get_or_create(name='-') snr = remove_str(stroka[1]['ОСШ']) date = stroka[1]['Дата'].date() time_ = stroka[1]['Время'] @@ -192,17 +201,7 @@ def get_point_from_json(filepath: str): def get_points_from_csv(file_content): - import io - if hasattr(file_content, 'read'): - content = file_content.read() - if isinstance(content, bytes): - content = content.decode('utf-8') - else: - if isinstance(file_content, bytes): - content = content.decode('utf-8') - else: - content = file_content - df = pd.read_csv(io.StringIO(content), sep=";", + df = pd.read_csv(io.StringIO(file_content), sep=";", names=['id', 'obj', 'lat', 'lon', 'h', 'time', 'sat', 'norad_id', 'freq', 'f_range', 'et', 'qaul', 'mir_1', 'mir_2', 'mir_3']) df[['lat', 'lon', 'freq', 'f_range']] = df[['lat', 'lon', 'freq', 'f_range']].replace(',', '.', regex=True).astype(float) df['time'] = pd.to_datetime(df['time'], format='%d.%m.%Y %H:%M:%S') @@ -266,73 +265,22 @@ def get_points_from_csv(file_content): } ) obj_item_obj.save() - # df = pd.read_csv(filepath, sep=";", - # names=['id', 'obj', 'lat', 'lon', 'h', 'time', 'sat', 'norad_id', 'freq', 'f_range', 'et', 'qaul', 'mir_1', 'mir_2', 'mir_3']) - # df[['lat', 'lon', 'freq', 'f_range']] = df[['lat', 'lon', 'freq', 'f_range']].replace(',', '.', regex=True).astype(float) - # df['time'] = pd.to_datetime(df['time'], format='%d.%m.%Y %H:%M:%S') - # for row in df.iterrows(): - # row = row[1] - # match row['obj'].split(' ')[-1]: - # case 'V': - # pol = 'Вертикальная' - # case 'H': - # pol = 'Горизонтальная' - # case 'R': - # pol = 'Правая' - # case 'L': - # pol = 'Левая' - # case _: - # pol = '-' - # pol_obj, _ = Polarization.objects.get_or_create( - # name=pol - # ) - # sat_obj, _ = Satellite.objects.get_or_create( - # name=row['sat'], - # defaults={'norad': row['norad_id']} - # ) - # mir_1_obj, _ = Mirror.objects.get_or_create( - # name=row['mir_1'] - # ) - # mir_2_obj, _ = Mirror.objects.get_or_create( - # name=row['mir_2'] - # ) - # mir_lst = [row['mir_1'], row['mir_2']] - # if not pd.isna(row['mir_3']): - # mir_3_obj, _ = Mirror.objects.get_or_create( - - # name=row['mir_3'] - # ) - # vch_load_obj, _ = Parameter.objects.get_or_create( - # id_satellite=sat_obj, - # polarization=pol_obj, - # frequency=row['freq'], - # freq_range=row['f_range'], - # defaults={'id_user_add': CustomUser.objects.get(id=1)} - # ) - - # geo_obj, _ = Geo.objects.get_or_create( - # timestamp=row['time'], - # coords=Point(row['lon'], row['lat'], srid=4326), - # defaults={ - # 'is_average': False, - # 'id_user_add': CustomUser.objects.get(id=1), - # } - # ) - # geo_obj.mirrors.set(Mirror.objects.filter(name__in=mir_lst)) - - # obj_item_obj, _ = ObjItem.objects.get_or_create( - # name=row['obj'], - # # id_satellite=sat_obj, - # id_vch_load=vch_load_obj, - # id_geo=geo_obj, - # defaults={ - # 'id_user_add': CustomUser.objects.get(id=1) - # } - # ) - # obj_item_obj.save() - def get_vch_load_from_html(file, sat: Satellite) -> None: + filename = file.name.split('_') + transfer = filename[3] + match filename[2]: + case 'H': + pol = 'Горизонтальная' + case 'V': + pol = 'Вертикальная' + case 'R': + pol = 'Правая' + case 'L': + pol = 'Левая' + case _: + pol = '-' + tables = pd.read_html(file, encoding='windows-1251') df = tables[0] df = df.drop(0).reset_index(drop=True) @@ -362,6 +310,10 @@ def get_vch_load_from_html(file, sat: Satellite) -> None: else: pack = None + polarization, _ = Polarization.objects.get_or_create( + name=pol + ) + mod, _ = Modulation.objects.get_or_create( name=value['Модуляция'] ) @@ -372,7 +324,10 @@ def get_vch_load_from_html(file, sat: Satellite) -> None: id_satellite=sat, frequency=value['Частота, МГц'], freq_range=value['Полоса, МГц'], + polarization=polarization, defaults={ + "transfer": float(transfer), + # "polarization": polarization, "status": value['Статус'], "power": value['Мощность, дБм'], "bod_velocity": bod_velocity, @@ -386,15 +341,6 @@ def get_vch_load_from_html(file, sat: Satellite) -> None: ) sigma_load.save() -def define_ku_transfer(min_freq: float, max_freq: float) -> int | None: - fss = (10700, 11700) - dss = (11700, 12750) - if min_freq + 9750 >= fss[0] and max_freq + 9750 <= fss[1]: - return 9750 - elif min_freq + 10750 >= dss[0] and max_freq + 10750 <= dss[1]: - return 10750 - return None - def compare_and_link_vch_load(sat_id: Satellite, eps_freq: float, eps_frange: float, ku_range: float): item_obj = ObjItem.objects.filter(id_vch_load__id_satellite=sat_id) vch_sigma = SigmaParameter.objects.filter(id_satellite=sat_id) @@ -406,10 +352,62 @@ def compare_and_link_vch_load(sat_id: Satellite, eps_freq: float, eps_frange: fl continue # if unique_points = Point.objects.order_by('frequency').distinct('frequency') for sigma in vch_sigma: - if abs(sigma.frequency + ku_range - vch_load.frequency) <= vch_load.frequency*eps_freq/100 and abs(sigma.freq_range - vch_load.freq_range) <= vch_load.freq_range*eps_frange/100: + if ( + abs(sigma.transfer_frequency - vch_load.frequency) <= vch_load.frequency*eps_freq/100 and + abs(sigma.freq_range - vch_load.freq_range) <= vch_load.freq_range*eps_frange/100 and + sigma.polarization == vch_load.polarization + ): sigma.parameter = vch_load sigma.save() link_count += 1 return obj_count, link_count - \ No newline at end of file +def kub_report(data_in: io.StringIO) -> pd.DataFrame: + df_in = pd.read_excel(data_in) + df = pd.DataFrame(columns=['Дата', 'Широта', 'Долгота', + 'Высота', 'Населённый пункт', 'ИСЗ', + 'Прямой канал, МГц', 'Обратный канал, МГц', 'Перенос, МГц', 'Полоса, МГц', 'Зеркала']) + for row in df_in.iterrows(): + value = row[1] + date = datetime.date(datetime.now()) + lat = value['Широта, град'] + lon = value['Долгота, град'] + isz = value['ИСЗ'] + downlink = value['Обратный канал, МГц'] + freq_range = value['Полоса, МГц'] + norad = int(re.findall(r'\((\d+)\)', isz)[0]) + sat_obj = Satellite.objects.get(norad=norad) + pol_obj = Polarization.objects.get(name=value['Поляризация'].strip()) + transponder = Transponders.objects.filter( + sat_id=sat_obj, + polarization=pol_obj, + downlink__gte=downlink - F('frequency_range')/2, + downlink__lte=downlink + F('frequency_range')/2, + ).first() + # try: + # location = geolocator.reverse(f"{lat}, {lon}", language="ru").raw['address'] + # loc_name = location.get('city', '') or location.get('town', '') or location.get('province', '') or location.get('country', '') + # except AttributeError: + # loc_name = '' + # time.sleep(1) + loc_name = '' + if transponder: #and not (len(transponder) > 1): + transfer = transponder.transfer + uplink = transfer + downlink + new_row = pd.DataFrame([{'Дата': date, + 'Широта': lat, + 'Долгота': lon, + 'Высота': 0.0, + 'Населённый пункт': loc_name, + 'ИСЗ': isz, + 'Прямой канал, МГц': uplink, + 'Обратный канал, МГц': downlink, + 'Перенос, МГц': transfer, + 'Полоса, МГц': freq_range, + 'Зеркала': '' + }]) + df = pd.concat([df, new_row], ignore_index=True) + else: + print("Ничего не найдено в транспондерах") + return df + diff --git a/dbapp/mainapp/views.py b/dbapp/mainapp/views.py index 564fc52..b5ec0d0 100644 --- a/dbapp/mainapp/views.py +++ b/dbapp/mainapp/views.py @@ -1,6 +1,6 @@ from django.shortcuts import render, redirect from django.contrib import messages -from django.http import JsonResponse +from django.http import JsonResponse, HttpResponse from django.views.decorators.http import require_GET from django.contrib.admin.views.decorators import staff_member_required from django.utils.decorators import method_decorator @@ -13,13 +13,15 @@ from .utils import ( add_satellite_list, get_points_from_csv, get_vch_load_from_html, - compare_and_link_vch_load + compare_and_link_vch_load, + kub_report ) -from mapsapp.utils import parse_transponders_from_json -from .forms import LoadExcelData, LoadCsvData, UploadFileForm, VchLinkForm +from mapsapp.utils import parse_transponders_from_json, parse_transponders_from_xml +from .forms import LoadExcelData, LoadCsvData, UploadFileForm, VchLinkForm, UploadVchLoad, NewEventForm from .models import ObjItem from .clusters import get_clusters -from dbapp.settings import BASE_DIR +from io import BytesIO + class AddSatellitesView(View): @@ -27,13 +29,33 @@ class AddSatellitesView(View): add_satellite_list() return redirect('home') -class AddTranspondersView(View): - def get(self, request): +# class AddTranspondersView(View): +# def get(self, request): +# try: +# parse_transponders_from_json(BASE_DIR / "transponders.json") +# except FileNotFoundError: +# print("Файл не найден") +# return redirect('home') + +class AddTranspondersView(FormView): + template_name = 'mainapp/transponders_upload.html' + form_class = UploadFileForm + + def form_valid(self, form): + uploaded_file = self.request.FILES['file'] try: - parse_transponders_from_json(BASE_DIR / "transponders.json") - except FileNotFoundError: - print("Файл не найден") - return redirect('home') + content = uploaded_file.read() + parse_transponders_from_xml(BytesIO(content)) + messages.success(self.request, "Файл успешно обработан") + except ValueError as e: + messages.error(self.request, f"Ошибка при чтении таблиц: {e}") + except Exception as e: + messages.error(self.request, f"Неизвестная ошибка: {e}") + return redirect('add_trans') + + def form_invalid(self, form): + messages.error(self.request, "Форма заполнена некорректно.") + return super().form_invalid(form) class HomePageView(TemplateView): template_name = 'mainapp/home.html' @@ -118,22 +140,7 @@ class LoadCsvDataView(FormView): messages.error(self.request, "Форма заполнена некорректно.") return super().form_invalid(form) -# def upload_file(request): -# if request.method == 'POST' and request.FILES: -# form = UploadFileForm(request.POST, request.FILES) -# if form.is_valid(): -# uploaded_file = request.FILES['file'] -# # Обработка текстового файла, например: -# df = pd.read_csv(uploaded_file) -# df = pd.read_csv(filepath, sep=";", -# names=['id', 'obj', 'lat', 'lon', 'h', 'time', 'sat', 'norad_id', 'freq', 'f_range', 'et', 'qaul', 'mir_1', 'mir_2', 'mir_3']) -# df[['lat', 'lon', 'freq', 'f_range']] = df[['lat', 'lon', 'freq', 'f_range']].replace(',', '.', regex=True).astype(float) -# df['time'] = pd.to_datetime(df['time'], format='%d.%m.%Y %H:%M:%S') -# get_points_from_csv(df) -# return JsonResponse({'status': 'success'}) -# else: -# return JsonResponse({'status': 'error', 'errors': form.errors}, status=400) -# return render(request, 'mainapp/add_data_from_csv.html') + from collections import defaultdict @method_decorator(staff_member_required, name='dispatch') @@ -162,7 +169,6 @@ class ShowMapView(UserPassesTestMixin, View): 'frequency': p["freq"] }) - # Преобразуем в список словарей для удобства в шаблоне groups = [ { "name": name, @@ -190,7 +196,7 @@ class ClusterTestView(View): class UploadVchLoadView(FormView): template_name = 'mainapp/upload_html.html' - form_class = UploadFileForm + form_class = UploadVchLoad def form_valid(self, form): selected_sat = form.cleaned_data['sat_choice'] @@ -224,4 +230,38 @@ class LinkVchSigmaView(FormView): return redirect('link_vch_sigma') def form_invalid(self, form): - return self.render_to_response(self.get_context_data(form=form)) \ No newline at end of file + return self.render_to_response(self.get_context_data(form=form)) + + +class ProcessKubsatView(FormView): + template_name = 'mainapp/process_kubsat.html' + form_class = NewEventForm + + def form_valid(self, form): + # selected_sat = form.cleaned_data['sat_choice'] + # selected_pol = form.cleaned_data['pol_choice'] + uploaded_file = self.request.FILES['file'] + try: + content = uploaded_file.read() + df = kub_report(BytesIO(content)) + output = BytesIO() + with pd.ExcelWriter(output, engine='openpyxl') as writer: + df.to_excel(writer, index=False, sheet_name='Результат') + output.seek(0) + + response = HttpResponse( + output.getvalue(), + content_type='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' + ) + response['Content-Disposition'] = f'attachment; filename="kubsat_report.xlsx"' + + messages.success(self.request, "Событие успешно обработано!") + return response + except Exception as e: + messages.error(self.request, f"Ошибка при обработке файла: {str(e)}") + return redirect('kubsat_excel') + # return redirect('kubsat_excel') + + def form_invalid(self, form): + messages.error(self.request, "Форма заполнена некорректно.") + return super().form_invalid(form) \ No newline at end of file diff --git a/dbapp/mapsapp/admin.py b/dbapp/mapsapp/admin.py index c755340..d0b9bf2 100644 --- a/dbapp/mapsapp/admin.py +++ b/dbapp/mapsapp/admin.py @@ -5,20 +5,24 @@ from more_admin_filters import MultiSelectDropdownFilter, MultiSelectFilter, Mul from import_export.admin import ImportExportActionModelAdmin @admin.register(Transponders) -class PolarizationAdmin(ImportExportActionModelAdmin, admin.ModelAdmin): +class TranspondersAdmin(ImportExportActionModelAdmin, admin.ModelAdmin): list_display = ( "sat_id", "name", "zone_name", - "frequency", + "downlink", + "uplink", "frequency_range", + "transfer", "polarization", ) list_filter = ( ("polarization", MultiSelectRelatedDropdownFilter), ("sat_id", MultiSelectRelatedDropdownFilter), - ("frequency", NumericRangeFilterBuilder()), + # ("frequency", NumericRangeFilterBuilder()), "zone_name" ) - search_fields = ("name",) + search_fields = ("name", "sat_id__name") ordering = ("name",) + # def sat_name(self, obj): + # return diff --git a/dbapp/mapsapp/migrations/0002_remove_transponders_frequency_transponders_downlink_and_more.py b/dbapp/mapsapp/migrations/0002_remove_transponders_frequency_transponders_downlink_and_more.py new file mode 100644 index 0000000..4629613 --- /dev/null +++ b/dbapp/mapsapp/migrations/0002_remove_transponders_frequency_transponders_downlink_and_more.py @@ -0,0 +1,34 @@ +# Generated by Django 5.2.7 on 2025-10-27 12:20 + +import django.db.models.expressions +import django.db.models.functions.math +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('mapsapp', '0001_initial'), + ] + + operations = [ + migrations.RemoveField( + model_name='transponders', + name='frequency', + ), + migrations.AddField( + model_name='transponders', + name='downlink', + field=models.FloatField(blank=True, null=True, verbose_name='Downlink'), + ), + migrations.AddField( + model_name='transponders', + name='uplink', + field=models.FloatField(blank=True, null=True, verbose_name='Uplink'), + ), + migrations.AddField( + model_name='transponders', + name='transfer', + field=models.GeneratedField(db_persist=True, expression=models.ExpressionWrapper(django.db.models.functions.math.Abs(django.db.models.expressions.CombinedExpression(models.F('downlink'), '-', models.F('uplink'))), output_field=models.FloatField()), null=True, output_field=models.FloatField(), verbose_name='Расстояние между купсатом и гео, км'), + ), + ] diff --git a/dbapp/mapsapp/migrations/0003_alter_transponders_transfer.py b/dbapp/mapsapp/migrations/0003_alter_transponders_transfer.py new file mode 100644 index 0000000..50a890d --- /dev/null +++ b/dbapp/mapsapp/migrations/0003_alter_transponders_transfer.py @@ -0,0 +1,20 @@ +# Generated by Django 5.2.7 on 2025-10-27 13:10 + +import django.db.models.expressions +import django.db.models.functions.math +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('mapsapp', '0002_remove_transponders_frequency_transponders_downlink_and_more'), + ] + + operations = [ + migrations.AlterField( + model_name='transponders', + name='transfer', + field=models.GeneratedField(db_persist=True, expression=models.ExpressionWrapper(django.db.models.functions.math.Abs(django.db.models.expressions.CombinedExpression(models.F('downlink'), '-', models.F('uplink'))), output_field=models.FloatField()), null=True, output_field=models.FloatField(), verbose_name='Перенос'), + ), + ] diff --git a/dbapp/mapsapp/migrations/0004_alter_transponders_zone_name.py b/dbapp/mapsapp/migrations/0004_alter_transponders_zone_name.py new file mode 100644 index 0000000..ff585f7 --- /dev/null +++ b/dbapp/mapsapp/migrations/0004_alter_transponders_zone_name.py @@ -0,0 +1,18 @@ +# Generated by Django 5.2.7 on 2025-10-28 05:44 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('mapsapp', '0003_alter_transponders_transfer'), + ] + + operations = [ + migrations.AlterField( + model_name='transponders', + name='zone_name', + field=models.CharField(blank=True, max_length=255, null=True, verbose_name='Название зоны'), + ), + ] diff --git a/dbapp/mapsapp/migrations/0005_alter_transponders_frequency_range.py b/dbapp/mapsapp/migrations/0005_alter_transponders_frequency_range.py new file mode 100644 index 0000000..99f1ab7 --- /dev/null +++ b/dbapp/mapsapp/migrations/0005_alter_transponders_frequency_range.py @@ -0,0 +1,18 @@ +# Generated by Django 5.2.7 on 2025-10-29 14:00 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('mapsapp', '0004_alter_transponders_zone_name'), + ] + + operations = [ + migrations.AlterField( + model_name='transponders', + name='frequency_range', + field=models.FloatField(blank=True, null=True, verbose_name='Полоса'), + ), + ] diff --git a/dbapp/mapsapp/models.py b/dbapp/mapsapp/models.py index edc5d79..6e7e8ed 100644 --- a/dbapp/mapsapp/models.py +++ b/dbapp/mapsapp/models.py @@ -1,15 +1,27 @@ from django.db import models from mainapp.models import Satellite, Polarization, get_default_polarization +from django.db.models import F, ExpressionWrapper +from django.db.models.functions import Abs class Transponders(models.Model): name = models.CharField(max_length=30, null=True, blank=True, verbose_name="Название транспондера") - frequency = models.FloatField(blank=True, null=True, verbose_name="Центральная частота") - frequency_range = models.FloatField(blank=True, null=True, verbose_name="Полоса частот") - zone_name = models.CharField(max_length=60, blank=True, null=True, verbose_name="Название зоны") + downlink = models.FloatField(blank=True, null=True, verbose_name="Downlink") + frequency_range = models.FloatField(blank=True, null=True, verbose_name="Полоса") + uplink = models.FloatField(blank=True, null=True, verbose_name="Uplink") + zone_name = models.CharField(max_length=255, blank=True, null=True, verbose_name="Название зоны") polarization = models.ForeignKey( Polarization, default=get_default_polarization, on_delete=models.SET_DEFAULT, related_name="tran_polarizations", null=True, blank=True, verbose_name="Поляризация" ) sat_id = models.ForeignKey(Satellite, on_delete=models.PROTECT, related_name="tran_satellite", verbose_name="Спутник") + transfer =models.GeneratedField( + expression=ExpressionWrapper( + Abs(F('downlink') - F('uplink')), + output_field=models.FloatField() + ), + output_field=models.FloatField(), + db_persist=True, + null=True, blank=True, verbose_name="Перенос" + ) def __str__(self): return self.name diff --git a/dbapp/mapsapp/utils.py b/dbapp/mapsapp/utils.py index e56a3e3..2c01317 100644 --- a/dbapp/mapsapp/utils.py +++ b/dbapp/mapsapp/utils.py @@ -3,6 +3,7 @@ import re import json from .models import Transponders from mainapp.models import Polarization, Satellite +from io import BytesIO def search_satellite_on_page(data: dict, satellite_name: str): for pos, value in data.get('page', {}).get('positions').items(): @@ -90,3 +91,68 @@ def parse_transponders_from_json(filepath: str): ) tran_obj.save() + +from lxml import etree + +def parse_transponders_from_xml(data_in: BytesIO): + + tree = etree.parse(data_in) + ns = { + 'i': 'http://www.w3.org/2001/XMLSchema-instance', + 'ns': 'http://schemas.datacontract.org/2004/07/Geolocation.Domain.Utils.Repository.SatellitesSerialization.Memos', + 'tr': 'http://schemas.datacontract.org/2004/07/Geolocation.Common.Extensions' + } + satellites = tree.xpath('//ns:SatelliteMemo', namespaces=ns) + for sat in satellites[:]: + name = sat.xpath('./ns:name/text()', namespaces=ns)[0] + if name == 'X' or 'DONT USE' in name: + continue + norad = sat.xpath('./ns:norad/text()', namespaces=ns) + beams = sat.xpath('.//ns:BeamMemo', namespaces=ns) + zones = {} + for zone in beams: + zone_name = zone.xpath('./ns:name/text()', namespaces=ns)[0] if zone.xpath('./ns:name/text()', namespaces=ns) else '-' + zones[zone.xpath('./ns:id/text()', namespaces=ns)[0]] = { + "name": zone_name, + "pol": zone.xpath('./ns:polarization/text()', namespaces=ns)[0], + } + transponders = sat.xpath('.//ns:TransponderMemo', namespaces=ns) + for transponder in transponders: + tr_id = transponder.xpath('./ns:downlinkBeamId/text()', namespaces=ns)[0] + downlink_start = float(transponder.xpath('./ns:downlinkFrequency/tr:start/text()', namespaces=ns)[0]) + downlink_end = float(transponder.xpath('./ns:downlinkFrequency/tr:end/text()', namespaces=ns)[0]) + uplink_start = float(transponder.xpath('./ns:uplinkFrequency/tr:start/text()', namespaces=ns)[0]) + uplink_end = float(transponder.xpath('./ns:uplinkFrequency/tr:end/text()', namespaces=ns)[0]) + tr_data = zones[tr_id] + # p = tr_data['pol'][0] if tr_data['pol'] else '-' + match tr_data['pol']: + case 'Horizontal': + pol = 'Горизонтальная' + case 'Vertical': + pol = 'Вертикальная' + case 'CircularRight': + pol = 'Правая' + case 'CircularLeft': + pol = 'Левая' + case _: + pol = '-' + tr_name = transponder.xpath('./ns:name/text()', namespaces=ns)[0] + + pol_obj, _ = Polarization.objects.get_or_create(name=pol) + sat_obj, _ = Satellite.objects.get_or_create( + name=name, + defaults={ + "norad": int(norad[0]) if norad else -1 + }) + trans_obj, _ = Transponders.objects.get_or_create( + polarization=pol_obj, + downlink=(downlink_start+downlink_end)/2/1000000, + uplink=(uplink_start+uplink_end)/2/1000000, + frequency_range=abs(downlink_end-downlink_start)/1000000, + name=tr_name, + defaults={ + "zone_name": tr_data['name'], + "sat_id": sat_obj, + } + ) + trans_obj.save() diff --git a/dbapp/pyproject.toml b/dbapp/pyproject.toml index 8097f46..cbeb3f6 100644 --- a/dbapp/pyproject.toml +++ b/dbapp/pyproject.toml @@ -19,7 +19,9 @@ dependencies = [ "django-leaflet>=0.32.0", "django-map-widgets>=0.5.1", "django-more-admin-filters>=1.13", - "gdal", + "dotenv>=0.9.9", + "geopy>=2.4.1", + "gunicorn>=23.0.0", "lxml>=6.0.2", "matplotlib>=3.10.7", "numpy>=2.3.3", @@ -28,9 +30,11 @@ dependencies = [ "psycopg>=3.2.10", "redis>=6.4.0", "requests>=2.32.5", + "reverse-geocoder>=1.5.1", "scikit-learn>=1.7.2", "setuptools>=80.9.0", ] -[tool.uv.sources] -gdal = { path = "gdal-3.10.2-cp313-cp313-win_amd64.whl" } + +[dependency-groups] +dev = [] diff --git a/dbapp/uv.lock b/dbapp/uv.lock index c04d333..f0c03a5 100644 --- a/dbapp/uv.lock +++ b/dbapp/uv.lock @@ -212,7 +212,9 @@ dependencies = [ { name = "django-leaflet" }, { name = "django-map-widgets" }, { name = "django-more-admin-filters" }, - { name = "gdal" }, + { name = "dotenv" }, + { name = "geopy" }, + { name = "gunicorn" }, { name = "lxml" }, { name = "matplotlib" }, { name = "numpy" }, @@ -221,6 +223,7 @@ dependencies = [ { name = "psycopg" }, { name = "redis" }, { name = "requests" }, + { name = "reverse-geocoder" }, { name = "scikit-learn" }, { name = "setuptools" }, ] @@ -241,7 +244,9 @@ requires-dist = [ { name = "django-leaflet", specifier = ">=0.32.0" }, { name = "django-map-widgets", specifier = ">=0.5.1" }, { name = "django-more-admin-filters", specifier = ">=1.13" }, - { name = "gdal", path = "gdal-3.10.2-cp313-cp313-win_amd64.whl" }, + { name = "dotenv", specifier = ">=0.9.9" }, + { name = "geopy", specifier = ">=2.4.1" }, + { name = "gunicorn", specifier = ">=23.0.0" }, { name = "lxml", specifier = ">=6.0.2" }, { name = "matplotlib", specifier = ">=3.10.7" }, { name = "numpy", specifier = ">=2.3.3" }, @@ -250,10 +255,14 @@ requires-dist = [ { name = "psycopg", specifier = ">=3.2.10" }, { name = "redis", specifier = ">=6.4.0" }, { name = "requests", specifier = ">=2.32.5" }, + { name = "reverse-geocoder", specifier = ">=1.5.1" }, { name = "scikit-learn", specifier = ">=1.7.2" }, { name = "setuptools", specifier = ">=80.9.0" }, ] +[package.metadata.requires-dev] +dev = [] + [[package]] name = "diff-match-patch" version = "20241021" @@ -410,6 +419,17 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/87/7c/4b261b96b357d94ef267f39856ef0bb72a33f078a38bd22ee96d168fe272/django_more_admin_filters-1.13-py3-none-any.whl", hash = "sha256:df4d46e4b589566b85f149ea5b7558c6cc4ae22b0d264973f8d4a2d478ef5120", size = 147360, upload-time = "2025-06-06T11:26:42.964Z" }, ] +[[package]] +name = "dotenv" +version = "0.9.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "python-dotenv" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/b2/b7/545d2c10c1fc15e48653c91efde329a790f2eecfbbf2bd16003b5db2bab0/dotenv-0.9.9-py2.py3-none-any.whl", hash = "sha256:29cf74a087b31dafdb5a446b6d7e11cbce8ed2741540e2339c69fbef92c94ce9", size = 1892, upload-time = "2025-02-19T22:15:01.647Z" }, +] + [[package]] name = "et-xmlfile" version = "2.0.0" @@ -453,16 +473,37 @@ wheels = [ ] [[package]] -name = "gdal" -version = "3.10.2" -source = { path = "gdal-3.10.2-cp313-cp313-win_amd64.whl" } +name = "geographiclib" +version = "2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/df/78/4892343230a9d29faa1364564e525307a37e54ad776ea62c12129dbba704/geographiclib-2.1.tar.gz", hash = "sha256:6a6545e6262d0ed3522e13c515713718797e37ed8c672c31ad7b249f372ef108", size = 37004, upload-time = "2025-08-21T21:34:26Z" } wheels = [ - { filename = "gdal-3.10.2-cp313-cp313-win_amd64.whl", hash = "sha256:d6aae781b9847065f831f3457c6c01d0b9272818656031d723dc88c160a8ec26" }, + { url = "https://files.pythonhosted.org/packages/31/b3/802576f2ea5dcb48501bb162e4c7b7b3ca5654a42b2c968ef98a797a4c79/geographiclib-2.1-py3-none-any.whl", hash = "sha256:e2a873b9b9e7fc38721ad73d5f4e6c9ed140d428a339970f505c07056997d40b", size = 40740, upload-time = "2025-08-21T21:34:24.955Z" }, ] -[package.metadata] -requires-dist = [{ name = "numpy", marker = "extra == 'numpy'", specifier = ">1.0.0" }] -provides-extras = ["numpy"] +[[package]] +name = "geopy" +version = "2.4.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "geographiclib" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0e/fd/ef6d53875ceab72c1fad22dbed5ec1ad04eb378c2251a6a8024bad890c3b/geopy-2.4.1.tar.gz", hash = "sha256:50283d8e7ad07d89be5cb027338c6365a32044df3ae2556ad3f52f4840b3d0d1", size = 117625, upload-time = "2023-11-23T21:49:32.734Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e5/15/cf2a69ade4b194aa524ac75112d5caac37414b20a3a03e6865dfe0bd1539/geopy-2.4.1-py3-none-any.whl", hash = "sha256:ae8b4bc5c1131820f4d75fce9d4aaaca0c85189b3aa5d64c3dcaf5e3b7b882a7", size = 125437, upload-time = "2023-11-23T21:49:30.421Z" }, +] + +[[package]] +name = "gunicorn" +version = "23.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "packaging" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/34/72/9614c465dc206155d93eff0ca20d42e1e35afc533971379482de953521a4/gunicorn-23.0.0.tar.gz", hash = "sha256:f014447a0101dc57e294f6c18ca6b40227a4c90e9bdb586042628030cba004ec", size = 375031, upload-time = "2024-08-10T20:25:27.378Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/7d/6dac2a6e1eba33ee43f318edbed4ff29151a49b5d37f080aad1e6469bca4/gunicorn-23.0.0-py3-none-any.whl", hash = "sha256:ec400d38950de4dfd418cff8328b2c8faed0edb0d517d3394e457c317908ca4d", size = 85029, upload-time = "2024-08-10T20:25:24.996Z" }, +] [[package]] name = "idna" @@ -851,6 +892,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" }, ] +[[package]] +name = "python-dotenv" +version = "1.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f0/26/19cadc79a718c5edbec86fd4919a6b6d3f681039a2f6d66d14be94e75fb9/python_dotenv-1.2.1.tar.gz", hash = "sha256:42667e897e16ab0d66954af0e60a9caa94f0fd4ecf3aaf6d2d260eec1aa36ad6", size = 44221, upload-time = "2025-10-26T15:12:10.434Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/14/1b/a298b06749107c305e1fe0f814c6c74aea7b2f1e10989cb30f544a1b3253/python_dotenv-1.2.1-py3-none-any.whl", hash = "sha256:b81ee9561e9ca4004139c6cbba3a238c32b03e4894671e181b671e8cb8425d61", size = 21230, upload-time = "2025-10-26T15:12:09.109Z" }, +] + [[package]] name = "python-slugify" version = "8.0.4" @@ -896,6 +946,16 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z" }, ] +[[package]] +name = "reverse-geocoder" +version = "1.5.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, + { name = "scipy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0b/0f/b7d5d4b36553731f11983e19e1813a1059ad0732c5162c01b3220c927d31/reverse_geocoder-1.5.1.tar.gz", hash = "sha256:2a2e781b5f69376d922b78fe8978f1350c84fce0ddb07e02c834ecf98b57c75c", size = 2246559, upload-time = "2016-09-15T16:46:46.277Z" } + [[package]] name = "scikit-learn" version = "1.7.2"