// 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 = `
Настройка маркера
`; 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: `
${shapeFunc(hexColor, size)}
` }); const marker = L.marker(latlng, { icon: icon }); // Add popup with label if provided if (this.settings.label) { marker.bindPopup(`${this.settings.label}`); 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 = ` ${text} `; 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'; } } }