Resum curt (meta‑description):
Projecte en Python (Pygame) que dibuixa un mapa mundial estil WarGames, mostra la posició de l’ISS en temps real i incorpora botons tàctils autoajustables (128×64 px) per controlar capes i opcions. Compatible amb shapefiles de Natural Earth (1:50m) per a contorns reals.

1) Què fa aquest projecte?
- Renderitza un mapa equirectangular amb l’estètica verda/CRT de WarGames (scanlines, vinyeta, neó).
- Dibuixa graticula (paral·lels/meridians) amb etiquetes de graus.
- Carrega contorns reals dels continents des d’un shapefile de Natural Earth 1:50m (
ne_50m_land.shp). - Consulta cada 5 s la posició de l’Estació Espacial Internacional (ISS) i pinta:
- Un blip amb glow tipus radar.
- Un marcador triangular orientat amb el rumb entre les dues últimes posicions.
- La traça del recorregut, evitant el salt en el meridià ±180°.
- Incorpora una barra de botons tàctils (128×64 px, multi‑fila, autoajustable) per activar/aturar: Grid, Cities, Scanlines, Outline img, Land outline, ISS trail, Holes, Refresh, Screenshot, Fullscreen i Quit.
2) Requisits i instal·lació
- Python 3.8+ (provats en Ubuntu 20.04).
- Llibreries:
pip install pygame requests pyshp
- Mostrar más líneas
- Fitxers del shapefile de Natural Earth (al mateix directori que el script):
ne_50m_land.shpne_50m_land.shxne_50m_land.dbf(opcional) ne_50m_land.prj
Si no hi són, el programa cau al mode low‑poly integrat per seguir funcionant.
3) Arquitectura general del codi
El fitxer principal (per exemple war_games.py) organitza el projecte en 5 blocs:
- Estètica i utilitats: projecció (lon/lat → x/y), scanlines, vinyeta, text amb ombra i colors “neó”.
- Dades geoespacials: càrrega del shapefile amb PyShp, simplificació RDP i segmentació al dateline.
- Grid i capes: dibuix de paral·lels i meridians; “ciutats” estil blip.
- ISS: consulta a l’API
open-notify, càlcul de rumb i traça. - UI tàctil: classe
Button, distribució automàtica (128×64) i gestió d’esdeveniments.
A continuació, expliquem cada peça.
ef lonlat_to_xy(lon, lat, rect):
x = rect.left + (lon + 180.0) / 360.0 * rect.width
y = rect.top + (90.0 - lat) / 180.0 * rect.height
return int(x), int(y)
4) Projecció equirectangular i “look” WarGames
Conversió lon/lat → píxels
PythonMostrar más líneas
- Entrada: longitud (−180..180), latitud (−90..90) i el rectangle interior del mapa.
- Sortida: coordenades de pantalla (x, y) sobre una projecció equirectangular.
Efecte CRT (scanlines + vinyeta)
- Scanlines: línies horitzontals semitransparents cada 2 píxels.
- Vinyeta: cercles concèntrics foscos als marges per “tubar” el monitor.
Aquests overlays es recalculen quan canvia la mida de la finestra (resizable / fullscreen).
5) Contorns reals amb Shapefile (Natural Earth)
Lectura del Shapefile
S’utilitza PyShp per llegir ne_50m_land.shp. Recorda que Shapefile requereix .shx i .dbf al costat del .shp.
import shapefile
r = shapefile.Reader("ne_50m_land.shp")
for shape in r.shapes():
pts = shape.points # llista [(x, y), ...] en WGS84 (lon/lat)
parts = list(shape.parts) + [len(pts)] # parts = anells
# ...
Anells i “forats” (holes)
- Un polígon pot tenir anells exteriors i interiors.
- Per norma habitual, els exteriors solen ser horaris (CW) i els forats antihoraris (CCW).
- El codi calcula l’àrea signada (shoelace) per distingir-los i, per defecte, ignora els forats (es poden activar amb el botó HOLES).
Simplificació (RDP) i dateline
- Ramer–Douglas–Peucker (RDP): redueix vèrtexs mantenint la forma; paràmetre
epsilonajustable (0.04per defecte) per balancejar detall i FPS. - Dateline (±180°): si un segment “salta” ~>170° de longitud, es segmenta en trams separats per evitar línies que travessen tot el mapa.
Resultat: el dibuix és continu, fiable al meridià ±180° i prou lleuger per temps real.
6) Grid i “blips” de ciutats
- Meridians/Paral·lels: línies discontínues amb gruix/color diferenciat per les principals (cada 30° i 20°).
- Etiquetes de graus: sobre i sota per meridians; a esquerra/dreta pels paral·lels.
- Ciutats: punts amb glow verd i etiqueta estable sense “tremolor”.
7) ISS en temps real
- Consulta a l’API
http://api.open-notify.org/iss-now.jsoncada 5 segons. - Rumb: es calcula l’angle geodèsic aproximat entre l’última i la penúltima posició per orientar el marcador triangular.
- Traça: llista de punts (lat, lon) limitada (p. ex. 720) i segmentada al dateline per evitar salts.
8) Botons tàctils (128×64) autoajustables
Classe Button
Gestió del rectangle, estat “premut” i dibuix amb borde verd neó i text centrat.
Distribució automàtica i reserva d’espai
La funció layout_buttons(...) calcula:
- Quants botons caben per fila segons l’amplada.
- Quantes files calen.
- Un rectangle de “softbar” al peu.
El rect del mapa es redueix per no trepitjar la barra de botons.
Esdeveniments tàctils
En SDL2/Pygame 2, molts tocs arriben com a MOUSEBUTTONDOWN. Amb això, els botons són tàctils sense canvis.
9) Controls disponibles
- Teclat:
GGrid ·CCities ·SScanlines ·OOutline img ·LLand outline ·IISS trail ·HHoles ·RRefresh ·PScreenshot ·FFullscreen ·ESC/QSortir - Tàctil: els botons al peu repliquen exactament aquestes accions.
10) Rendiment i consells
- Si el FPS baixa (sobretot amb shapefile 10m), puja
simplify_eps(per ex.0.06). - Si no tens el shapefile a mà, el codi fa fallback a un perfil low‑poly integrat.
- Les scanlines/vinyeta són overlays; pots desactivar-les per guanyar FPS.
11) Com executar
Assegura’t que el shapefile està al mateix directori o adapta la ruta dins del codi. Per a pantalles tàctils, simplement obre en fullscreen (
F) i utilitza la barra de botons.
12) Depuració habitual
- “No se encontró shapefile: ne_50m_land.shp” → col·loca
.shp/.shx/.dbfal directori del script (o ajusta la ruta). - Baix rendiment → puja
simplify_epsi/o desactivaSCANLINES. - No respon el botó → comprova que
MOUSEBUTTONDOWNno estigui capturat per cap altre handler i quebtn.rectcol·lisiona amb el mouse pos.
13) Fragments de codi clau (copiables al post)
a) Carregar i preparar els contorns (PyShp + RDP + dateline)
b) Dibuix de la traça ISS sense salts al ±180°
c) Botons tàctils autoajustables (128×64)
14) Llicència i dades
- El codi de l’entrada el pots publicar amb la llicència que prefereixis.
- Les dades Natural Earth són de domini públic (ideal per a projectes oberts).
- L’API
open-notifyés oberta; si vols més metadades de l’ISS, es pot integrar TLE i propagadors (SGP4) en futurs posts.
15) Descàrrega i variants
- Variant sense shapefile (només low‑poly) per a demostracions ràpides.
- Variant GeoJSON (en lloc de Shapefile) si prefereixes no dependre de PyShp.
- Afegir rius/llacs o límit polític (altres capes Natural Earth).
- Mode “Night Terminator” (gradients dia/nit) per donar un toc astronòmic.
# -*- coding: utf-8 -*-
"""
WARGAMES + ISS tracker (Pygame)
- Contornos reales desde shapefile Natural Earth 50m (ne_50m_land.shp)
- Botones táctiles 128x64 auto-ajustables en la parte inferior
Dependencias:
pip install pygame requests pyshp
Coloca en el mismo directorio:
ne_50m_land.shp, ne_50m_land.shx, ne_50m_land.dbf (y opcionalmente .prj)
"""
import os
import math
import random
import time
from datetime import datetime
import pygame
import requests
# --- PyShp (shapefile) ---
try:
import shapefile
except ImportError:
shapefile = None
# ----------------------- Config -----------------------
WIDTH, HEIGHT = 1280, 720
MAP_MARGIN = 60
BG_COLOR = (5, 8, 6)
NEON = (0, 255, 100)
GRID_COLOR = (0, 180, 90)
TEXT_COLOR = (120, 255, 170)
SCANLINE_ALPHA = 28
VIGNETTE_ALPHA = 80
FPS = 60
# Botones (tamaño fijo, distribución auto)
BTN_W, BTN_H =100, 64
BTN_GAP = 10 # separación entre botones
BTN_PAD = 12 # padding inferior
BTN_RADIUS = 10 # esquinas redondeadas
LAT_MAJOR = list(range(-80, 81, 20))
LAT_MINOR = list(range(-90, 91, 10))
LON_MAJOR = list(range(-180, 181, 30))
LON_MINOR = list(range(-180, 181, 10))
# ------------------------ Ciudades para el estilo WarGames ------------------------
CITIES = [
("Washington", 38.90, -77.04), ("New York", 40.71, -74.01),
("Los Angeles", 34.05, -118.24), ("London", 51.51, -0.13),
("Paris", 48.86, 2.35), ("Moscow", 55.75, 37.62),
("Berlin", 52.52, 13.40), ("Tokyo", 35.68, 139.69),
("Beijing", 39.90, 116.40), ("Delhi", 28.61, 77.21),
("Sydney", -33.87, 151.21), ("Cairo", 30.04, 31.24),
("Johannesburg", -26.20, 28.04), ("Rio de Janeiro", -22.90, -43.20),
("Buenos Aires", -34.60, -58.38),
]
# ------------------------ Utilidades de proyección y estilo ------------------------
def lonlat_to_xy(lon, lat, rect):
x = rect.left + (lon + 180.0) / 360.0 * rect.width
y = rect.top + (90.0 - lat) / 180.0 * rect.height
return int(x), int(y)
def draw_dashed_line(surface, color, start, end, dash_len=10, gap_len=6, width=1):
sx, sy = start; ex, ey = end
dx, dy = ex - sx, ey - sy
dist = math.hypot(dx, dy)
if dist == 0: return
vx, vy = dx / dist, dy / dist
drawn = 0.0
while drawn < dist:
seg = min(dash_len, dist - drawn)
x1 = int(sx + vx * drawn); y1 = int(sy + vy * drawn)
x2 = int(sx + vx * (drawn + seg)); y2 = int(sy + vy * (drawn + seg))
pygame.draw.line(surface, color, (x1, y1), (x2, y2), width)
drawn += dash_len + gap_len
def draw_glow_point(surface, x, y, base_color=NEON, r=6):
glow = pygame.Surface((r*6, r*6), pygame.SRCALPHA)
cx = cy = r*3
for i, alpha in enumerate([140, 90, 60, 30, 15]):
pygame.draw.circle(glow, (*base_color, alpha), (cx, cy), r + i*2)
surface.blit(glow, (x - cx, y - cy))
pygame.draw.circle(surface, base_color, (x, y), max(1, r//2))
def make_scanlines(w, h, alpha=SCANLINE_ALPHA):
overlay = pygame.Surface((w, h), pygame.SRCALPHA)
line_color = (0, 0, 0, alpha)
for y in range(0, h, 2):
overlay.fill(line_color, rect=pygame.Rect(0, y, w, 1))
return overlay
def make_vignette(w, h, alpha=VIGNETTE_ALPHA):
vignette = pygame.Surface((w, h), pygame.SRCALPHA)
center = (w/2, h/2)
max_r = math.hypot(w/2, h/2)
for i in range(0, int(max_r), 8):
a = int(alpha * (i / max_r))
pygame.draw.circle(vignette, (0, 0, 0, a), center, i, width=8)
return vignette
def text(surface, font, s, pos, color=TEXT_COLOR, shadow=True):
if shadow:
sh = font.render(s, True, (20, 40, 30))
surface.blit(sh, (pos[0]+1, pos[1]+1))
img = font.render(s, True, color)
surface.blit(img, pos)
# ------------------------ LOW-POLY integrado (fallback) ------------------------
CONTINENTS = {
"Africa": [(-5, 35), (0, 36), (15, 33), (25, 33), (29, 31), (31, 22),
(36, 0), (40, 6), (43, 12), (51, 9), (40, -20), (32, -28),
(21, -33), (12, -23), (14, -15), (12, -5), (6, 5),
(-7, 4), (-18, 15), (-15, 30), (-5, 35)],
"SouthAmerica": [(-62, 10), (-77, 8), (-80, -5), (-78, -15), (-75, -25),
(-70, -50), (-62, -55), (-48, -36), (-38, -13), (-35, -6),
(-50, 0), (-62, 10)],
"NorthAmerica": [(-80, 25), (-90, 18), (-117, 32), (-125, 40), (-150, 60),
(-140, 70), (-95, 70), (-70, 55), (-66, 45), (-80, 25)],
"Europe": [(-6, 36), (0, 43), (10, 45), (15, 46), (20, 48),
(14, 50), (10, 54), (5, 58), (20, 70), (40, 68),
(30, 60), (20, 52), (36, 45), (29, 41), (12, 40),
(7, 43), (-6, 36)],
"Asia": [(41, 40), (50, 42), (60, 45), (70, 50), (100, 70), (140, 70),
(146, 45), (120, 20), (103, 10), (95, 5), (75, 10),
(68, 25), (60, 30), (50, 35), (41, 40)],
"Australia": [(113, -10), (128, -15), (146, -25), (150, -35), (147, -43),
(140, -37), (125, -35), (113, -25), (113, -10)],
"Greenland": [(-45, 60), (-20, 75), (-45, 83), (-60, 75), (-45, 60)],
"UK": [(-8, 50), (-3, 58), (-1, 57), (-2, 54), (-5, 51), (-8, 50)],
"Japan": [(141, 45), (141, 41), (139, 36), (135, 33), (130, 32), (140, 39), (141, 45)],
"Madagascar": [(49, -12), (48, -25), (50, -25), (50, -12), (49, -12)],
"NewZealand": [(173, -35), (170, -45), (167, -46), (175, -38), (173, -35)],
"Antarctica": [(-180, -65), (-120, -75), (-60, -80), (0, -78), (60, -75),
(120, -70), (180, -65), (-180, -65)]
}
# ------------------------ ISS ------------------------
ISS_ALT_KM = 420
ISS_SPEED_KMH = 27600
def fetch_iss_position(timeout=8):
url = "http://api.open-notify.org/iss-now.json"
r = requests.get(url, timeout=timeout)
data = r.json()
lat = float(data["iss_position"]["latitude"])
lon = float(data["iss_position"]["longitude"])
ts = int(data.get("timestamp", time.time()))
return lat, lon, ts
def normalize_lon(lon):
while lon > 180: lon -= 360
while lon < -180: lon += 360
return lon
def bearing_deg(lat1, lon1, lat2, lon2):
φ1, λ1, φ2, λ2 = map(math.radians, [lat1, lon1, lat2, lon2])
dλ = λ2 - λ1
y = math.sin(dλ) * math.cos(φ2)
x = math.cos(φ1) * math.sin(φ2) - math.sin(φ1) * math.cos(φ2) * math.cos(dλ)
θ = math.degrees(math.atan2(y, x))
return (θ + 360) % 360
def draw_iss_marker(surface, rect, lat, lon, prev_lat=None, prev_lon=None):
lon = normalize_lon(lon)
x, y = lonlat_to_xy(lon, lat, rect)
draw_glow_point(surface, x, y, NEON, r=7)
angle = 0.0
if prev_lat is not None and prev_lon is not None:
angle = bearing_deg(prev_lat, prev_lon, lat, lon)
rad = math.radians(90 - angle)
size = 10
tip = (x + int(math.cos(rad) * size), y - int(math.sin(rad) * size))
left = (x + int(math.cos(rad + 2.7) * size*0.7), y - int(math.sin(rad + 2.7) * size*0.7))
right = (x + int(math.cos(rad - 2.7) * size*0.7), y - int(math.sin(rad - 2.7) * size*0.7))
pygame.draw.polygon(surface, NEON, [tip, left, right], width=2)
text(surface, pygame.font.SysFont("Consolas", 16), "ISS", (x + 12, y - 18))
def draw_iss_trail(surface, rect, trail_lonlat):
if len(trail_lonlat) < 2: return
segment = []
last_lon = None
for (lat, lon) in trail_lonlat:
lon = normalize_lon(lon)
if last_lon is not None and abs(lon - last_lon) > 170:
if len(segment) > 1:
pts = [lonlat_to_xy(normalize_lon(lo), la, rect) for (la, lo) in segment]
pygame.draw.lines(surface, NEON, False, pts, 2)
segment = []
segment.append((lat, lon))
last_lon = lon
if len(segment) > 1:
pts = [lonlat_to_xy(normalize_lon(lo), la, rect) for (la, lo) in segment]
pygame.draw.lines(surface, NEON, False, pts, 2)
# ------------------------ RDP + dateline ------------------------
def _perp_dist(p, a, b):
(px, py), (ax, ay), (bx, by) = p, a, b
if a == b:
return math.hypot(px - ax, py - ay)
t = ((px - ax) * (bx - ax) + (py - ay) * (by - ay)) / ((bx - ax)**2 + (by - ay)**2)
t = max(0.0, min(1.0, t))
x = ax + t * (bx - ax); y = ay + t * (by - ay)
return math.hypot(px - x, py - y)
def simplify_rdp(points, epsilon=0.04):
if len(points) < 3 or epsilon <= 0:
return points[:]
keep = [False] * len(points)
keep[0] = keep[-1] = True
stack = [(0, len(points) - 1)]
while stack:
start, end = stack.pop()
max_dist = 0.0; idx = None
a, b = points[start], points[end]
for i in range(start + 1, end):
d = _perp_dist(points[i], a, b)
if d > max_dist:
max_dist, idx = d, i
if max_dist > epsilon and idx is not None:
keep[idx] = True
stack.append((start, idx))
stack.append((idx, end))
return [pt for i, pt in enumerate(points) if keep[i]]
def segment_for_dateline(ring):
if not ring: return []
segments = []
seg = [(normalize_lon(ring[0][0]), ring[0][1])]
last_lon = seg[0][0]
for lon, lat in ring[1:]:
lon = normalize_lon(lon)
if abs(lon - last_lon) > 170:
if len(seg) > 1:
segments.append(seg)
seg = []
seg.append((lon, lat))
last_lon = lon
if len(seg) > 1:
segments.append(seg)
return segments
def signed_area(ring):
if len(ring) < 3: return 0.0
s = 0.0
for i in range(len(ring)):
x1, y1 = ring[i]
x2, y2 = ring[(i+1) % len(ring)]
s += x1 * y2 - x2 * y1
return 0.5 * s # >0 CCW (agujero), <0 CW (exterior)
# ------------------------ Shapefile Natural Earth ------------------------
def load_shp_land_segments(path, simplify_eps=0.04, include_holes=False):
if shapefile is None:
print("PyShp no está instalado. Instala con: pip install pyshp")
return None
if not os.path.isfile(path):
print("No se encontró shapefile:", path)
return None
try:
r = shapefile.Reader(path)
except Exception as e:
print("Error leyendo shapefile:", e)
return None
segments = []
for shp in r.shapes():
pts = shp.points
parts = list(shp.parts) + [len(pts)]
for i in range(len(parts) - 1):
start = parts[i]; end = parts[i+1]
ring = [(float(x), float(y)) for (x, y) in pts[start:end]]
if ring and ring[0] != ring[-1]:
ring.append(ring[0])
area = signed_area(ring)
is_hole = area > 0 # CCW => agujero
if not include_holes and is_hole:
continue
ring_simpl = simplify_rdp(ring, epsilon=simplify_eps)
segs = segment_for_dateline(ring_simpl)
segments.extend(segs)
return segments
# ------------------------ Grid y ciudades ------------------------
def draw_grid(surface, rect, font):
for lon in LON_MINOR:
x1, y1 = lonlat_to_xy(lon, -90, rect)
x2, y2 = lonlat_to_xy(lon, 90, rect)
color = GRID_COLOR if lon % 30 != 0 else NEON
width = 1 if lon % 30 != 0 else 2
draw_dashed_line(surface, color, (x1, y1), (x2, y2), dash_len=12, gap_len=6, width=width)
for lat in LAT_MINOR:
x1, y1 = lonlat_to_xy(-180, lat, rect)
x2, y2 = lonlat_to_xy(180, lat, rect)
color = GRID_COLOR if lat % 20 != 0 else NEON
width = 1 if lat % 20 != 0 else 2
draw_dashed_line(surface, color, (x1, y1), (x2, y2), dash_len=12, gap_len=6, width=width)
for lon in LON_MAJOR:
xt, yt_top = lonlat_to_xy(lon, 90, rect)
_, yt_bottom = lonlat_to_xy(lon, -90, rect)
label = f"{lon}°"
text(surface, font, label, (xt-14, yt_top-24))
text(surface, font, label, (xt-14, yt_bottom+8))
for lat in LAT_MAJOR:
xl, yl = lonlat_to_xy(-180, lat, rect)
xr, yr = lonlat_to_xy(180, lat, rect)
hemi = "N" if lat > 0 else ("S" if lat < 0 else "")
label = f"{abs(lat)}°{hemi}"
text(surface, font, label, (xl-50, yl-10))
text(surface, font, label, (xr+12, yr-10))
def draw_cities(surface, rect, font):
for name, lat, lon in CITIES:
x, y = lonlat_to_xy(lon, lat, rect)
draw_glow_point(surface, x, y, NEON, r=6)
text(surface, font, name.upper(), (x + 10, y - 14))
# ------------------------ Botones táctiles ------------------------
class Button:
def __init__(self, key, label):
self.key = key # identificador lógico
self.label = label # texto del botón
self.rect = pygame.Rect(0, 0, BTN_W, BTN_H)
self.pressed = False
def draw(self, surface, font):
# Fondo boton
bg = (10, 20, 15) if not self.pressed else (20, 40, 30)
pygame.draw.rect(surface, bg, self.rect, border_radius=BTN_RADIUS)
# Borde neón
pygame.draw.rect(surface, NEON, self.rect, width=2, border_radius=BTN_RADIUS)
# Etiqueta centrada
label = font.render(self.label, True, TEXT_COLOR)
lx = self.rect.centerx - label.get_width()//2
ly = self.rect.centery - label.get_height()//2
surface.blit(label, (lx, ly))
def layout_buttons(buttons, screen_w, screen_h):
"""
Calcula posición de botones en filas al pie de pantalla.
Devuelve (softbar_rect, rows_count).
"""
# ¿cuántos caben por fila?
inner_w = screen_w - MAP_MARGIN*2
per_row = max(1, (inner_w + BTN_GAP) // (BTN_W + BTN_GAP))
rows = math.ceil(len(buttons) / per_row)
# Altura total de la barra
softbar_h = rows*BTN_H + (rows-1)*BTN_GAP + BTN_PAD
softbar_rect = pygame.Rect(
MAP_MARGIN,
screen_h - softbar_h - BTN_PAD,
inner_w,
softbar_h
)
# Colocar cada botón
x0 = softbar_rect.left
y_start = softbar_rect.bottom - BTN_PAD - rows*BTN_H - (rows-1)*BTN_GAP
for idx, btn in enumerate(buttons):
row = idx // per_row
col = idx % per_row
# centrar la fila si hay hueco
used_w = min(per_row, len(buttons) - row*per_row)*(BTN_W + BTN_GAP) - BTN_GAP
row_x0 = x0 + (inner_w - used_w)//2
x = row_x0 + col*(BTN_W + BTN_GAP)
y = y_start + row*(BTN_H + BTN_GAP)
btn.rect.topleft = (x, y)
return softbar_rect, rows
# ------------------------ Dibujo de continentes ------------------------
def draw_continents_outline(screen, rect, dash=False, from_segments=None):
color = NEON; width = 2
def draw_segment(seg):
pts = [lonlat_to_xy(lon, lat, rect) for (lon, lat) in seg]
for i in range(len(pts) - 1):
if dash:
draw_dashed_line(screen, color, pts[i], pts[i+1], dash_len=12, gap_len=6, width=width)
else:
pygame.draw.line(screen, color, pts[i], pts[i+1], width)
if from_segments:
for seg in from_segments:
draw_segment(seg)
else:
for _, poly in CONTINENTS.items():
pts = [lonlat_to_xy(lon, lat, rect) for (lon, lat) in poly]
for i in range(len(pts) - 1):
pygame.draw.line(screen, color, pts[i], pts[i+1], width)
if len(pts) > 1:
pygame.draw.line(screen, color, pts[-1], pts[0], width)
# ------------------------ Main ------------------------
def main():
pygame.init()
flags = pygame.RESIZABLE
screen = pygame.display.set_mode((WIDTH, HEIGHT), flags)
pygame.display.set_caption("WARGAMES - GLOBAL MAP GRID + ISS TRACKER (Touch Buttons)")
clock = pygame.time.Clock()
try:
font = pygame.font.SysFont("Consolas", 16)
big = pygame.font.SysFont("Consolas", 20)
except:
font = pygame.font.SysFont(None, 16)
big = pygame.font.SysFont(None, 20)
scanlines = make_scanlines(WIDTH, HEIGHT, SCANLINE_ALPHA)
vignette = make_vignette(WIDTH, HEIGHT, VIGNETTE_ALPHA)
outline_img = None
if os.path.isfile("world_outline.png"):
try:
img = pygame.image.load("world_outline.png").convert_alpha()
outline_img = img
except Exception as e:
print("No se pudo cargar world_outline.png:", e)
# Shapefile Land
land_shp_path = "ne_50m_land.shp"
include_holes = False
simplify_eps = 0.04
land_segments = load_shp_land_segments(land_shp_path, simplify_eps=simplify_eps, include_holes=include_holes)
if land_segments:
print(f"[LAND] cargados {len(land_segments)} segmentos desde {land_shp_path}")
else:
print("[LAND] usando perfil low‑poly integrado (no se cargó shapefile).")
# Estados de visualización
show_grid = True
show_cities = True
show_scanlines = True
show_outline_img = outline_img is not None
show_land_outline = True
show_iss_trail = True
fullscreen = False
# ISS
iss_lat = iss_lon = iss_ts = None
prev_lat = prev_lon = None
trail = []
last_fetch_ms = 0
FETCH_INTERVAL_MS = 5000
# --- Botones táctiles ---
buttons = [
Button("grid", "GRID"),
Button("cities", "CITIES"),
Button("scanlines", "SCANLINES"),
Button("outline_img", "OUTLINE IMG"),
Button("land", "LAND OUTLINE"),
Button("iss_trail", "ISS TRAIL"),
Button("holes", "HOLES"),
Button("refresh", "REFRESH"),
Button("screenshot", "SHOT"),
Button("fullscreen", "FULLSCREEN"),
Button("quit", "QUIT"),
]
running = True
while running:
dt = clock.tick(FPS) / 1000.0
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
elif event.type == pygame.KEYDOWN:
if event.key in (pygame.K_ESCAPE, pygame.K_q):
running = False
elif event.key == pygame.K_g:
show_grid = not show_grid
elif event.key == pygame.K_c:
show_cities = not show_cities
elif event.key == pygame.K_s:
show_scanlines = not show_scanlines
elif event.key == pygame.K_o:
show_outline_img = not show_outline_img
elif event.key == pygame.K_l:
show_land_outline = not show_land_outline
elif event.key == pygame.K_i:
show_iss_trail = not show_iss_trail
elif event.key == pygame.K_h:
include_holes = not include_holes
land_segments = load_shp_land_segments(land_shp_path, simplify_eps=simplify_eps, include_holes=include_holes)
elif event.key == pygame.K_r:
last_fetch_ms = 0
elif event.key == pygame.K_p:
pygame.image.save(screen, "wargames_iss.png")
elif event.key == pygame.K_f:
fullscreen = not fullscreen
if fullscreen:
screen = pygame.display.set_mode((0, 0), pygame.FULLSCREEN)
else:
screen = pygame.display.set_mode((WIDTH, HEIGHT), flags)
# Táctil / ratón sobre botones
elif event.type == pygame.MOUSEBUTTONDOWN and event.button == 1:
mx, my = event.pos
for btn in buttons:
if btn.rect.collidepoint(mx, my):
btn.pressed = True
# Ejecutar acción
if btn.key == "grid":
show_grid = not show_grid
elif btn.key == "cities":
show_cities = not show_cities
elif btn.key == "scanlines":
show_scanlines = not show_scanlines
elif btn.key == "outline_img":
show_outline_img = not show_outline_img
elif btn.key == "land":
show_land_outline = not show_land_outline
elif btn.key == "iss_trail":
show_iss_trail = not show_iss_trail
elif btn.key == "holes":
include_holes = not include_holes
land_segments = load_shp_land_segments(land_shp_path, simplify_eps=simplify_eps, include_holes=include_holes)
elif btn.key == "refresh":
last_fetch_ms = 0
elif btn.key == "screenshot":
pygame.image.save(screen, "wargames_iss.png")
elif btn.key == "fullscreen":
fullscreen = not fullscreen
if fullscreen:
screen = pygame.display.set_mode((0, 0), pygame.FULLSCREEN)
else:
screen = pygame.display.set_mode((WIDTH, HEIGHT), flags)
elif btn.key == "quit":
running = False
elif event.type == pygame.MOUSEBUTTONUP and event.button == 1:
for btn in buttons:
btn.pressed = False
w, h = screen.get_size()
if scanlines.get_size() != (w, h):
scanlines = make_scanlines(w, h, SCANLINE_ALPHA)
vignette = make_vignette(w, h, VIGNETTE_ALPHA)
# Distribuir botones y reservar altura de barra
softbar_rect, rows = layout_buttons(buttons, w, h)
softbar_h = rows*BTN_H + (rows-1)*BTN_GAP + BTN_PAD*2
# Fondo con leve flicker
flicker = random.randint(0, 2)
screen.fill((BG_COLOR[0] + flicker, BG_COLOR[1], BG_COLOR[2]))
# Rect del mapa, restando barra inferior de botones
rect = pygame.Rect(MAP_MARGIN, MAP_MARGIN, w - MAP_MARGIN*2, h - MAP_MARGIN*2 - softbar_h)
pygame.draw.rect(screen, NEON, rect, width=2)
# Outline por imagen (opcional)
if show_outline_img and outline_img is not None:
img = pygame.transform.smoothscale(outline_img, (rect.width, rect.height))
tint = pygame.Surface(img.get_size(), pygame.SRCALPHA)
tint.fill((*NEON, 0))
img.blit(tint, (0, 0), special_flags=pygame.BLEND_ADD)
screen.blit(img, rect.topleft)
# Perfil continental
if show_land_outline:
draw_continents_outline(screen, rect, dash=False, from_segments=land_segments)
# Grid / ciudades
if show_grid:
draw_grid(screen, rect, font)
if show_cities:
draw_cities(screen, rect, font)
# Actualización ISS cada 5s
now_ms = pygame.time.get_ticks()
if now_ms - last_fetch_ms > 5000:
last_fetch_ms = now_ms
try:
lat, lon, ts = fetch_iss_position(timeout=8)
prev_lat, prev_lon = iss_lat, iss_lon
iss_lat, iss_lon, iss_ts = lat, lon, ts
if iss_lat is not None and iss_lon is not None:
trail.append((iss_lat, iss_lon))
if len(trail) > 720: trail.pop(0)
except Exception:
pass
if show_iss_trail and trail:
draw_iss_trail(screen, rect, trail)
if iss_lat is not None and iss_lon is not None:
draw_iss_marker(screen, rect, iss_lat, iss_lon, prev_lat, prev_lon)
# Título / HUD superior
title = "GLOBAL THERMAL MAP INTERFACE // WARGAMES"
text(screen, big, title, (MAP_MARGIN, 18))
# Dibujar barra y botones
# (ligero fondo semitransparente debajo para contraste)
bar_bg = pygame.Surface((softbar_rect.width, softbar_rect.height), pygame.SRCALPHA)
bar_bg.fill((0, 0, 0, 80))
screen.blit(bar_bg, softbar_rect.topleft)
for btn in buttons:
btn.draw(screen, font)
# Efectos CRT
if show_scanlines:
screen.blit(scanlines, (0, 0))
screen.blit(vignette, (0, 0))
pygame.display.flip()
pygame.quit()
if __name__ == "__main__":
main()
Debe estar conectado para enviar un comentario.