Repetidoras no MMAR - Radioamador

Foi introduzido no MMAR - Radioamador a possibilidade de consultar repetidoras a partir da localização geográfica, bastando a gente digitar uma coordenada e o raio de busca. O único ponto de atenção é que apesar de muito funcional e rápido, algumas repetidoras devem estar com o cadastro errado, até porque tem coordenadas no meio do Atlântico Norte.

Para consultar basta clicar em Consultar Repetidora na aba Opções e inserir a coordenada geografica. Dica: você pode copiar diretamente do Google Maps e ao colar ele quebrará automaticamente entre latitude e longitude:

Consulta de repetidoras no MMAR - Radioamador

Plotando em um mapa as localizações

Ver a localização geográfica já é muito útil, mas é muito mais interessante visualizar os pontos em um mapa. Para isso é possível utilizar um script para renderizar no mapa a localização e o resultado você pode ver abaixo:

Abrir mapa de repetidoras em tela cheia

O script de geração deste mapa, baseado no arquivo CSV que pode ser recuperado na consulta do MMAR - Radioamador, é este logo abaixo e para executa-lo basta rodar o comando uv run plot.py no mesmo diretório onde estiver o CSV com o nome repetidoras.csv:

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

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

# 1. Carregar dados
df = pd.read_csv('repetidoras.csv', sep=';')

# 2. Criar mapa (sem tiles padrão: as camadas são adicionadas abaixo)
mapa = folium.Map(
    location=[-15.7801, -47.9292],
    zoom_start=4,
    max_zoom=13,
    tiles=None
)

# 2.1 Camadas de fundo (seletor de mapas para visualizar estados/cidades)
# Voyager é o padrão: mostra nomes de cidades e vias principais mantendo leveza.
folium.TileLayer('CartoDB voyager', name='Voyager (cidades e vias)').add_to(mapa)
folium.TileLayer('OpenStreetMap', name='OpenStreetMap (ruas detalhadas)').add_to(mapa)
folium.TileLayer('CartoDB positron', name='Claro (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='Satélite (Esri)'
).add_to(mapa)

# 2.2 Sobreposição de rótulos (nomes de cidades e vias) — útil sobre o satélite
folium.TileLayer(
    tiles='https://{s}.basemaps.cartocdn.com/rastertiles/voyager_only_labels/{z}/{x}/{y}{r}.png',
    attr='© OpenStreetMap, © CARTO',
    name='Rótulos (cidades e vias)',
    overlay=True,
    control=True,
    show=False
).add_to(mapa)

# 3. Adicionar a data de referência (Texto flutuante)
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; Gerado em: {data_hoje}
     </div>
     """
mapa.get_root().html.add_child(folium.Element(texto_data))

# 3.1 Deixar as bolhas de agrupamento semitransparentes (ver cidades por baixo)
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); }

     /* Responsivo: compacta busca e seletor de camadas em telas pequenas/verticais */
     @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. Cluster de marcadores: abre os marcadores sobrepostos (mesmo endereço) ao clicar
marker_cluster = MarkerCluster(
    name="Repetidoras",
    options={
        # No zoom máximo o clique "abre" (spiderfy) os marcadores empilhados
        "spiderfyOnMaxZoom": True,
        "zoomToBoundsOnClick": True,
        # Distância maior entre as "pernas" do spiderfy facilita separar repetidoras
        "spiderfyDistanceMultiplier": 2,
        # Agrupa apenas marcadores bem próximos, reduzindo aglomeração
        "maxClusterRadius": 40,
    },
).add_to(mapa)

# 4.1 Camada GeoJSON usada pela busca (propriedades são pesquisáveis no JS)
features = []
for _, linha in df.iterrows():
    info = f"<b>{linha['Indicativo']}</b><br>Freq: {linha['Frequência de Operação']}"

    # Marcador visual (dentro do 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 para busca por indicativo
    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="Busca por indicativo",
    show=False,  # camada apenas para busca; não polui o mapa
    # Marcador invisível: o plugin Search exige a camada visível, mas não queremos
    # bolinhas cobrindo o mapa. O popup continua funcionando ao localizar.
    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. Busca centraliza e dá zoom moderado, abrindo o popup
Search(
    layer=geojson_busca,
    search_label='Indicativo',
    geom_type='Point',
    placeholder="Buscar Indicativo...",
    collapsed=False,
    search_zoom=12,
).add_to(mapa)

# 6. Controle de camadas (alternar mapas de fundo)
folium.LayerControl(collapsed=False).add_to(mapa)

mapa.save('mapa_repetidoras.html')
print(f"Mapa gerado com sucesso! Data de referência: {data_hoje}")