Files
dbstorage/dbapp/mainapp/templates/mainapp/source_map.html

682 lines
24 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

{% extends "mainapp/base.html" %}
{% load static %}
{% block title %}Карта объектов{% endblock title %}
{% block extra_css %}
<!-- MapLibre GL CSS -->
<link href="{% static 'maplibre/maplibre-gl.css' %}" rel="stylesheet">
<style>
body {
overflow: hidden;
}
#map {
position: fixed;
top: 56px;
bottom: 0;
left: 0;
right: 0;
z-index: 0;
}
/* Легенда */
.maplibregl-ctrl-legend {
background: white;
padding: 10px;
border-radius: 4px;
box-shadow: 0 0 0 2px rgba(0,0,0,.1);
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
font-size: 11px;
max-height: 400px;
overflow-y: auto;
}
.maplibregl-ctrl-legend h6 {
font-size: 12px;
margin: 0 0 8px 0;
font-weight: bold;
}
.legend-item {
margin: 4px 0;
display: flex;
align-items: center;
padding: 2px;
}
.legend-marker {
width: 12px;
height: 12px;
margin-right: 8px;
border-radius: 50%;
border: 2px solid white;
box-shadow: 0 0 3px rgba(0,0,0,0.3);
}
.legend-section {
margin-bottom: 8px;
padding-bottom: 6px;
border-bottom: 1px solid #ddd;
}
.legend-section:last-child {
border-bottom: none;
margin-bottom: 0;
}
/* Слои контрол */
.maplibregl-ctrl-layers {
background: white;
padding: 10px;
border-radius: 4px;
box-shadow: 0 0 0 2px rgba(0,0,0,.1);
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
max-width: 250px;
max-height: 400px;
overflow-y: auto;
}
.maplibregl-ctrl-layers h6 {
margin: 0 0 10px 0;
font-size: 13px;
font-weight: bold;
}
.layer-item {
margin: 5px 0;
font-size: 12px;
}
.layer-item label {
cursor: pointer;
display: flex;
align-items: center;
gap: 5px;
user-select: none;
}
.layer-item input[type="checkbox"] {
cursor: pointer;
}
/* Кастомные кнопки контролов */
.maplibregl-ctrl-projection .maplibregl-ctrl-icon,
.maplibregl-ctrl-3d .maplibregl-ctrl-icon,
.maplibregl-ctrl-style .maplibregl-ctrl-icon {
background-size: 20px 20px;
background-position: center;
background-repeat: no-repeat;
}
.maplibregl-ctrl-projection .maplibregl-ctrl-icon {
background-image: url('data:image/svg+xml;charset=utf-8,<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20"><circle cx="10" cy="10" r="6" fill="none" stroke="%23333" stroke-width="1.5"/><ellipse cx="10" cy="10" rx="3" ry="6" fill="none" stroke="%23333" stroke-width="1.5"/><line x1="4" y1="10" x2="16" y2="10" stroke="%23333" stroke-width="1.5"/></svg>');
}
.maplibregl-ctrl-3d .maplibregl-ctrl-icon {
background-image: url('data:image/svg+xml;charset=utf-8,<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20"><path d="M 5 13 L 5 8 L 10 5 L 15 8 L 15 13 L 10 16 Z M 5 8 L 10 10.5 M 10 10.5 L 15 8 M 10 10.5 L 10 16" fill="none" stroke="%23333" stroke-width="1.5" stroke-linejoin="round"/></svg>');
}
.maplibregl-ctrl-style .maplibregl-ctrl-icon {
background-image: url('data:image/svg+xml;charset=utf-8,<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20"><rect x="3" y="3" width="6" height="6" fill="none" stroke="%23333" stroke-width="1.5"/><rect x="11" y="3" width="6" height="6" fill="none" stroke="%23333" stroke-width="1.5"/><rect x="3" y="11" width="6" height="6" fill="none" stroke="%23333" stroke-width="1.5"/><rect x="11" y="11" width="6" height="6" fill="none" stroke="%23333" stroke-width="1.5"/></svg>');
}
/* Popup стили */
.maplibregl-popup-content {
padding: 10px 15px;
min-width: 150px;
}
.popup-title {
font-weight: bold;
margin-bottom: 5px;
font-size: 13px;
}
.popup-info {
font-size: 12px;
color: #666;
}
/* Стили меню */
.style-menu {
background: white;
border-radius: 4px;
box-shadow: 0 0 0 2px rgba(0,0,0,.1);
padding: 5px;
min-width: 120px;
}
.style-menu button {
display: block;
width: 100%;
padding: 6px 10px;
border: none;
background: none;
text-align: left;
cursor: pointer;
font-size: 12px;
border-radius: 2px;
}
.style-menu button:hover {
background-color: #f0f0f0;
}
.style-menu button.active {
background-color: #e3f2fd;
color: #1976d2;
font-weight: 500;
}
</style>
{% endblock %}
{% block content %}
<div id="map"></div>
{% endblock content %}
{% block extra_js %}
<!-- MapLibre GL JavaScript -->
<script src="{% static 'maplibre/maplibre-gl.js' %}"></script>
<script>
// Цвета для маркеров
const markerColors = {
'blue': '#3388ff',
'orange': '#ff8c00',
'green': '#28a745',
'violet': '#9c27b0'
};
// Данные групп из Django
const groups = [
{% for group in groups %}
{
name: '{{ group.name|escapejs }}',
color: '{{ group.color }}',
points: [
{% for point_data in group.points %}
{
id: '{{ point_data.source_id|escapejs }}',
coordinates: [{{ point_data.point.0|safe }}, {{ point_data.point.1|safe }}]
}{% if not forloop.last %},{% endif %}
{% endfor %}
]
}{% if not forloop.last %},{% endif %}
{% endfor %}
];
// Полигон фильтра
const polygonCoords = {% if polygon_coords %}{{ polygon_coords|safe }}{% else %}null{% endif %};
// Стили карт
const mapStyles = {
streets: {
version: 8,
sources: {
'osm': {
type: 'raster',
tiles: ['https://a.tile.openstreetmap.org/{z}/{x}/{y}.png'],
tileSize: 256,
attribution: '&copy; OpenStreetMap'
}
},
layers: [{
id: 'osm',
type: 'raster',
source: 'osm'
}]
},
satellite: {
version: 8,
sources: {
'satellite': {
type: 'raster',
tiles: ['https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}'],
tileSize: 256,
attribution: '&copy; Esri'
}
},
layers: [{
id: 'satellite',
type: 'raster',
source: 'satellite'
}]
},
local: {
version: 8,
sources: {
'local': {
type: 'raster',
tiles: ['http://127.0.0.1:8080/styles/basic-preview/{z}/{x}/{y}.png'],
tileSize: 256,
attribution: 'Local'
}
},
layers: [{
id: 'local',
type: 'raster',
source: 'local'
}]
}
};
// Инициализация карты
const map = new maplibregl.Map({
container: 'map',
style: mapStyles.streets,
center: [37.62, 55.75],
zoom: 10,
maxZoom: 18,
minZoom: 0
});
let currentStyle = 'streets';
let is3DEnabled = false;
let isGlobeProjection = false;
const allMarkers = [];
// Добавляем стандартные контролы
map.addControl(new maplibregl.NavigationControl(), 'top-right');
map.addControl(new maplibregl.ScaleControl({ unit: 'metric' }), 'bottom-right');
map.addControl(new maplibregl.FullscreenControl(), 'top-right');
// Кастомный контрол для переключения проекции
class ProjectionControl {
onAdd(map) {
this._map = map;
this._container = document.createElement('div');
this._container.className = 'maplibregl-ctrl maplibregl-ctrl-group maplibregl-ctrl-projection';
this._container.innerHTML = '<button type="button" title="Переключить проекцию"><span class="maplibregl-ctrl-icon"></span></button>';
this._container.onclick = () => {
if (isGlobeProjection) {
map.setProjection({ type: 'mercator' });
isGlobeProjection = false;
} else {
map.setProjection({ type: 'globe' });
isGlobeProjection = true;
}
};
return this._container;
}
onRemove() {
this._container.parentNode.removeChild(this._container);
this._map = undefined;
}
}
// Кастомный контрол для переключения стилей
class StyleControl {
onAdd(map) {
this._map = map;
this._container = document.createElement('div');
this._container.className = 'maplibregl-ctrl maplibregl-ctrl-group maplibregl-ctrl-style';
const button = document.createElement('button');
button.type = 'button';
button.title = 'Стиль карты';
button.innerHTML = '<span class="maplibregl-ctrl-icon"></span>';
const menu = document.createElement('div');
menu.className = 'style-menu';
menu.style.display = 'none';
menu.style.position = 'absolute';
menu.style.top = '100%';
menu.style.right = '0';
menu.style.marginTop = '5px';
const styles = [
{ id: 'streets', name: 'Улицы' },
{ id: 'satellite', name: 'Спутник' },
{ id: 'local', name: 'Локально' }
];
styles.forEach(style => {
const btn = document.createElement('button');
btn.textContent = style.name;
btn.className = style.id === currentStyle ? 'active' : '';
btn.onclick = () => {
this.switchStyle(style.id);
menu.querySelectorAll('button').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
menu.style.display = 'none';
};
menu.appendChild(btn);
});
button.onclick = (e) => {
e.stopPropagation();
menu.style.display = menu.style.display === 'none' ? 'block' : 'none';
};
document.addEventListener('click', () => {
menu.style.display = 'none';
});
this._container.appendChild(button);
this._container.appendChild(menu);
this._container.style.position = 'relative';
return this._container;
}
switchStyle(styleName) {
const center = this._map.getCenter();
const zoom = this._map.getZoom();
const bearing = this._map.getBearing();
const pitch = this._map.getPitch();
this._map.setStyle(mapStyles[styleName]);
currentStyle = styleName;
this._map.once('styledata', () => {
this._map.setCenter(center);
this._map.setZoom(zoom);
this._map.setBearing(bearing);
this._map.setPitch(pitch);
addMarkersToMap();
addFilterPolygon();
});
}
onRemove() {
this._container.parentNode.removeChild(this._container);
this._map = undefined;
}
}
// Кастомный контрол для слоев
class LayersControl {
onAdd(map) {
this._map = map;
this._container = document.createElement('div');
this._container.className = 'maplibregl-ctrl maplibregl-ctrl-layers';
const title = document.createElement('h6');
title.textContent = 'Слои точек';
this._container.appendChild(title);
groups.forEach((group, groupIndex) => {
const layerId = `points-layer-${groupIndex}`;
const layerItem = document.createElement('div');
layerItem.className = 'layer-item';
const label = document.createElement('label');
const checkbox = document.createElement('input');
checkbox.type = 'checkbox';
checkbox.checked = true;
checkbox.addEventListener('change', (e) => {
const visibility = e.target.checked ? 'visible' : 'none';
if (map.getLayer(layerId)) {
map.setLayoutProperty(layerId, 'visibility', visibility);
}
// Также скрываем/показываем внутренний круг
const innerLayerId = `${layerId}-inner`;
if (map.getLayer(innerLayerId)) {
map.setLayoutProperty(innerLayerId, 'visibility', visibility);
}
});
const colorSpan = document.createElement('span');
colorSpan.className = 'legend-marker';
colorSpan.style.backgroundColor = markerColors[group.color];
const nameSpan = document.createElement('span');
nameSpan.textContent = `${group.name} (${group.points.length})`;
label.appendChild(checkbox);
label.appendChild(colorSpan);
label.appendChild(nameSpan);
layerItem.appendChild(label);
this._container.appendChild(layerItem);
});
return this._container;
}
onRemove() {
this._container.parentNode.removeChild(this._container);
this._map = undefined;
}
}
// Кастомный контрол для легенды
class LegendControl {
onAdd(map) {
this._map = map;
this._container = document.createElement('div');
this._container.className = 'maplibregl-ctrl maplibregl-ctrl-legend';
const title = document.createElement('h6');
title.textContent = 'Легенда';
this._container.appendChild(title);
groups.forEach(group => {
const section = document.createElement('div');
section.className = 'legend-section';
const item = document.createElement('div');
item.className = 'legend-item';
const marker = document.createElement('div');
marker.className = 'legend-marker';
marker.style.backgroundColor = markerColors[group.color];
const text = document.createElement('span');
text.textContent = `${group.name} (${group.points.length})`;
item.appendChild(marker);
item.appendChild(text);
section.appendChild(item);
this._container.appendChild(section);
});
if (polygonCoords && polygonCoords.length > 0) {
const section = document.createElement('div');
section.className = 'legend-section';
const item = document.createElement('div');
item.className = 'legend-item';
const marker = document.createElement('div');
marker.style.width = '18px';
marker.style.height = '18px';
marker.style.marginRight = '8px';
marker.style.backgroundColor = 'rgba(51, 136, 255, 0.2)';
marker.style.border = '2px solid #3388ff';
marker.style.borderRadius = '2px';
const text = document.createElement('span');
text.textContent = 'Область фильтра';
item.appendChild(marker);
item.appendChild(text);
section.appendChild(item);
this._container.appendChild(section);
}
return this._container;
}
onRemove() {
this._container.parentNode.removeChild(this._container);
this._map = undefined;
}
}
// Добавляем кастомные контролы
map.addControl(new ProjectionControl(), 'top-right');
map.addControl(new StyleControl(), 'top-right');
map.addControl(new LayersControl(), 'top-left');
map.addControl(new LegendControl(), 'bottom-left');
// Добавление маркеров на карту
function addMarkersToMap() {
groups.forEach((group, groupIndex) => {
const sourceId = `points-${groupIndex}`;
const layerId = `points-layer-${groupIndex}`;
// Создаем GeoJSON для группы
const geojson = {
type: 'FeatureCollection',
features: group.points.map(point => ({
type: 'Feature',
geometry: {
type: 'Point',
coordinates: point.coordinates
},
properties: {
id: point.id,
groupName: group.name,
color: markerColors[group.color]
}
}))
};
// Добавляем источник данных
if (!map.getSource(sourceId)) {
map.addSource(sourceId, {
type: 'geojson',
data: geojson
});
}
// Добавляем слой с кругами (основной маркер)
if (!map.getLayer(layerId)) {
map.addLayer({
id: layerId,
type: 'circle',
source: sourceId,
paint: {
'circle-radius': 10,
'circle-color': ['get', 'color'],
'circle-stroke-width': 3,
'circle-stroke-color': '#ffffff',
'circle-opacity': 1
}
});
// Добавляем внутренний круг
map.addLayer({
id: `${layerId}-inner`,
type: 'circle',
source: sourceId,
paint: {
'circle-radius': 4,
'circle-color': '#ffffff',
'circle-opacity': 1
}
});
// Добавляем popup при клике
map.on('click', layerId, (e) => {
const coordinates = e.features[0].geometry.coordinates.slice();
const { id, groupName } = e.features[0].properties;
new maplibregl.Popup()
.setLngLat(coordinates)
.setHTML(`<div class="popup-title">${id}</div><div class="popup-info">Группа: ${groupName}</div>`)
.addTo(map);
});
// Меняем курсор при наведении
map.on('mouseenter', layerId, () => {
map.getCanvas().style.cursor = 'pointer';
});
map.on('mouseleave', layerId, () => {
map.getCanvas().style.cursor = '';
});
}
});
}
// Добавление полигона фильтра
function addFilterPolygon() {
if (!polygonCoords || polygonCoords.length === 0) return;
try {
const polygonGeoJSON = {
type: 'Feature',
geometry: {
type: 'Polygon',
coordinates: [polygonCoords]
}
};
if (!map.getSource('filter-polygon')) {
map.addSource('filter-polygon', {
type: 'geojson',
data: polygonGeoJSON
});
}
if (!map.getLayer('filter-polygon-fill')) {
map.addLayer({
id: 'filter-polygon-fill',
type: 'fill',
source: 'filter-polygon',
paint: {
'fill-color': '#3388ff',
'fill-opacity': 0.2
}
});
}
if (!map.getLayer('filter-polygon-outline')) {
map.addLayer({
id: 'filter-polygon-outline',
type: 'line',
source: 'filter-polygon',
paint: {
'line-color': '#3388ff',
'line-width': 2,
'line-dasharray': [2, 2]
}
});
}
map.on('click', 'filter-polygon-fill', (e) => {
new maplibregl.Popup()
.setLngLat(e.lngLat)
.setHTML('<div class="popup-title">Область фильтра</div><div class="popup-info">Отображаются только источники с точками в этой области</div>')
.addTo(map);
});
} catch (e) {
console.error('Ошибка при отображении полигона фильтра:', e);
}
}
// Подгонка карты под все маркеры
function fitMapToBounds() {
const allCoordinates = [];
groups.forEach(group => {
group.points.forEach(point => {
allCoordinates.push(point.coordinates);
});
});
if (polygonCoords && polygonCoords.length > 0) {
polygonCoords.forEach(coord => {
allCoordinates.push(coord);
});
}
if (allCoordinates.length > 0) {
const bounds = allCoordinates.reduce((bounds, coord) => {
return bounds.extend(coord);
}, new maplibregl.LngLatBounds(allCoordinates[0], allCoordinates[0]));
map.fitBounds(bounds, { padding: 50 });
}
}
// Инициализация после загрузки карты
map.on('load', () => {
addMarkersToMap();
addFilterPolygon();
fitMapToBounds();
});
</script>
{% endblock extra_js %}