kglobe/kglobe.py
2025-07-20 17:00:21 +02:00

117 lines
3.7 KiB
Python

#!/usr/bin/env python3
from kitty import draw_to_terminal, get_position, set_position, hide_cursor, show_cursor #, draw_animation
from PIL import Image
import pyvista as pv
import numpy as np
import math
import subprocess
import re
import requests
# Convert lat/lon to Cartesian coordinates
def latlon_to_xyz(lat, lon, radius=1.0):
lat_rad = np.radians(lat)
lon_rad = np.radians(lon)
x = radius * np.cos(lat_rad) * np.cos(lon_rad)
y = radius * np.cos(lat_rad) * np.sin(lon_rad)
z = radius * np.sin(lat_rad)
return np.array([x, y, z])
# Create an arch between two 3D points
def generate_arch(p1, p2, height_factor=0.2, n_points=100):
# Normalize input points to lie on the unit sphere
p1 = p1 / np.linalg.norm(p1)
p2 = p2 / np.linalg.norm(p2)
# Compute angle between p1 and p2
omega = np.arccos(np.clip(np.dot(p1, p2), -1, 1))
if omega == 0:
return np.tile(p1, (n_points, 1)) # degenerate case
t = np.linspace(0, 1, n_points)
sin_omega = np.sin(omega)
# Spherical linear interpolation (slerp)
arch_points = (np.sin((1 - t)[:, None] * omega) * p1[None, :] +
np.sin(t[:, None] * omega) * p2[None, :]) / sin_omega
# Add radial height offset based on sine curve
heights = 1 + np.sin(np.pi * t) * height_factor
arch_points *= heights[:, None] # Scale outward from center
return arch_points
def traceroute(target):
# Run traceroute command
result = subprocess.run(['traceroute', '-n', target, '-q', '1', '-w', '1,3,10'], capture_output=True, text=True)
hops = re.findall(r'\n\s*\d+\s+([\d.]+)', result.stdout)
coords = []
for ip in hops:
try:
response = requests.get(f"http://ip-api.com/json/{ip}").json()
if response['status'] == 'success':
coords.append((response['lat'], response['lon']))
except Exception:
continue
return coords
def main():
#globe = pv.examples.load_globe()
locations = traceroute('news.ycombinator.com')
globe = pv.Sphere(radius=1.0, theta_resolution=120, phi_resolution=120,
start_theta=270.001, end_theta=270)
tex = pv.examples.load_globe_texture()
globe.active_texture_coordinates = np.zeros((globe.points.shape[0], 2))
globe.active_texture_coordinates[:, 0] = 0.5 + np.arctan2(globe.points[:, 1], globe.points[:, 0]) / (2 * math.pi)
globe.active_texture_coordinates[:, 1] = 0.5 + np.arcsin(globe.points[:, 2]) / math.pi
#locations = [
# (37.7749, -122.4194), # San Francisco
# (51.5074, -0.1278), # London
# (35.6895, 139.6917), # Tokyo
# (-33.8688, 151.2093), # Sydney
# (40.7128, -74.0060), # New York
#]
# Convert to 3D coordinates
points_3d = [latlon_to_xyz(lat, lon) for lat, lon in locations]
pl=pv.Plotter(off_screen=True)
pl.add_mesh(globe, color='tan', smooth_shading=True, texture=tex, show_edges=False)
for pt in points_3d:
city_marker = pv.Sphere(center=pt, radius=0.02)
pl.add_mesh(city_marker, color='blue')
for i in range(len(points_3d[:-1])):
arch = generate_arch(points_3d[i], points_3d[i+1], height_factor=0.5)
line = pv.lines_from_points(arch, close=False)
pl.add_mesh(line, color='red', line_width=2)
hide_cursor()
y, x = get_position()
print('\n' * 25, end='')
frames = []
try:
while True:
pl.camera.Azimuth(1)
image = pl.screenshot(transparent_background=True, window_size=(512, 512))
frames.append(Image.fromarray(image))
set_position(y-25, x)
draw_to_terminal(Image.fromarray(image))
#pl.show()
finally:
show_cursor()
if __name__ == '__main__':
main()