|
| 1 | +from branca.element import MacroElement |
| 2 | +from folium.template import Template |
| 3 | + |
| 4 | + |
| 5 | +class CoordinateInput(MacroElement): |
| 6 | + """ |
| 7 | + Add input form to the map for entering coordinates and placing markers at those locations. |
| 8 | + Supports marker removal by double-clicking. |
| 9 | +
|
| 10 | + Parameters |
| 11 | + ---------- |
| 12 | + position : str, default 'topleft' |
| 13 | + Corner of the map where the input form will be placed. |
| 14 | + Options: 'topleft', 'topright', 'bottomleft', 'bottomright' |
| 15 | + placeholder : str, default 'Latitude, Longitude' |
| 16 | + Placeholder text for the coordinate input field (e.g., "40.7128, -74.0060") |
| 17 | + button_text : str, default 'Add Marker' |
| 18 | + Text displayed on the submit button |
| 19 | + popup_text : str, default None |
| 20 | + Text to display in marker popups. Use ${lat} and ${lng} for coordinates. |
| 21 | + If None, will show 'Lat: {lat}, Lon: {lng}' |
| 22 | + show_instructions : bool, default True |
| 23 | + Whether to show instructions for removing markers |
| 24 | +
|
| 25 | + Examples |
| 26 | + -------- |
| 27 | + >>> m = folium.Map([45.5, -122.3], zoom_start=13) |
| 28 | + >>> CoordinateInput().add_to(m) |
| 29 | + >>> m.save('map.html') |
| 30 | +
|
| 31 | + >>> # With custom settings |
| 32 | + >>> CoordinateInput( |
| 33 | + ... position='topright', |
| 34 | + ... placeholder='Enter coordinates', |
| 35 | + ... button_text='Place Marker', |
| 36 | + ... popup_text='<b>Coordinates:</b><br>Lat: ${lat}<br>Lon: ${lng}' |
| 37 | + ... ).add_to(m) |
| 38 | +
|
| 39 | + To remove a marker, double-click on it. |
| 40 | + """ |
| 41 | + |
| 42 | + _template = Template( |
| 43 | + r""" |
| 44 | + {% macro script(this, kwargs) %} |
| 45 | + (function() { |
| 46 | + var {{ this.get_name() }}_control = L.control({position: '{{ this.position }}'}); |
| 47 | +
|
| 48 | + {{ this.get_name() }}_control.onAdd = function (map) { |
| 49 | + var div = L.DomUtil.create('div', 'leaflet-control leaflet-bar'); |
| 50 | + div.id = '{{ this.get_name() }}_container'; |
| 51 | + div.style.backgroundColor = 'white'; |
| 52 | + div.style.padding = '10px'; |
| 53 | + div.style.borderRadius = '5px'; |
| 54 | + div.style.boxShadow = '0 2px 4px rgba(0,0,0,0.1)'; |
| 55 | +
|
| 56 | + var instructionsHtml = '{% if this.show_instructions %}<div style="margin-bottom: 8px; font-size: 10px; color: #666; max-width: 150px;"><em>Double-click marker to remove</em></div>{% endif %}'; |
| 57 | +
|
| 58 | + div.innerHTML = '<div style="font-family: Arial, sans-serif; font-size: 12px; width: 150px;">' + |
| 59 | + '<div style="margin-bottom: 8px; font-weight: bold;">Add Marker</div>' + |
| 60 | + instructionsHtml + |
| 61 | + '<input type="text" id="{{ this.get_name() }}_coords" placeholder="{{ this.placeholder }}" style="width: 100%; padding: 5px; margin-bottom: 5px; box-sizing: border-box;">' + |
| 62 | + '<button id="{{ this.get_name() }}_btn" style="width: 100%; background-color: #4CAF50; color: white; border: none; border-radius: 3px; cursor: pointer; font-size: 12px;">{{ this.button_text }}</button>' + |
| 63 | + '<button id="{{ this.get_name() }}_clear_btn" style="width: 100%; margin-top: 5px; background-color: #f44336; color: white; border: none; border-radius: 3px; cursor: pointer; font-size: 12px;">Clear All</button>' + |
| 64 | + '</div>'; |
| 65 | +
|
| 66 | + L.DomEvent.disableClickPropagation(div); |
| 67 | +
|
| 68 | + return div; |
| 69 | + }; |
| 70 | +
|
| 71 | + {{ this.get_name() }}_control.addTo({{ this._parent.get_name() }}); |
| 72 | +
|
| 73 | + setTimeout(function() { |
| 74 | + var btnElement = document.getElementById('{{ this.get_name() }}_btn'); |
| 75 | + var clearBtnElement = document.getElementById('{{ this.get_name() }}_clear_btn'); |
| 76 | + var coordsElement = document.getElementById('{{ this.get_name() }}_coords'); |
| 77 | + var map = {{ this._parent.get_name() }}; |
| 78 | + var markerGroup = L.featureGroup(); |
| 79 | + markerGroup.addTo(map); |
| 80 | +
|
| 81 | + if (btnElement && coordsElement) { |
| 82 | + var addMarker = function() { |
| 83 | + var input = coordsElement.value.trim(); |
| 84 | + var parts = input.split(','); |
| 85 | +
|
| 86 | + if (parts.length !== 2) { |
| 87 | + alert('Please enter coordinates in format: latitude, longitude'); |
| 88 | + return; |
| 89 | + } |
| 90 | +
|
| 91 | + var lat = parseFloat(parts[0].trim()); |
| 92 | + var lng = parseFloat(parts[1].trim()); |
| 93 | +
|
| 94 | + if (isNaN(lat) || isNaN(lng)) { |
| 95 | + alert('Please enter valid numbers for coordinates'); |
| 96 | + return; |
| 97 | + } |
| 98 | +
|
| 99 | + if (lat < -90 || lat > 90) { |
| 100 | + alert('Latitude must be between -90 and 90'); |
| 101 | + return; |
| 102 | + } |
| 103 | +
|
| 104 | + if (lng < -180 || lng > 180) { |
| 105 | + alert('Longitude must be between -180 and 180'); |
| 106 | + return; |
| 107 | + } |
| 108 | +
|
| 109 | + var marker = L.marker([lat, lng]); |
| 110 | +
|
| 111 | + var popupText = '{{ this.popup_text }}'; |
| 112 | + if (popupText === '') { |
| 113 | + popupText = 'Lat: ' + lat.toFixed(4) + '<br>Lon: ' + lng.toFixed(4); |
| 114 | + } else { |
| 115 | + popupText = popupText.replace(/\$\{lat\}/g, lat.toFixed(4)); |
| 116 | + popupText = popupText.replace(/\$\{lng\}/g, lng.toFixed(4)); |
| 117 | + } |
| 118 | + marker.bindPopup(popupText); |
| 119 | +
|
| 120 | + // Add double-click to remove marker |
| 121 | + marker.on('dblclick', function() { |
| 122 | + markerGroup.removeLayer(marker); |
| 123 | + }); |
| 124 | +
|
| 125 | + markerGroup.addLayer(marker); |
| 126 | + coordsElement.value = ''; |
| 127 | + marker.openPopup(); |
| 128 | + }; |
| 129 | +
|
| 130 | + btnElement.addEventListener('click', addMarker); |
| 131 | +
|
| 132 | + coordsElement.addEventListener('keypress', function(e) { |
| 133 | + if (e.key === 'Enter') { |
| 134 | + addMarker(); |
| 135 | + } |
| 136 | + }); |
| 137 | +
|
| 138 | + clearBtnElement.addEventListener('click', function() { |
| 139 | + if (confirm('Are you sure you want to remove all markers?')) { |
| 140 | + markerGroup.clearLayers(); |
| 141 | + coordsElement.value = ''; |
| 142 | + } |
| 143 | + }); |
| 144 | + } |
| 145 | + }, 100); |
| 146 | + })(); |
| 147 | + {% endmacro %} |
| 148 | + """ |
| 149 | + ) |
| 150 | + |
| 151 | + def __init__( |
| 152 | + self, |
| 153 | + position: str = "topleft", |
| 154 | + placeholder: str = "Latitude, Longitude", |
| 155 | + button_text: str = "Add Marker", |
| 156 | + popup_text: str = "", |
| 157 | + show_instructions: bool = True, |
| 158 | + ): |
| 159 | + super().__init__() |
| 160 | + self._name = "CoordinateInput" |
| 161 | + |
| 162 | + self.position = position |
| 163 | + self.placeholder = placeholder |
| 164 | + self.button_text = button_text |
| 165 | + self.popup_text = popup_text if popup_text else "Lat: ${lat}<br>Lon: ${lng}" |
| 166 | + self.show_instructions = show_instructions |
0 commit comments