Compare commits

..

3 Commits

16 changed files with 2642 additions and 568 deletions

View File

@@ -534,13 +534,8 @@ class SourceForm(forms.ModelForm):
instance = super().save(commit=False) instance = super().save(commit=False)
# Обработка coords_average # coords_average НЕ обрабатываем здесь - это поле управляется только программно
avg_lat = self.cleaned_data.get("average_latitude") # (через _recalculate_average_coords в модели Source)
avg_lng = self.cleaned_data.get("average_longitude")
if avg_lat is not None and avg_lng is not None:
instance.coords_average = Point(avg_lng, avg_lat, srid=4326)
else:
instance.coords_average = None
# Обработка coords_kupsat # Обработка coords_kupsat
kup_lat = self.cleaned_data.get("kupsat_latitude") kup_lat = self.cleaned_data.get("kupsat_latitude")

View File

@@ -2,29 +2,32 @@
Переиспользуемый компонент для отображения сообщений Django Переиспользуемый компонент для отображения сообщений Django
Использование: Использование:
{% include 'mainapp/components/_messages.html' %} {% include 'mainapp/components/_messages.html' %}
Для отключения автоскрытия добавьте extra_tags='persistent':
messages.success(request, "Сообщение", extra_tags='persistent')
{% endcomment %} {% endcomment %}
{% if messages %} {% if messages %}
<div class="messages-container"> <div class="messages-container">
{% for message in messages %} {% for message in messages %}
<div class="alert alert-{{ message.tags }} alert-dismissible fade show auto-dismiss" role="alert"> <div class="alert alert-{% if 'error' in message.tags %}danger{% elif 'success' in message.tags %}success{% elif 'warning' in message.tags %}warning{% else %}info{% endif %} alert-dismissible fade show {% if 'persistent' not in message.tags %}auto-dismiss{% endif %}" role="alert">
{% if message.tags == 'error' %} {% if 'error' in message.tags %}
<i class="bi bi-exclamation-triangle-fill me-2"></i> <i class="bi bi-exclamation-triangle-fill me-2"></i>
{% elif message.tags == 'success' %} {% elif 'success' in message.tags %}
<i class="bi bi-check-circle-fill me-2"></i> <i class="bi bi-check-circle-fill me-2"></i>
{% elif message.tags == 'warning' %} {% elif 'warning' in message.tags %}
<i class="bi bi-exclamation-circle-fill me-2"></i> <i class="bi bi-exclamation-circle-fill me-2"></i>
{% elif message.tags == 'info' %} {% elif 'info' in message.tags %}
<i class="bi bi-info-circle-fill me-2"></i> <i class="bi bi-info-circle-fill me-2"></i>
{% endif %} {% endif %}
{{ message }} {{ message|safe }}
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button> <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div> </div>
{% endfor %} {% endfor %}
</div> </div>
<script> <script>
// Автоматическое скрытие уведомлений через 5 секунд // Автоматическое скрытие уведомлений через 5 секунд (кроме persistent)
document.addEventListener('DOMContentLoaded', function() { document.addEventListener('DOMContentLoaded', function() {
const alerts = document.querySelectorAll('.alert.auto-dismiss'); const alerts = document.querySelectorAll('.alert.auto-dismiss');
alerts.forEach(function(alert) { alerts.forEach(function(alert) {

File diff suppressed because it is too large Load Diff

View File

@@ -124,7 +124,7 @@
</div> </div>
<div class="averaging-container"> <div class="averaging-container">
<h2>Усреднение точек по источникам</h2> <h2>Усреднение точек по объектам</h2>
<div class="form-section"> <div class="form-section">
<div class="row"> <div class="row">
@@ -156,7 +156,7 @@
<div class="table-section"> <div class="table-section">
<div class="d-flex justify-content-between align-items-center"> <div class="d-flex justify-content-between align-items-center">
<div> <div>
<h5>Источники <span id="source-count" class="badge bg-primary">0</span></h5> <h5>Объекты <span id="source-count" class="badge bg-primary">0</span></h5>
</div> </div>
<div class="btn-group-custom"> <div class="btn-group-custom">
<button id="export-xlsx" class="btn btn-success" disabled> <button id="export-xlsx" class="btn btn-success" disabled>
@@ -180,7 +180,7 @@
<div class="modal-dialog modal-xl"> <div class="modal-dialog modal-xl">
<div class="modal-content"> <div class="modal-content">
<div class="modal-header"> <div class="modal-header">
<h5 class="modal-title" id="sourceDetailsModalLabel">Детали источника</h5> <h5 class="modal-title" id="sourceDetailsModalLabel">Детали объекта</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button> <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div> </div>
<div class="modal-body" id="modal-body-content"> <div class="modal-body" id="modal-body-content">
@@ -279,7 +279,7 @@ document.addEventListener('DOMContentLoaded', function() {
{column: "frequency", dir: "asc"} {column: "frequency", dir: "asc"}
], ],
columns: [ columns: [
{title: "Источник", field: "source_name", minWidth: 180, widthGrow: 2}, {title: "Объект", field: "source_name", minWidth: 180, widthGrow: 2},
{title: "Групп", field: "groups_count", minWidth: 70, hozAlign: "center"}, {title: "Групп", field: "groups_count", minWidth: 70, hozAlign: "center"},
{title: "Точек", field: "total_points", minWidth: 70, hozAlign: "center"}, {title: "Точек", field: "total_points", minWidth: 70, hozAlign: "center"},
{title: "Частота", field: "frequency", minWidth: 100, sorter: "number"}, {title: "Частота", field: "frequency", minWidth: 100, sorter: "number"},
@@ -327,7 +327,7 @@ document.addEventListener('DOMContentLoaded', function() {
// Delete source // Delete source
function deleteSource(sourceIdx) { function deleteSource(sourceIdx) {
//if (!confirm('Удалить этот источник со всеми группами?')) return; //if (!confirm('Удалить этот объект со всеми группами?')) return;
allSourcesData.splice(sourceIdx, 1); allSourcesData.splice(sourceIdx, 1);
updateSourcesTable(); updateSourcesTable();
} }
@@ -338,7 +338,7 @@ document.addEventListener('DOMContentLoaded', function() {
const source = allSourcesData[sourceIdx]; const source = allSourcesData[sourceIdx];
if (!source) return; if (!source) return;
document.getElementById('sourceDetailsModalLabel').textContent = `Источник: ${source.source_name}`; document.getElementById('sourceDetailsModalLabel').textContent = `Объект: ${source.source_name}`;
renderModalContent(); renderModalContent();
const modal = new bootstrap.Modal(document.getElementById('sourceDetailsModal')); const modal = new bootstrap.Modal(document.getElementById('sourceDetailsModal'));
@@ -619,7 +619,7 @@ document.addEventListener('DOMContentLoaded', function() {
allSourcesData.forEach(source => { allSourcesData.forEach(source => {
source.groups.forEach(group => { source.groups.forEach(group => {
summaryData.push({ summaryData.push({
'Источник': source.source_name, 'Объект': source.source_name,
'Частота, МГц': group.frequency, 'Частота, МГц': group.frequency,
'Полоса, МГц': group.freq_range, 'Полоса, МГц': group.freq_range,
'Символьная скорость, БОД': group.bod_velocity, 'Символьная скорость, БОД': group.bod_velocity,
@@ -646,7 +646,7 @@ document.addEventListener('DOMContentLoaded', function() {
source.groups.forEach(group => { source.groups.forEach(group => {
group.points.forEach(point => { group.points.forEach(point => {
allPointsData.push({ allPointsData.push({
'Источник': source.source_name, 'Объект': source.source_name,
'ID точки': point.id, 'ID точки': point.id,
'Имя точки': point.name, 'Имя точки': point.name,
'Частота, МГц': point.frequency, 'Частота, МГц': point.frequency,

View File

@@ -216,7 +216,7 @@
filterPolygon.addTo(map); filterPolygon.addTo(map);
// Добавляем popup с информацией // Добавляем popup с информацией
filterPolygon.bindPopup('<strong>Область фильтра</strong><br>Отображаются только источники с точками в этой области'); filterPolygon.bindPopup('<strong>Область фильтра</strong><br>Отображаются только объекты с точками в этой области');
// Если нет других точек, центрируем карту на полигоне // Если нет других точек, центрируем карту на полигоне
{% if not groups %} {% if not groups %}

View File

@@ -54,7 +54,25 @@ class AddTranspondersView(LoginRequiredMixin, FormMessageMixin, FormView):
try: try:
content = uploaded_file.read() content = uploaded_file.read()
# Передаем текущего пользователя в функцию парсинга # Передаем текущего пользователя в функцию парсинга
parse_transponders_from_xml(BytesIO(content), self.request.user.customuser) stats = parse_transponders_from_xml(BytesIO(content), self.request.user.customuser)
# Формируем сообщение со статистикой
stats_message = (
f"<strong>Импорт завершён</strong><br>"
f"Спутники: создано {stats['satellites_created']}, "
f"обновлено {stats['satellites_updated']}, "
f"пропущено {stats['satellites_skipped']}, "
f"игнорировано {stats['satellites_ignored']}<br>"
f"Транспондеры: создано {stats['transponders_created']}, "
f"существующих {stats['transponders_existing']}"
)
if stats['errors']:
stats_message += f"<br><strong>Ошибок: {len(stats['errors'])}</strong>"
messages.warning(self.request, stats_message, extra_tags='persistent')
else:
messages.success(self.request, stats_message, extra_tags='persistent')
except ValueError as e: except ValueError as e:
messages.error(self.request, f"Ошибка при чтении таблиц: {e}") messages.error(self.request, f"Ошибка при чтении таблиц: {e}")
return redirect("mainapp:add_trans") return redirect("mainapp:add_trans")

View File

@@ -154,7 +154,7 @@ class ShowSourcesMapView(LoginRequiredMixin, View):
points.append( points.append(
{ {
"point": (coords.x, coords.y), # (lon, lat) "point": (coords.x, coords.y), # (lon, lat)
"source_id": f"Источник #{source.id}", "source_id": f"Объект #{source.id}",
} }
) )

View File

@@ -0,0 +1,20 @@
# Generated by Django 5.2.7 on 2025-12-03 07:51
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('mainapp', '0017_add_satellite_alternative_name'),
('mapsapp', '0002_alter_transponders_snr'),
]
operations = [
migrations.AlterField(
model_name='transponders',
name='sat_id',
field=models.ForeignKey(help_text='Спутник, которому принадлежит транспондер', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='tran_satellite', to='mainapp.satellite', verbose_name='Спутник'),
),
]

View File

@@ -1,149 +1,150 @@
# Django imports # Django imports
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.core.validators import MaxValueValidator, MinValueValidator from django.core.validators import MaxValueValidator, MinValueValidator
from django.db import models from django.db import models
from django.db.models import ExpressionWrapper, F from django.db.models import ExpressionWrapper, F
from django.db.models.functions import Abs from django.db.models.functions import Abs
# Local imports # Local imports
from mainapp.models import Polarization, Satellite, get_default_polarization, CustomUser from mainapp.models import Polarization, Satellite, get_default_polarization, CustomUser
class Transponders(models.Model): class Transponders(models.Model):
""" """
Модель транспондера спутника. Модель транспондера спутника.
Хранит информацию о частотах uplink/downlink, зоне покрытия и поляризации. Хранит информацию о частотах uplink/downlink, зоне покрытия и поляризации.
""" """
# Основные поля # Основные поля
name = models.CharField( name = models.CharField(
max_length=30, max_length=30,
null=True, null=True,
blank=True, blank=True,
verbose_name="Название транспондера", verbose_name="Название транспондера",
db_index=True, db_index=True,
help_text="Название транспондера", help_text="Название транспондера",
) )
downlink = models.FloatField( downlink = models.FloatField(
blank=True, blank=True,
null=True, null=True,
verbose_name="Downlink", verbose_name="Downlink",
# validators=[MinValueValidator(0), MaxValueValidator(50000)], # validators=[MinValueValidator(0), MaxValueValidator(50000)],
# help_text="Частота downlink в МГц (0-50000)" # help_text="Частота downlink в МГц (0-50000)"
) )
frequency_range = models.FloatField( frequency_range = models.FloatField(
blank=True, blank=True,
null=True, null=True,
verbose_name="Полоса", verbose_name="Полоса",
# validators=[MinValueValidator(0), MaxValueValidator(1000)], # validators=[MinValueValidator(0), MaxValueValidator(1000)],
# help_text="Полоса частот в МГц (0-1000)" # help_text="Полоса частот в МГц (0-1000)"
) )
uplink = models.FloatField( uplink = models.FloatField(
blank=True, blank=True,
null=True, null=True,
verbose_name="Uplink", verbose_name="Uplink",
# validators=[MinValueValidator(0), MaxValueValidator(50000)], # validators=[MinValueValidator(0), MaxValueValidator(50000)],
# help_text="Частота uplink в МГц (0-50000)" # help_text="Частота uplink в МГц (0-50000)"
) )
zone_name = models.CharField( zone_name = models.CharField(
max_length=255, max_length=255,
blank=True, blank=True,
null=True, null=True,
verbose_name="Название зоны", verbose_name="Название зоны",
db_index=True, db_index=True,
help_text="Название зоны покрытия транспондера", help_text="Название зоны покрытия транспондера",
) )
snr = models.FloatField( snr = models.FloatField(
blank=True, blank=True,
null=True, null=True,
verbose_name="ОСШ, дБ", verbose_name="ОСШ, дБ",
# validators=[MinValueValidator(0), MaxValueValidator(1000)], # validators=[MinValueValidator(0), MaxValueValidator(1000)],
help_text="Отношение сигнал/шум в децибелах", help_text="Отношение сигнал/шум в децибелах",
) )
created_at = models.DateTimeField( created_at = models.DateTimeField(
auto_now_add=True, auto_now_add=True,
verbose_name="Дата создания", verbose_name="Дата создания",
help_text="Дата и время создания записи", help_text="Дата и время создания записи",
) )
created_by = models.ForeignKey( created_by = models.ForeignKey(
CustomUser, CustomUser,
on_delete=models.SET_NULL, on_delete=models.SET_NULL,
related_name="transponder_created", related_name="transponder_created",
null=True, null=True,
blank=True, blank=True,
verbose_name="Создан пользователем", verbose_name="Создан пользователем",
help_text="Пользователь, создавший запись", help_text="Пользователь, создавший запись",
) )
updated_at = models.DateTimeField( updated_at = models.DateTimeField(
auto_now=True, auto_now=True,
verbose_name="Дата последнего изменения", verbose_name="Дата последнего изменения",
help_text="Дата и время последнего изменения", help_text="Дата и время последнего изменения",
) )
updated_by = models.ForeignKey( updated_by = models.ForeignKey(
CustomUser, CustomUser,
on_delete=models.SET_NULL, on_delete=models.SET_NULL,
related_name="transponder_updated", related_name="transponder_updated",
null=True, null=True,
blank=True, blank=True,
verbose_name="Изменен пользователем", verbose_name="Изменен пользователем",
help_text="Пользователь, последним изменивший запись", help_text="Пользователь, последним изменивший запись",
) )
# Связи # Связи
polarization = models.ForeignKey( polarization = models.ForeignKey(
Polarization, Polarization,
default=get_default_polarization, default=get_default_polarization,
on_delete=models.SET_DEFAULT, on_delete=models.SET_DEFAULT,
related_name="tran_polarizations", related_name="tran_polarizations",
null=True, null=True,
blank=True, blank=True,
verbose_name="Поляризация", verbose_name="Поляризация",
help_text="Поляризация сигнала", help_text="Поляризация сигнала",
) )
sat_id = models.ForeignKey( sat_id = models.ForeignKey(
Satellite, Satellite,
on_delete=models.PROTECT, on_delete=models.SET_NULL,
related_name="tran_satellite", null=True,
verbose_name="Спутник", related_name="tran_satellite",
db_index=True, verbose_name="Спутник",
help_text="Спутник, которому принадлежит транспондер", db_index=True,
) help_text="Спутник, которому принадлежит транспондер",
)
# Вычисляемые поля
transfer = models.GeneratedField( # Вычисляемые поля
expression=ExpressionWrapper( transfer = models.GeneratedField(
Abs(F("downlink") - F("uplink")), output_field=models.FloatField() expression=ExpressionWrapper(
), Abs(F("downlink") - F("uplink")), output_field=models.FloatField()
output_field=models.FloatField(), ),
db_persist=True, output_field=models.FloatField(),
null=True, db_persist=True,
blank=True, null=True,
verbose_name="Перенос", blank=True,
) verbose_name="Перенос",
)
# def clean(self):
# """Валидация на уровне модели""" # def clean(self):
# super().clean() # """Валидация на уровне модели"""
# super().clean()
# # Проверка что downlink и uplink заданы
# if self.downlink and self.uplink: # # Проверка что downlink и uplink заданы
# # Обычно uplink выше downlink для спутниковой связи # if self.downlink and self.uplink:
# if self.uplink < self.downlink: # # Обычно uplink выше downlink для спутниковой связи
# raise ValidationError({ # if self.uplink < self.downlink:
# 'uplink': 'Частота uplink обычно выше частоты downlink' # raise ValidationError({
# }) # 'uplink': 'Частота uplink обычно выше частоты downlink'
# })
def __str__(self):
if self.name: def __str__(self):
return self.name if self.name:
return f"Транспондер {self.sat_id.name if self.sat_id else 'Unknown'}" return self.name
return f"Транспондер {self.sat_id.name if self.sat_id else 'Unknown'}"
class Meta:
verbose_name = "Транспондер" class Meta:
verbose_name_plural = "Транспондеры" verbose_name = "Транспондер"
ordering = ["sat_id", "downlink"] verbose_name_plural = "Транспондеры"
indexes = [ ordering = ["sat_id", "downlink"]
models.Index(fields=["sat_id", "downlink"]), indexes = [
models.Index(fields=["sat_id", "zone_name"]), models.Index(fields=["sat_id", "downlink"]),
] models.Index(fields=["sat_id", "zone_name"]),
]

View File

@@ -0,0 +1,642 @@
/* Multi Sources Playback Map Styles */
body {
overflow: hidden;
}
#map {
position: fixed;
top: 56px;
bottom: 0;
left: 0;
right: 0;
z-index: 1;
}
.legend {
background: white;
padding: 10px;
border-radius: 4px;
box-shadow: 0 0 10px rgba(0,0,0,0.2);
font-size: 11px;
max-height: 350px;
overflow-y: auto;
}
.legend h6 {
font-size: 12px;
margin: 0 0 8px 0;
}
.legend-item {
margin: 4px 0;
display: flex;
align-items: center;
}
.legend-section {
margin-top: 10px;
padding-top: 10px;
border-top: 1px solid #ddd;
}
.legend-section:first-child {
margin-top: 0;
padding-top: 0;
border-top: none;
}
.playback-control {
position: fixed;
bottom: 20px;
left: 50%;
transform: translateX(-50%);
z-index: 1000;
background: white;
padding: 15px 20px;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0,0,0,0.3);
display: flex;
align-items: center;
gap: 15px;
flex-wrap: wrap;
max-width: 90%;
}
.playback-control button {
padding: 8px 14px;
border: none;
border-radius: 4px;
background: #007bff;
color: white;
cursor: pointer;
font-size: 14px;
}
.playback-control button:hover {
background: #0056b3;
}
.playback-control button:disabled {
background: #ccc;
cursor: not-allowed;
}
.playback-control .time-display {
font-size: 14px;
font-weight: bold;
min-width: 180px;
text-align: center;
}
.playback-control input[type="range"] {
width: 300px;
}
.playback-control .speed-control {
display: flex;
align-items: center;
gap: 8px;
}
.playback-control .speed-control label {
font-size: 12px;
margin: 0;
}
.playback-control .speed-control select {
padding: 4px 8px;
border-radius: 4px;
border: 1px solid #ccc;
}
.loading-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(255,255,255,0.9);
z-index: 2000;
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
}
.loading-overlay .spinner-border {
width: 3rem;
height: 3rem;
}
.moving-marker {
transition: transform 0.1s linear;
}
.marker-size-control {
position: fixed;
bottom: 90px;
right: 10px;
z-index: 999;
background: white;
padding: 10px 12px;
border-radius: 4px;
box-shadow: 0 1px 5px rgba(0,0,0,0.3);
font-size: 11px;
}
.marker-size-control label {
display: block;
margin-bottom: 5px;
font-weight: bold;
}
.marker-size-control input[type="range"] {
width: 120px;
cursor: pointer;
}
.marker-size-control .size-value {
display: inline-block;
margin-left: 5px;
font-weight: bold;
color: #007bff;
}
/* Layer Manager Panel */
.layer-manager-panel {
position: fixed;
top: 66px;
right: 10px;
z-index: 1001;
background: white;
border-radius: 6px;
box-shadow: 0 2px 10px rgba(0,0,0,0.3);
width: 320px;
max-height: calc(100vh - 180px);
display: flex;
flex-direction: column;
}
.layer-manager-header {
padding: 12px 15px;
background: #007bff;
color: white;
border-radius: 6px 6px 0 0;
display: flex;
justify-content: space-between;
align-items: center;
}
.layer-manager-header h6 {
margin: 0;
font-size: 14px;
}
.layer-manager-header .btn-close {
filter: brightness(0) invert(1);
opacity: 0.8;
}
.layer-manager-body {
padding: 10px;
overflow-y: auto;
flex: 1;
}
.layer-section {
margin-bottom: 15px;
}
.layer-section-title {
font-size: 12px;
font-weight: bold;
color: #666;
margin-bottom: 8px;
padding-bottom: 5px;
border-bottom: 1px solid #eee;
display: flex;
justify-content: space-between;
align-items: center;
}
.layer-item {
display: flex;
align-items: center;
padding: 6px 8px;
margin: 3px 0;
background: #f8f9fa;
border-radius: 4px;
font-size: 12px;
}
.layer-item.active {
background: #e3f2fd;
border: 1px solid #2196f3;
}
.layer-item input[type="checkbox"] {
margin-right: 8px;
}
.layer-item .layer-name {
flex: 1;
cursor: pointer;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.layer-item .layer-actions {
display: flex;
gap: 4px;
}
.layer-item .layer-actions button {
padding: 2px 6px;
font-size: 10px;
border: none;
border-radius: 3px;
cursor: pointer;
}
.layer-item .layer-actions .btn-edit {
background: #ffc107;
color: #000;
}
.layer-item .layer-actions .btn-delete {
background: #dc3545;
color: white;
}
.layer-item .layer-actions .btn-expand {
background: #6c757d;
color: white;
}
.layer-children {
margin-left: 20px;
display: none;
}
.layer-children.expanded {
display: block;
}
.layer-child-item {
display: flex;
align-items: center;
padding: 4px 6px;
margin: 2px 0;
background: #fff;
border-radius: 3px;
font-size: 11px;
border: 1px solid #e0e0e0;
}
.add-layer-btn {
width: 100%;
padding: 6px;
font-size: 12px;
margin-top: 5px;
}
/* Import/Export buttons */
.io-buttons {
display: flex;
gap: 5px;
padding: 10px;
border-top: 1px solid #eee;
}
.io-buttons button {
flex: 1;
padding: 6px 10px;
font-size: 11px;
border: none;
border-radius: 4px;
cursor: pointer;
}
.io-buttons .btn-import {
background: #28a745;
color: white;
}
.io-buttons .btn-export {
background: #17a2b8;
color: white;
}
/* Toggle button for layer panel */
.layer-toggle-btn {
position: fixed;
top: 66px;
right: 10px;
z-index: 1000;
background: white;
border: none;
padding: 8px 12px;
border-radius: 4px;
box-shadow: 0 1px 5px rgba(0,0,0,0.3);
cursor: pointer;
font-size: 14px;
}
.layer-toggle-btn:hover {
background: #f0f0f0;
}
/* Drawing style modal */
.style-modal {
display: none;
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
z-index: 2001;
background: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 4px 20px rgba(0,0,0,0.3);
min-width: 300px;
}
.style-modal.show {
display: block;
}
.style-modal-overlay {
display: none;
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0,0,0,0.5);
z-index: 2000;
}
.style-modal-overlay.show {
display: block;
}
.style-modal h5 {
margin: 0 0 15px 0;
font-size: 16px;
}
.style-modal .form-group {
margin-bottom: 12px;
}
.style-modal label {
display: block;
font-size: 12px;
margin-bottom: 4px;
font-weight: 500;
}
.style-modal input, .style-modal select, .style-modal textarea {
width: 100%;
padding: 6px 10px;
border: 1px solid #ccc;
border-radius: 4px;
font-size: 13px;
}
.style-modal input[type="color"] {
height: 36px;
padding: 2px;
}
.style-modal .btn-row {
display: flex;
gap: 10px;
margin-top: 15px;
}
.style-modal .btn-row button {
flex: 1;
padding: 8px;
border: none;
border-radius: 4px;
cursor: pointer;
}
.style-modal .btn-save {
background: #007bff;
color: white;
}
.style-modal .btn-cancel {
background: #6c757d;
color: white;
}
/* Custom Marker Tool Modal */
.custom-marker-modal {
display: none;
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
z-index: 2001;
background: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 4px 20px rgba(0,0,0,0.3);
min-width: 350px;
max-width: 400px;
}
.custom-marker-modal.show {
display: block;
}
.custom-marker-modal h5 {
margin: 0 0 15px 0;
font-size: 16px;
color: #333;
}
.custom-marker-modal .form-group {
margin-bottom: 12px;
}
.custom-marker-modal label {
display: block;
font-size: 12px;
margin-bottom: 4px;
font-weight: 500;
color: #555;
}
.custom-marker-modal input,
.custom-marker-modal select {
width: 100%;
padding: 8px 10px;
border: 1px solid #ccc;
border-radius: 4px;
font-size: 13px;
}
.custom-marker-modal input[type="color"] {
height: 40px;
padding: 2px;
cursor: pointer;
}
.custom-marker-modal input[type="range"] {
cursor: pointer;
}
.custom-marker-modal .range-value {
display: inline-block;
margin-left: 8px;
font-weight: bold;
color: #007bff;
min-width: 40px;
}
.custom-marker-modal .shape-preview {
display: flex;
gap: 8px;
flex-wrap: wrap;
margin-top: 8px;
}
.custom-marker-modal .shape-option {
width: 40px;
height: 40px;
border: 2px solid #ddd;
border-radius: 4px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all 0.2s;
}
.custom-marker-modal .shape-option:hover {
border-color: #007bff;
background: #f0f8ff;
}
.custom-marker-modal .shape-option.selected {
border-color: #007bff;
background: #e3f2fd;
box-shadow: 0 0 5px rgba(0,123,255,0.3);
}
.custom-marker-modal .marker-preview {
display: flex;
align-items: center;
justify-content: center;
padding: 20px;
background: #f8f9fa;
border-radius: 4px;
margin: 15px 0;
min-height: 80px;
}
.custom-marker-modal .btn-row {
display: flex;
gap: 10px;
margin-top: 15px;
}
.custom-marker-modal .btn-row button {
flex: 1;
padding: 10px;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
font-weight: 500;
}
.custom-marker-modal .btn-place {
background: #28a745;
color: white;
}
.custom-marker-modal .btn-place:hover {
background: #218838;
}
.custom-marker-modal .btn-cancel {
background: #6c757d;
color: white;
}
.custom-marker-modal .btn-cancel:hover {
background: #5a6268;
}
/* Geoman toolbar adjustments */
.leaflet-pm-toolbar {
margin-top: 10px !important;
}
/* Geoman custom marker button active state */
.leaflet-pm-icon-custom-marker.active,
.leaflet-buttons-container .leaflet-pm-action.active {
background-color: #007bff !important;
}
/* Geoman button container active state */
.leaflet-pm-actions-container .active {
background-color: #007bff !important;
}
/* Crosshair cursor when placing marker */
.marker-placement-mode {
cursor: crosshair !important;
}
.marker-placement-mode * {
cursor: crosshair !important;
}
/* Custom edit mode cursor */
.custom-edit-mode {
cursor: pointer !important;
}
.custom-edit-mode .leaflet-interactive {
cursor: pointer !important;
}
/* Custom edit mode indicator */
.custom-edit-indicator {
position: fixed;
top: 70px;
left: 50%;
transform: translateX(-50%);
z-index: 1500;
background: #28a745;
color: white;
padding: 8px 16px;
border-radius: 4px;
box-shadow: 0 2px 10px rgba(0,0,0,0.3);
font-size: 13px;
font-weight: 500;
display: none;
}
.custom-edit-indicator.active {
display: block;
}
/* Imported text marker styles */
.imported-text-marker {
background: transparent !important;
border: none !important;
}
.imported-text-marker > div {
box-shadow: 0 1px 3px rgba(0,0,0,0.2);
}

View File

@@ -0,0 +1,557 @@
// Custom Marker Tool for Leaflet Map integrated with Geoman
// Allows placing custom markers with shape, color, size, and label configuration
class CustomMarkerTool {
constructor(map, shapeMap, colorMap) {
this.map = map;
this.shapeMap = shapeMap;
this.colorMap = colorMap;
this.isActive = false;
this.pendingMarkerLatLng = null;
this.clickHandler = null;
// Default marker settings
this.settings = {
shape: 'circle',
color: 'red',
size: 1.0,
opacity: 1.0,
label: ''
};
this.init();
}
init() {
this.createGeomanControl();
this.createModal();
this.attachEventListeners();
this.setupGeomanIntegration();
}
createGeomanControl() {
// Add custom action to Geoman toolbar
const customMarkerAction = {
name: 'customMarker',
block: 'draw',
title: 'Добавить кастомный маркер',
className: 'leaflet-pm-icon-custom-marker',
toggle: true,
onClick: () => {},
afterClick: () => {
this.toggleTool();
}
};
// Add the action to Geoman
this.map.pm.Toolbar.createCustomControl(customMarkerAction);
// Add custom icon style
const style = document.createElement('style');
style.textContent = `
.leaflet-pm-icon-custom-marker {
background-image: url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9IiMzMzMiIHN0cm9rZS13aWR0aD0iMiIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIj48cGF0aCBkPSJNMjEgMTBjMCA3LTkgMTMtOSAxM3MtOS02LTktMTNhOSA5IDAgMCAxIDE4IDB6Ij48L3BhdGg+PGNpcmNsZSBjeD0iMTIiIGN5PSIxMCIgcj0iMyI+PC9jaXJjbGU+PC9zdmc+');
background-size: 18px 18px;
background-position: center;
background-repeat: no-repeat;
}
`;
document.head.appendChild(style);
}
createModal() {
const overlay = document.createElement('div');
overlay.id = 'customMarkerModalOverlay';
overlay.className = 'style-modal-overlay';
const modal = document.createElement('div');
modal.id = 'customMarkerModal';
modal.className = 'custom-marker-modal';
modal.innerHTML = `
<h5><i class="bi bi-geo-alt-fill"></i> Настройка маркера</h5>
<div class="form-group">
<label for="markerLabel">Подпись маркера:</label>
<input type="text" id="markerLabel" placeholder="Введите подпись (необязательно)">
</div>
<div class="form-group">
<label>Форма маркера:</label>
<div class="shape-preview" id="shapePreview"></div>
</div>
<div class="form-group">
<label for="markerColor">Цвет маркера:</label>
<select id="markerColor"></select>
</div>
<div class="form-group">
<label for="markerSize">Размер: <span class="range-value" id="markerSizeValue">1.0x</span></label>
<input type="range" id="markerSize" min="0.5" max="3" step="0.1" value="1.0">
</div>
<div class="form-group">
<label for="markerOpacity">Прозрачность: <span class="range-value" id="markerOpacityValue">100%</span></label>
<input type="range" id="markerOpacity" min="0" max="1" step="0.1" value="1.0">
</div>
<div class="marker-preview" id="markerPreview">
<div id="previewIcon"></div>
</div>
<div class="btn-row">
<button class="btn-cancel" id="customMarkerCancel">Отмена</button>
<button class="btn-place" id="customMarkerPlace">Разместить на карте</button>
</div>
`;
document.body.appendChild(overlay);
document.body.appendChild(modal);
this.modal = modal;
this.overlay = overlay;
this.populateShapes();
this.populateColors();
this.updatePreview();
}
populateShapes() {
const shapePreview = document.getElementById('shapePreview');
const shapes = Object.keys(this.shapeMap);
shapes.forEach(shape => {
const option = document.createElement('div');
option.className = 'shape-option';
option.dataset.shape = shape;
option.innerHTML = this.shapeMap[shape]('#666', 24);
option.title = this.getShapeName(shape);
if (shape === this.settings.shape) {
option.classList.add('selected');
}
option.addEventListener('click', () => {
document.querySelectorAll('.shape-option').forEach(el => el.classList.remove('selected'));
option.classList.add('selected');
this.settings.shape = shape;
this.updatePreview();
});
shapePreview.appendChild(option);
});
}
populateColors() {
const colorSelect = document.getElementById('markerColor');
Object.keys(this.colorMap).forEach(colorName => {
const option = document.createElement('option');
option.value = colorName;
option.textContent = this.getColorName(colorName);
option.style.color = this.colorMap[colorName];
if (colorName === this.settings.color) {
option.selected = true;
}
colorSelect.appendChild(option);
});
}
getShapeName(shape) {
const names = {
'circle': 'Круг',
'square': 'Квадрат',
'triangle': 'Треугольник',
'star': 'Звезда',
'pentagon': 'Пятиугольник',
'hexagon': 'Шестиугольник',
'diamond': 'Ромб',
'cross': 'Крест'
};
return names[shape] || shape;
}
getColorName(color) {
const names = {
'red': 'Красный',
'blue': 'Синий',
'green': 'Зелёный',
'purple': 'Фиолетовый',
'orange': 'Оранжевый',
'cyan': 'Голубой',
'magenta': 'Пурпурный',
'pink': 'Розовый',
'teal': 'Бирюзовый',
'indigo': 'Индиго',
'brown': 'Коричневый',
'navy': 'Тёмно-синий',
'maroon': 'Бордовый',
'olive': 'Оливковый',
'coral': 'Коралловый',
'turquoise': 'Бирюзовый'
};
return names[color] || color;
}
attachEventListeners() {
// Size slider
const sizeSlider = document.getElementById('markerSize');
const sizeValue = document.getElementById('markerSizeValue');
sizeSlider.addEventListener('input', () => {
this.settings.size = parseFloat(sizeSlider.value);
sizeValue.textContent = this.settings.size.toFixed(1) + 'x';
this.updatePreview();
});
// Opacity slider
const opacitySlider = document.getElementById('markerOpacity');
const opacityValue = document.getElementById('markerOpacityValue');
opacitySlider.addEventListener('input', () => {
this.settings.opacity = parseFloat(opacitySlider.value);
opacityValue.textContent = Math.round(this.settings.opacity * 100) + '%';
this.updatePreview();
});
// Color select
const colorSelect = document.getElementById('markerColor');
colorSelect.addEventListener('change', () => {
this.settings.color = colorSelect.value;
this.updatePreview();
});
// Label input
const labelInput = document.getElementById('markerLabel');
labelInput.addEventListener('input', () => {
this.settings.label = labelInput.value;
});
// Modal buttons
document.getElementById('customMarkerCancel').addEventListener('click', () => {
this.closeModal();
});
document.getElementById('customMarkerPlace').addEventListener('click', () => {
this.startPlacement();
});
this.overlay.addEventListener('click', () => {
this.closeModal();
});
}
updatePreview() {
const previewIcon = document.getElementById('previewIcon');
const hexColor = this.colorMap[this.settings.color] || this.settings.color;
const size = Math.round(20 * this.settings.size);
const shapeFunc = this.shapeMap[this.settings.shape];
if (shapeFunc) {
previewIcon.innerHTML = shapeFunc(hexColor, size);
previewIcon.style.opacity = this.settings.opacity;
}
}
setupGeomanIntegration() {
// Listen to other Geoman tools to deactivate custom marker tool
this.map.on('pm:globaldrawmodetoggled', (e) => {
if (e.enabled && e.shape !== 'customMarker' && this.isActive) {
this.deactivate();
}
});
this.map.on('pm:globaleditmodetoggled', (e) => {
if (e.enabled && this.isActive) {
this.deactivate();
}
});
this.map.on('pm:globaldragmodetoggled', (e) => {
if (e.enabled && this.isActive) {
this.deactivate();
}
});
this.map.on('pm:globalremovalmodetoggled', (e) => {
if (e.enabled && this.isActive) {
this.deactivate();
}
});
// Listen to custom edit mode toggle
this.map.on('customeditmodetoggled', (e) => {
if (e.enabled && this.isActive) {
this.deactivate();
}
});
}
toggleTool() {
if (this.isActive) {
this.deactivate();
} else {
this.activate();
}
}
activate() {
if (this.isActive) return; // Prevent double activation
this.isActive = true;
// Disable all other Geoman tools (without triggering events that cause recursion)
this.map.pm.disableDraw();
this.map.pm.disableGlobalEditMode();
this.map.pm.disableGlobalDragMode();
this.map.pm.disableGlobalRemovalMode();
// Disable custom edit mode
if (window.customEditModeActive) {
window.customEditModeActive = false;
const editBtn = document.querySelector('.leaflet-pm-icon-custom-edit');
if (editBtn && editBtn.parentElement) {
editBtn.parentElement.classList.remove('active');
}
const indicator = document.getElementById('customEditIndicator');
if (indicator) {
indicator.classList.remove('active');
}
}
// Toggle Geoman button state
const customBtn = document.querySelector('.leaflet-pm-icon-custom-marker');
if (customBtn && customBtn.parentElement) {
customBtn.parentElement.classList.add('active');
}
this.showModal();
}
deactivate() {
if (!this.isActive) return; // Prevent double deactivation
this.isActive = false;
// Toggle Geoman button state
const customBtn = document.querySelector('.leaflet-pm-icon-custom-marker');
if (customBtn && customBtn.parentElement) {
customBtn.parentElement.classList.remove('active');
}
this.closeModal();
this.cancelPlacement();
}
showModal() {
this.overlay.classList.add('show');
this.modal.classList.add('show');
// Reset form
document.getElementById('markerLabel').value = this.settings.label;
document.getElementById('markerSize').value = this.settings.size;
document.getElementById('markerOpacity').value = this.settings.opacity;
document.getElementById('markerSizeValue').textContent = this.settings.size.toFixed(1) + 'x';
document.getElementById('markerOpacityValue').textContent = Math.round(this.settings.opacity * 100) + '%';
}
closeModal() {
this.overlay.classList.remove('show');
this.modal.classList.remove('show');
this.deactivate();
}
startPlacement() {
this.overlay.classList.remove('show');
this.modal.classList.remove('show');
// Add crosshair cursor
this.map.getContainer().classList.add('marker-placement-mode');
// Remove previous click handler if exists
if (this.clickHandler) {
this.map.off('click', this.clickHandler);
}
// Create new click handler
this.clickHandler = (e) => {
// Prevent event from bubbling
L.DomEvent.stopPropagation(e);
this.placeMarker(e.latlng);
};
// Wait for map click
this.map.on('click', this.clickHandler);
// Show instruction
this.showInstruction('Кликните на карту для размещения маркера. ESC для отмены.');
// Add keyboard handlers
this.keyHandler = (e) => {
if (e.key === 'Escape') {
this.deactivate();
} else if (e.key === 'Enter' && this.pendingMarkerLatLng) {
this.placeMarker(this.pendingMarkerLatLng);
}
};
document.addEventListener('keydown', this.keyHandler);
}
cancelPlacement() {
this.map.getContainer().classList.remove('marker-placement-mode');
// Remove click handler
if (this.clickHandler) {
this.map.off('click', this.clickHandler);
this.clickHandler = null;
}
// Remove keyboard handler
if (this.keyHandler) {
document.removeEventListener('keydown', this.keyHandler);
this.keyHandler = null;
}
this.hideInstruction();
}
placeMarker(latlng) {
// Cancel placement mode
this.cancelPlacement();
const hexColor = this.colorMap[this.settings.color] || this.settings.color;
const baseSize = 16;
const size = Math.round(baseSize * this.settings.size);
const shapeFunc = this.shapeMap[this.settings.shape];
const icon = L.divIcon({
className: 'custom-placed-marker',
iconSize: [size, size],
iconAnchor: [size/2, size/2],
popupAnchor: [0, -size/2],
html: `<div style="opacity: ${this.settings.opacity}">${shapeFunc(hexColor, size)}</div>`
});
const marker = L.marker(latlng, { icon: icon });
// Add popup with label if provided
if (this.settings.label) {
marker.bindPopup(`<b>${this.settings.label}</b>`);
marker.bindTooltip(this.settings.label, { permanent: false, direction: 'top' });
}
// Add to active drawing layer or create new one
if (window.activeDrawingLayerId && window.drawingLayers[window.activeDrawingLayerId]) {
// Capture layerId in closure - important for click handler
const layerId = window.activeDrawingLayerId;
const activeLayer = window.drawingLayers[layerId];
activeLayer.layerGroup.addLayer(marker);
// Store element info
const elementInfo = {
layer: marker,
visible: true,
label: this.settings.label,
style: {
color: hexColor,
shape: this.settings.shape,
size: this.settings.size,
opacity: this.settings.opacity
}
};
activeLayer.elements.push(elementInfo);
// Add click handler for editing ONLY in custom edit mode
// Use captured layerId, not window.activeDrawingLayerId
marker.on('click', function(e) {
// Check if custom edit mode is active (from global scope)
if (window.customEditModeActive) {
L.DomEvent.stopPropagation(e);
// Find element in the layer where it was added
const layer = window.drawingLayers[layerId];
if (layer) {
const idx = layer.elements.findIndex(el => el.layer === marker);
if (idx !== -1 && window.openStyleModal) {
window.openStyleModal(layerId, idx);
}
}
}
});
// Update layer panel
if (window.renderDrawingLayers) {
window.renderDrawingLayers();
}
} else {
// Fallback: add directly to map
marker.addTo(this.map);
}
// Deactivate tool after placing marker
this.deactivate();
// Fire custom event
this.map.fire('custommarker:created', { marker: marker, settings: { ...this.settings } });
}
showInstruction(text) {
let instruction = document.getElementById('markerPlacementInstruction');
if (!instruction) {
instruction = document.createElement('div');
instruction.id = 'markerPlacementInstruction';
instruction.style.cssText = `
position: fixed;
top: 70px;
left: 50%;
transform: translateX(-50%);
z-index: 1500;
background: #007bff;
color: white;
padding: 10px 20px;
border-radius: 4px;
box-shadow: 0 2px 10px rgba(0,0,0,0.3);
font-size: 14px;
font-weight: 500;
display: flex;
align-items: center;
gap: 10px;
`;
document.body.appendChild(instruction);
// Prevent map interactions on instruction
L.DomEvent.disableClickPropagation(instruction);
L.DomEvent.disableScrollPropagation(instruction);
}
instruction.innerHTML = `
<span>${text}</span>
<button id="finishMarkerBtn" style="
background: white;
color: #007bff;
border: none;
padding: 4px 12px;
border-radius: 3px;
cursor: pointer;
font-weight: 500;
font-size: 12px;
">Отмена (ESC)</button>
`;
instruction.style.display = 'flex';
// Add finish button handler
const finishBtn = document.getElementById('finishMarkerBtn');
if (finishBtn) {
finishBtn.addEventListener('click', () => {
this.deactivate();
});
}
}
hideInstruction() {
const instruction = document.getElementById('markerPlacementInstruction');
if (instruction) {
instruction.style.display = 'none';
}
}
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,16 @@
/*! @preserve
* Leaflet Panel Layers v1.3.1 - 2022-11-18
*
* Copyright 2022 Stefano Cudini
* stefano.cudini@gmail.com
* https://opengeo.tech/
*
* Licensed under the MIT license.
*
* Demos:
* https://opengeo.tech/maps/leaflet-panel-layers/
*
* Source:
* git@github.com:stefanocudini/leaflet-panel-layers.git
*/
.leaflet-panel-layers .leaflet-panel-layers-list{display:block}.leaflet-panel-layers.expanded .leaflet-panel-layers-list{display:block}.leaflet-top.leaflet-right .leaflet-panel-layers:not(.compact){margin:0}.leaflet-panel-layers{width:30px;min-width:30px}.leaflet-panel-layers.expanded{width:auto;overflow-x:hidden;overflow-y:auto}.leaflet-panel-layers.expanded .leaflet-panel-layers-list{display:block}.leaflet-panel-layers:not(.expanded) .leaflet-panel-layers-grouplabel,.leaflet-panel-layers:not(.expanded) .leaflet-panel-layers-selector,.leaflet-panel-layers:not(.expanded) .leaflet-panel-layers-title>span{display:none}.leaflet-panel-layers-separator{clear:both}.leaflet-panel-layers-item .leaflet-panel-layers-title{display:block;white-space:nowrap;float:none;cursor:pointer}.leaflet-panel-layers-title .leaflet-panel-layers-selector{float:right}.leaflet-panel-layers-group{position:relative;width:auto;height:auto;clear:both;overflow:hidden}.leaflet-panel-layers-icon{text-align:center;float:left}.leaflet-panel-layers-group.collapsible:not(.expanded){height:20px}.leaflet-panel-layers-group.collapsible:not(.expanded) .leaflet-panel-layers-grouplabel{height:20px;overflow:hidden}.leaflet-panel-layers-group.collapsible:not(.expanded) .leaflet-panel-layers-item{display:none}.leaflet-panel-layers-group.collapsible .leaflet-panel-layers-grouplabel{display:block;cursor:pointer;-webkit-touch-callout:none;-webkit-user-select:none;-khtml-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.leaflet-panel-layers-item{display:block;height:auto;clear:both;white-space:nowrap;-webkit-touch-callout:none;-webkit-user-select:none;-khtml-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.leaflet-panel-layers-overlays .leaflet-panel-layers-item{white-space:pre-wrap;white-space:-moz-pre-wrap;white-space:-pre-wrap;white-space:-o-pre-wrap;word-wrap:break-word;width:auto;display:block}.leaflet-panel-layers-base .leaflet-panel-layers-selector{float:left}.leaflet-panel-layers-overlays .leaflet-panel-layers-selector{float:right}.leaflet-panel-layers.expanded .leaflet-panel-layers-overlays input{display:block}.leaflet-control-layers-selector{float:left}.leaflet-panel-layers-grouplabel .leaflet-panel-layers-selector{visibility:hidden;position:absolute;top:1px;right:7px}.leaflet-panel-layers-group:hover .leaflet-panel-layers-selector{visibility:visible}.leaflet-panel-layers{padding:4px;background:rgba(255,255,255,.5);box-shadow:-2px 0 8px rgba(0,0,0,.3)}.leaflet-panel-layers.expanded{padding:4px}.leaflet-panel-layers-selector{position:relative;top:1px;margin-top:2px}.leaflet-panel-layers-separator{height:8px;margin:12px 4px 0 4px;border-top:1px solid rgba(0,0,0,.3)}.leaflet-panel-layers-item{min-height:20px}.leaflet-panel-layers-margin{height:25px}.leaflet-panel-layers-icon{line-height:20px;display:inline-block;height:20px;width:20px;background:#fff}.leaflet-panel-layers-group.collapsible .leaflet-panel-layers-icon:first-child{min-width:20px;font-size:16px;text-align:center;background:0 0}.leaflet-panel-layers-group{padding:2px 4px;margin-bottom:4px;border:1px solid rgba(0,0,0,.3);background:rgba(255,255,255,.6);border-radius:3px}.leaflet-panel-layers-overlays .leaflet-panel-layers-item{margin-bottom:4px;padding:2px;background:#fff;border:1px solid rgba(0,0,0,.3);border-radius:4px}.leaflet-panel-layers-overlays .leaflet-panel-layers-item:hover{border:1px solid #888;cursor:pointer}

File diff suppressed because one or more lines are too long

View File

@@ -64,5 +64,12 @@ def test_celery_connection():
print("=" * 60) print("=" * 60)
return True return True
if __name__ == "__main__": # if __name__ == "__main__":
test_celery_connection() # test_celery_connection()
import requests
url = f"https://www.lyngsat.com/europe.html"
payload = {"cmd": "request.get", "url": url, "maxTimeout": 60000}
response = requests.post("http://localhost:8191/v1", json=payload)
print(response.content)