Files
georelays/scripts/generate_relay_map.py

233 lines
7.3 KiB
Python
Executable File

#!/usr/bin/env python3
"""
Generate World Map of Nostr Relay Locations
This script reads the nostr_relays.csv file, which contains information about
BitChat-compatible Nostr relays, including their geographical coordinates.
It generates both an interactive HTML map and a static PNG image showing
the distribution of relays around the world.
The maps are saved in the assets directory.
"""
import os
import pandas as pd
import matplotlib.pyplot as plt
import folium
from folium.plugins import MarkerCluster
import numpy as np
from matplotlib.colors import LinearSegmentedColormap
import time
def create_interactive_map(df, output_path):
"""
Create an interactive HTML map showing relay locations with clustering.
Args:
df: DataFrame containing relay data with Latitude and Longitude columns
output_path: Path to save the HTML map
"""
# Create map centered at (0, 0) with zoom level 2
world_map = folium.Map(location=[0, 0], zoom_start=2, tiles='CartoDB positron')
# Add marker cluster
marker_cluster = MarkerCluster().add_to(world_map)
# Add markers for each relay
for idx, row in df.iterrows():
folium.Marker(
location=[row['Latitude'], row['Longitude']],
popup=row['Relay URL'],
icon=folium.Icon(color='blue', icon='signal', prefix='fa')
).add_to(marker_cluster)
# Save map
world_map.save(output_path)
return len(df)
def create_static_map(df, output_path):
"""
Create a static PNG map showing relay locations on a proper world map.
Args:
df: DataFrame containing relay data with Latitude and Longitude columns
output_path: Path to save the PNG map
"""
import cartopy.crs as ccrs
import cartopy.feature as cfeature
plt.figure(figsize=(15, 10))
# Create a map with a proper projection
ax = plt.axes(projection=ccrs.PlateCarree())
# Add map features
ax.add_feature(cfeature.LAND, facecolor='#E5E5E5')
ax.add_feature(cfeature.OCEAN, facecolor='#DDEEFF')
ax.add_feature(cfeature.COASTLINE, linewidth=0.5, edgecolor='#999999')
ax.add_feature(cfeature.BORDERS, linewidth=0.3, edgecolor='#AAAAAA')
ax.add_feature(cfeature.LAKES, facecolor='#DDEEFF', edgecolor='#999999', linewidth=0.5)
ax.add_feature(cfeature.RIVERS, edgecolor='#99CCFF', linewidth=0.5)
# Add grid
gl = ax.gridlines(crs=ccrs.PlateCarree(), draw_labels=True,
linewidth=0.5, color='gray', alpha=0.5, linestyle='--')
gl.top_labels = False
gl.right_labels = False
# Set map limits
ax.set_extent([-180, 180, -90, 90], crs=ccrs.PlateCarree())
# Plot relay locations
plt.scatter(
df['Longitude'],
df['Latitude'],
alpha=0.8,
c='blue',
s=30,
edgecolor='white',
linewidth=0.5,
transform=ccrs.PlateCarree()
)
# Add title and labels
plt.title('Global Distribution of BitChat-Compatible Nostr Relays', fontsize=16)
# Add timestamp
timestamp = time.strftime("%Y-%m-%d", time.localtime())
plt.annotate(
f'Generated: {timestamp} | Total Relays: {len(df)}',
xy=(0.02, 0.02),
xycoords='axes fraction',
fontsize=10,
bbox=dict(boxstyle="round,pad=0.3", fc="white", alpha=0.8)
)
# Adjust layout and save
plt.tight_layout()
plt.savefig(output_path, dpi=300, bbox_inches='tight')
plt.close()
return len(df)
def create_heatmap(df, output_path):
"""
Create a heatmap visualization showing relay density across the world.
Args:
df: DataFrame containing relay data with Latitude and Longitude columns
output_path: Path to save the PNG heatmap
"""
import cartopy.crs as ccrs
import cartopy.feature as cfeature
from scipy.ndimage import gaussian_filter
plt.figure(figsize=(15, 10))
# Create a map with a proper projection
ax = plt.axes(projection=ccrs.PlateCarree())
# Add map features
ax.add_feature(cfeature.LAND, facecolor='#E5E5E5')
ax.add_feature(cfeature.OCEAN, facecolor='#DDEEFF')
ax.add_feature(cfeature.COASTLINE, linewidth=0.5, edgecolor='#999999')
ax.add_feature(cfeature.BORDERS, linewidth=0.3, edgecolor='#AAAAAA')
# Add grid
gl = ax.gridlines(crs=ccrs.PlateCarree(), draw_labels=True,
linewidth=0.5, color='gray', alpha=0.5, linestyle='--')
gl.top_labels = False
gl.right_labels = False
# Set map limits
ax.set_extent([-180, 180, -90, 90], crs=ccrs.PlateCarree())
# Create grid for heatmap
x = np.linspace(-180, 180, 360)
y = np.linspace(-90, 90, 180)
# Count relays in each grid cell
heatmap = np.zeros((len(y)-1, len(x)-1))
for _, row in df.iterrows():
lon_idx = np.searchsorted(x, row['Longitude']) - 1
lat_idx = np.searchsorted(y, row['Latitude']) - 1
if 0 <= lon_idx < len(x)-1 and 0 <= lat_idx < len(y)-1:
heatmap[lat_idx, lon_idx] += 1
# Apply Gaussian smoothing to heatmap
heatmap = gaussian_filter(heatmap, sigma=3)
# Create custom colormap (blue to white)
colors = [(0, 0, 0.8, 0), (0, 0, 1, 0.7), (0.5, 0.5, 1, 0.8), (1, 1, 1, 0.9)]
cmap = LinearSegmentedColormap.from_list('custom_blue', colors)
# Plot heatmap
plt.pcolormesh(x, y, heatmap, cmap=cmap, alpha=0.7, transform=ccrs.PlateCarree())
# Add title
plt.title('Global Heatmap of BitChat-Compatible Nostr Relays', fontsize=16)
# Add timestamp and relay count
timestamp = time.strftime("%Y-%m-%d", time.localtime())
plt.annotate(
f'Generated: {timestamp} | Total Relays: {len(df)}',
xy=(0.02, 0.02),
xycoords='axes fraction',
fontsize=10,
bbox=dict(boxstyle="round,pad=0.3", fc="white", alpha=0.8)
)
# Adjust layout and save
plt.tight_layout()
plt.savefig(output_path, dpi=300, bbox_inches='tight')
plt.close()
return len(df)
def main():
"""
Main function to generate maps of relay locations.
"""
# Change to the root directory of the project
script_dir = os.path.dirname(os.path.abspath(__file__))
root_dir = os.path.dirname(script_dir)
os.chdir(root_dir)
# Ensure the assets directory exists
os.makedirs('assets', exist_ok=True)
# Read the relay data
df = pd.read_csv('nostr_relays.csv')
# Filter out rows with missing or invalid coordinates
df = df.dropna(subset=['Latitude', 'Longitude'])
# Filter out invalid coordinates
df = df[(df['Latitude'] >= -90) & (df['Latitude'] <= 90) &
(df['Longitude'] >= -180) & (df['Longitude'] <= 180)]
# Generate maps. Any failure must propagate so CI cannot commit a partial
# update while reporting a successful workflow.
relay_count_interactive = create_interactive_map(
df,
'assets/relay_locations_interactive.html'
)
relay_count_static = create_static_map(
df,
'assets/relay_locations_static.png'
)
relay_count_heatmap = create_heatmap(
df,
'assets/relay_locations_heatmap.png'
)
print(f"Generated interactive map with {relay_count_interactive} relays")
print(f"Generated static map with {relay_count_static} relays")
print(f"Generated heatmap with {relay_count_heatmap} relays")
if __name__ == "__main__":
main()