Сделал 1 карту на LibreMap
This commit is contained in:
@@ -3,46 +3,169 @@
|
|||||||
{% block title %}Карта объектов{% endblock title %}
|
{% block title %}Карта объектов{% endblock title %}
|
||||||
|
|
||||||
{% block extra_css %}
|
{% block extra_css %}
|
||||||
<!-- Leaflet CSS -->
|
<!-- MapLibre GL CSS -->
|
||||||
<link href="{% static 'leaflet/leaflet.css' %}" rel="stylesheet">
|
<link href="{% static 'maplibre/maplibre-gl.css' %}" rel="stylesheet">
|
||||||
<link href="{% static 'leaflet-measure/leaflet-measure.css' %}" rel="stylesheet">
|
|
||||||
<link href="{% static 'leaflet-tree/L.Control.Layers.Tree.css' %}" rel="stylesheet">
|
|
||||||
<style>
|
<style>
|
||||||
body {
|
body {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
#map {
|
#map {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
top: 56px; /* Высота navbar */
|
top: 56px;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
z-index: 1;
|
z-index: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.legend {
|
/* Легенда */
|
||||||
|
.maplibregl-ctrl-legend {
|
||||||
background: white;
|
background: white;
|
||||||
padding: 8px;
|
padding: 10px;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
box-shadow: 0 0 10px rgba(0,0,0,0.2);
|
box-shadow: 0 0 0 2px rgba(0,0,0,.1);
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
|
max-height: 400px;
|
||||||
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
.legend h6 {
|
|
||||||
|
.maplibregl-ctrl-legend h6 {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
margin: 0 0 6px 0;
|
margin: 0 0 8px 0;
|
||||||
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
.legend-item {
|
.legend-item {
|
||||||
margin: 3px 0;
|
margin: 4px 0;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
padding: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.legend-marker {
|
.legend-marker {
|
||||||
width: 18px;
|
width: 12px;
|
||||||
height: 30px;
|
height: 12px;
|
||||||
margin-right: 6px;
|
margin-right: 8px;
|
||||||
background-size: contain;
|
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;
|
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>
|
</style>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
@@ -51,181 +174,567 @@
|
|||||||
{% endblock content %}
|
{% endblock content %}
|
||||||
|
|
||||||
{% block extra_js %}
|
{% block extra_js %}
|
||||||
<!-- Leaflet JavaScript -->
|
<!-- MapLibre GL JavaScript -->
|
||||||
<script src="{% static 'leaflet/leaflet.js' %}"></script>
|
<script src="{% static 'maplibre/maplibre-gl.js' %}"></script>
|
||||||
<script src="{% static 'leaflet-measure/leaflet-measure.ru.js' %}"></script>
|
|
||||||
<script src="{% static 'leaflet-tree/L.Control.Layers.Tree.js' %}"></script>
|
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
// Инициализация карты
|
|
||||||
let map = L.map('map').setView([55.75, 37.62], 10);
|
|
||||||
L.control.scale({
|
|
||||||
imperial: false,
|
|
||||||
metric: true
|
|
||||||
}).addTo(map);
|
|
||||||
map.attributionControl.setPrefix(false);
|
|
||||||
|
|
||||||
const street = L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
|
||||||
maxZoom: 19,
|
|
||||||
attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
|
|
||||||
});
|
|
||||||
street.addTo(map);
|
|
||||||
|
|
||||||
const satellite = L.tileLayer('https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}', {
|
|
||||||
attribution: 'Tiles © Esri'
|
|
||||||
});
|
|
||||||
|
|
||||||
const street_local = L.tileLayer('http://127.0.0.1:8080/styles/basic-preview/{z}/{x}/{y}.png', {
|
|
||||||
maxZoom: 19,
|
|
||||||
attribution: 'Local Tiles'
|
|
||||||
});
|
|
||||||
|
|
||||||
const baseLayers = {
|
|
||||||
"Улицы": street,
|
|
||||||
"Спутник": satellite,
|
|
||||||
"Локально": street_local
|
|
||||||
};
|
|
||||||
|
|
||||||
L.control.layers(baseLayers).addTo(map);
|
|
||||||
map.setMaxZoom(18);
|
|
||||||
map.setMinZoom(0);
|
|
||||||
L.control.measure({ primaryLengthUnit: 'kilometers' }).addTo(map);
|
|
||||||
|
|
||||||
// Цвета для маркеров
|
// Цвета для маркеров
|
||||||
var markerColors = {
|
const markerColors = {
|
||||||
'blue': 'blue',
|
'blue': '#3388ff',
|
||||||
'orange': 'orange',
|
'orange': '#ff8c00',
|
||||||
'green': 'green',
|
'green': '#28a745',
|
||||||
'violet': 'violet'
|
'violet': '#9c27b0'
|
||||||
};
|
};
|
||||||
|
|
||||||
var getColorIcon = function(color) {
|
// Данные групп из Django
|
||||||
return L.icon({
|
const groups = [
|
||||||
iconUrl: '{% static "leaflet-markers/img/marker-icon-" %}' + color + '.png',
|
|
||||||
shadowUrl: '{% static "leaflet-markers/img/marker-shadow.png" %}',
|
|
||||||
iconSize: [25, 41],
|
|
||||||
iconAnchor: [12, 41],
|
|
||||||
popupAnchor: [1, -34],
|
|
||||||
shadowSize: [41, 41]
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
var overlays = [];
|
|
||||||
|
|
||||||
// Создаём слои для каждого типа координат
|
|
||||||
{% for group in groups %}
|
{% for group in groups %}
|
||||||
var groupName = '{{ group.name|escapejs }}';
|
{
|
||||||
var colorName = '{{ group.color }}';
|
name: '{{ group.name|escapejs }}',
|
||||||
var groupIcon = getColorIcon(colorName);
|
color: '{{ group.color }}',
|
||||||
var groupLayer = L.layerGroup();
|
points: [
|
||||||
|
|
||||||
var subgroup = [];
|
|
||||||
{% for point_data in group.points %}
|
{% for point_data in group.points %}
|
||||||
var pointName = "{{ point_data.source_id|escapejs }}";
|
{
|
||||||
var marker = L.marker([{{ point_data.point.1|safe }}, {{ point_data.point.0|safe }}], {
|
id: '{{ point_data.source_id|escapejs }}',
|
||||||
icon: groupIcon
|
coordinates: [{{ point_data.point.0|safe }}, {{ point_data.point.1|safe }}]
|
||||||
}).bindPopup(pointName);
|
}{% if not forloop.last %},{% endif %}
|
||||||
groupLayer.addLayer(marker);
|
|
||||||
|
|
||||||
subgroup.push({
|
|
||||||
label: "{{ forloop.counter }} - {{ point_data.source_id|escapejs }}",
|
|
||||||
layer: marker
|
|
||||||
});
|
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
]
|
||||||
overlays.push({
|
}{% if not forloop.last %},{% endif %}
|
||||||
label: groupName,
|
|
||||||
selectAllCheckbox: true,
|
|
||||||
children: subgroup,
|
|
||||||
layer: groupLayer
|
|
||||||
});
|
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
];
|
||||||
|
|
||||||
// Корневая группа
|
// Полигон фильтра
|
||||||
const rootGroup = {
|
const polygonCoords = {% if polygon_coords %}{{ polygon_coords|safe }}{% else %}null{% endif %};
|
||||||
label: "Все точки",
|
|
||||||
selectAllCheckbox: true,
|
|
||||||
children: overlays,
|
|
||||||
layer: L.layerGroup()
|
|
||||||
};
|
|
||||||
|
|
||||||
// Создаём tree control
|
// Стили карт
|
||||||
const layerControl = L.control.layers.tree(baseLayers, [rootGroup], {
|
const mapStyles = {
|
||||||
collapsed: false,
|
streets: {
|
||||||
autoZIndex: true
|
version: 8,
|
||||||
});
|
sources: {
|
||||||
layerControl.addTo(map);
|
'osm': {
|
||||||
|
type: 'raster',
|
||||||
// Подгоняем карту под все маркеры
|
tiles: ['https://a.tile.openstreetmap.org/{z}/{x}/{y}.png'],
|
||||||
{% if groups %}
|
tileSize: 256,
|
||||||
var groupBounds = L.featureGroup([]);
|
attribution: '© OpenStreetMap'
|
||||||
{% for group in groups %}
|
|
||||||
{% for point_data in group.points %}
|
|
||||||
groupBounds.addLayer(L.marker([{{ point_data.point.1|safe }}, {{ point_data.point.0|safe }}]));
|
|
||||||
{% endfor %}
|
|
||||||
{% endfor %}
|
|
||||||
map.fitBounds(groupBounds.getBounds().pad(0.1));
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
// Добавляем легенду в левый нижний угол
|
|
||||||
var legend = L.control({ position: 'bottomleft' });
|
|
||||||
legend.onAdd = function(map) {
|
|
||||||
var div = L.DomUtil.create('div', 'legend');
|
|
||||||
div.innerHTML = '<h6><strong>Легенда</strong></h6>';
|
|
||||||
|
|
||||||
{% for group in groups %}
|
|
||||||
div.innerHTML += `
|
|
||||||
<div class="legend-item">
|
|
||||||
<div class="legend-marker" style="background-image: url('{% static "leaflet-markers/img/marker-icon-" %}{{ group.color }}.png');"></div>
|
|
||||||
<span>{{ group.name|escapejs }}</span>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
{% endfor %}
|
|
||||||
|
|
||||||
{% if polygon_coords %}
|
|
||||||
div.innerHTML += `
|
|
||||||
<div class="legend-item" style="margin-top: 8px; padding-top: 8px; border-top: 1px solid #ddd;">
|
|
||||||
<div style="width: 18px; height: 18px; margin-right: 6px; background-color: rgba(51, 136, 255, 0.2); border: 2px solid #3388ff;"></div>
|
|
||||||
<span>Область фильтра</span>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
return div;
|
|
||||||
};
|
|
||||||
legend.addTo(map);
|
|
||||||
|
|
||||||
// Добавляем полигон фильтра на карту, если он есть
|
|
||||||
{% if polygon_coords %}
|
|
||||||
try {
|
|
||||||
const polygonCoords = {{ polygon_coords|safe }};
|
|
||||||
if (polygonCoords && polygonCoords.length > 0) {
|
|
||||||
// Преобразуем координаты из [lng, lat] в [lat, lng] для Leaflet
|
|
||||||
const latLngs = polygonCoords.map(coord => [coord[1], coord[0]]);
|
|
||||||
|
|
||||||
// Создаем полигон
|
|
||||||
const filterPolygon = L.polygon(latLngs, {
|
|
||||||
color: '#3388ff',
|
|
||||||
fillColor: '#3388ff',
|
|
||||||
fillOpacity: 0.2,
|
|
||||||
weight: 2,
|
|
||||||
dashArray: '5, 5'
|
|
||||||
});
|
|
||||||
|
|
||||||
// Добавляем полигон на карту
|
|
||||||
filterPolygon.addTo(map);
|
|
||||||
|
|
||||||
// Добавляем popup с информацией
|
|
||||||
filterPolygon.bindPopup('<strong>Область фильтра</strong><br>Отображаются только источники с точками в этой области');
|
|
||||||
|
|
||||||
// Если нет других точек, центрируем карту на полигоне
|
|
||||||
{% if not groups %}
|
|
||||||
map.fitBounds(filterPolygon.getBounds());
|
|
||||||
{% endif %}
|
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
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: '© 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');
|
||||||
|
map.addControl(new maplibregl.GeolocateControl({
|
||||||
|
positionOptions: { enableHighAccuracy: true },
|
||||||
|
trackUserLocation: true
|
||||||
|
}), '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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Кастомный контрол для 3D зданий
|
||||||
|
class Buildings3DControl {
|
||||||
|
onAdd(map) {
|
||||||
|
this._map = map;
|
||||||
|
this._container = document.createElement('div');
|
||||||
|
this._container.className = 'maplibregl-ctrl maplibregl-ctrl-group maplibregl-ctrl-3d';
|
||||||
|
this._container.innerHTML = '<button type="button" title="3D здания"><span class="maplibregl-ctrl-icon"></span></button>';
|
||||||
|
this._container.onclick = () => {
|
||||||
|
if (is3DEnabled) {
|
||||||
|
if (map.getLayer('3d-buildings')) {
|
||||||
|
map.removeLayer('3d-buildings');
|
||||||
|
}
|
||||||
|
is3DEnabled = false;
|
||||||
|
this._container.querySelector('button').style.backgroundColor = '';
|
||||||
|
} else {
|
||||||
|
this.add3DBuildings();
|
||||||
|
is3DEnabled = true;
|
||||||
|
this._container.querySelector('button').style.backgroundColor = '#e3f2fd';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
return this._container;
|
||||||
|
}
|
||||||
|
add3DBuildings() {
|
||||||
|
if (this._map.getLayer('3d-buildings')) return;
|
||||||
|
|
||||||
|
const layers = this._map.getStyle().layers;
|
||||||
|
let labelLayerId;
|
||||||
|
for (let i = 0; i < layers.length; i++) {
|
||||||
|
if (layers[i].type === 'symbol' && layers[i].layout && layers[i].layout['text-field']) {
|
||||||
|
labelLayerId = layers[i].id;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this._map.addLayer({
|
||||||
|
'id': '3d-buildings',
|
||||||
|
'source': 'composite',
|
||||||
|
'source-layer': 'building',
|
||||||
|
'filter': ['==', 'extrude', 'true'],
|
||||||
|
'type': 'fill-extrusion',
|
||||||
|
'minzoom': 15,
|
||||||
|
'paint': {
|
||||||
|
'fill-extrusion-color': '#aaa',
|
||||||
|
'fill-extrusion-height': ['get', 'height'],
|
||||||
|
'fill-extrusion-base': ['get', 'min_height'],
|
||||||
|
'fill-extrusion-opacity': 0.6
|
||||||
|
}
|
||||||
|
}, labelLayerId);
|
||||||
|
}
|
||||||
|
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 Buildings3DControl(), '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) {
|
} catch (e) {
|
||||||
console.error('Ошибка при отображении полигона фильтра:', e);
|
console.error('Ошибка при отображении полигона фильтра:', e);
|
||||||
}
|
}
|
||||||
{% endif %}
|
}
|
||||||
|
|
||||||
|
// Подгонка карты под все маркеры
|
||||||
|
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>
|
</script>
|
||||||
{% endblock extra_js %}
|
{% endblock extra_js %}
|
||||||
|
|||||||
Reference in New Issue
Block a user