Repeaters on MMAR - Radioamador

MMAR - Radioamador has introduced the ability to look up repeaters based on geographic location: you just type a coordinate and a search radius. The only thing to watch out for is that, although it’s very functional and fast, some repeaters may have incorrect records — there are even coordinates in the middle of the North Atlantic.

To look one up, just click Consultar Repetidora in the Opções tab and enter the geographic coordinate. Tip: you can copy it directly from Google Maps, and when you paste it, it will automatically split into latitude and longitude:

Repeater lookup on MMAR - Radioamador

Plotting the locations on a map

Seeing the geographic location is already very useful, but it’s much more interesting to visualize the points on a map. To do that you can use a script to render the locations on the map, and you can see the result below:

Open the repeaters map in full screen

The script that generates this map, based on the CSV file you can retrieve from the MMAR - Radioamador lookup, is shown below. To run it, just execute the command uv run plot.py in the same directory as the CSV named repetidoras.csv:

# /// script
# dependencies = [
#   "pandas",
#   "folium",
#   "datetime"
# ]
# ///

import pandas as pd
import folium
from folium.plugins import MarkerCluster, Search
from datetime import datetime

# 1. Load data
df = pd.read_csv('repetidoras.csv', sep=';')

# 2. Create map (no default tiles: layers are added below)
mapa = folium.Map(
    location=[-15.7801, -47.9292],
    zoom_start=4,
    max_zoom=13,
    tiles=None
)

# 2.1 Background layers (map selector to view states/cities)
# Voyager is the default: shows city names and main roads while staying light.
folium.TileLayer('CartoDB voyager', name='Voyager (cities and roads)').add_to(mapa)
folium.TileLayer('OpenStreetMap', name='OpenStreetMap (detailed streets)').add_to(mapa)
folium.TileLayer('CartoDB positron', name='Light (CartoDB Positron)').add_to(mapa)
folium.TileLayer(
    tiles='https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}',
    attr='Esri, Maxar, Earthstar Geographics',
    name='Satellite (Esri)'
).add_to(mapa)

# 2.2 Label overlay (city and road names) — useful over satellite
folium.TileLayer(
    tiles='https://{s}.basemaps.cartocdn.com/rastertiles/voyager_only_labels/{z}/{x}/{y}{r}.png',
    attr='© OpenStreetMap, © CARTO',
    name='Labels (cities and roads)',
    overlay=True,
    control=True,
    show=False
).add_to(mapa)

# 3. Add the reference date (floating text)
data_hoje = datetime.now().strftime("%d/%m/%Y")
texto_data = f"""
     <div style="position: fixed; 
     bottom: 50px; left: 50px; width: 200px; height: 30px; 
     background-color: white; border:2px solid grey; z-index:9999; font-size:14px;
     padding: 5px; text-align: center;">&nbsp; Generated on: {data_hoje}
     </div>
     """
mapa.get_root().html.add_child(folium.Element(texto_data))

# 3.1 Make the cluster bubbles semi-transparent (see cities underneath)
css_clusters = """
     <style>
     .marker-cluster { background-clip: padding-box; }
     .marker-cluster-small  { background-color: rgba(110, 204, 57, 0.45); }
     .marker-cluster-small div  { background-color: rgba(110, 204, 57, 0.75); }
     .marker-cluster-medium { background-color: rgba(240, 194, 12, 0.45); }
     .marker-cluster-medium div { background-color: rgba(240, 194, 12, 0.75); }
     .marker-cluster-large  { background-color: rgba(241, 128, 23, 0.45); }
     .marker-cluster-large div  { background-color: rgba(241, 128, 23, 0.75); }

     /* Responsive: compact search and layer selector on small/portrait screens */
     @media (max-width: 600px) {
        .leaflet-control-search .search-input { width: 130px; font-size: 12px; }
        .leaflet-control-search { margin-right: 6px !important; }
        .leaflet-control-layers-expanded {
            max-width: 150px;
            max-height: 40vh;
            overflow-y: auto;
            font-size: 12px;
            padding: 6px 8px;
            line-height: 1.3;
        }
        .leaflet-control-layers label { margin-bottom: 2px; }
        .leaflet-top.leaflet-right { max-width: 55vw; }
     }
     </style>
     """
mapa.get_root().html.add_child(folium.Element(css_clusters))

# 4. Marker cluster: expands overlapping markers (same address) on click
marker_cluster = MarkerCluster(
    name="Repeaters",
    options={
        # At max zoom, clicking "opens" (spiderfy) the stacked markers
        "spiderfyOnMaxZoom": True,
        "zoomToBoundsOnClick": True,
        # A larger distance between the spiderfy "legs" makes repeaters easier to tell apart
        "spiderfyDistanceMultiplier": 2,
        # Cluster only very close markers, reducing clutter
        "maxClusterRadius": 40,
    },
).add_to(mapa)

# 4.1 GeoJSON layer used by the search (properties are searchable in JS)
features = []
for _, linha in df.iterrows():
    info = f"<b>{linha['Indicativo']}</b><br>Freq: {linha['Frequência de Operação']}"

    # Visual marker (inside the cluster)
    folium.Marker(
        location=[linha['Latitude'], linha['Longitude']],
        popup=folium.Popup(info, max_width=300),
        icon=folium.Icon(color='blue', icon='tower', prefix='fa')
    ).add_to(marker_cluster)

    # Feature for callsign search
    features.append({
        "type": "Feature",
        "geometry": {
            "type": "Point",
            "coordinates": [linha['Longitude'], linha['Latitude']],
        },
        "properties": {
            "Indicativo": str(linha['Indicativo']),
            "popup": info,
        },
    })

geojson_busca = folium.GeoJson(
    {"type": "FeatureCollection", "features": features},
    name="Search by callsign",
    show=False,  # search-only layer; does not clutter the map
    # Invisible marker: the Search plugin requires a visible layer, but we don't
    # want dots covering the map. The popup still works when a match is found.
    marker=folium.CircleMarker(
        radius=0, opacity=0, fill=False, fill_opacity=0, stroke=False
    ),
    popup=folium.GeoJsonPopup(fields=["popup"], labels=False),
).add_to(mapa)

# 5. Search centers and applies a moderate zoom, opening the popup
Search(
    layer=geojson_busca,
    search_label='Indicativo',
    geom_type='Point',
    placeholder="Search callsign...",
    collapsed=False,
    search_zoom=12,
).add_to(mapa)

# 6. Layer control (switch background maps)
folium.LayerControl(collapsed=False).add_to(mapa)

mapa.save('mapa_repetidoras.html')
print(f"Map generated successfully! Reference date: {data_hoje}")