Files
dbstorage/dbapp/static/js/custom_marker_tool.js

559 lines
20 KiB
JavaScript
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.

// 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('Кликните на карту для размещения маркера.');
// Add keyboard handlers
this.keyHandler = (e) => {
if (e.key === 'Escape') {
console.log("Жму кнопку");
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';
}
}
}