Files
tianxuan/scripts/gen_world_map.py
TianXuan Developer 66e3649152 feat: static SVG world map + large clickable points + edge detail
- Generate high-quality SVG world map (static, no tiles)
- Points: radius 7, white border, high-contrast subnet colors
- Node click: show detail panel + highlight edges on map
- Highlighted edges: clickable, show edge detail panel
- Edge detail: Beijing time, avg bytes, src/dst IP clickable
- Map init: requestAnimationFrame ensures container has dimensions
- Cleanup: remove polygon-based continent drawing code

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-23 21:58:02 +08:00

220 lines
8.8 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
"""Generate a clean world map PNG using only stdlib (struct + zlib).
Output: static/simple_analysis/world_map.png (2048×1024, ~50KB)
"""
import struct, zlib, math
from pathlib import Path
W, H = 2048, 1024
def lon_to_x(lon): return int((lon + 180) / 360 * W)
def lat_to_y(lat): return int((90 - lat) / 180 * H)
# ── Continent outlines (simplified Natural Earth-style paths) ──
# Each entry: (fill_rgb, outline_rgb, [(lat, lon), ...])
CONTINENTS = [
# North America
((220, 228, 235), (160, 172, 186), [
(72, -168),(71, -156),(70, -144),(68, -136),(66, -130),(64, -124),(62, -118),
(60, -114),(58, -110),(56, -106),(54, -100),(52, -96),(50, -92),(48, -88),
(46, -84),(44, -80),(42, -78),(40, -76),(38, -76),(35, -78),(32, -80),
(30, -82),(28, -84),(26, -82),(24, -80),(22, -78),(20, -76),(18, -78),
(16, -82),(14, -86),(12, -88),(9, -82),(8, -80),(8, -78),(9, -76),
(12, -72),(15, -66),(18, -62),(21, -60),(25, -60),(28, -58),(32, -58),
(35, -57),(35, -50),(34, -42),(35, -35),(40, -30),(45, -28),(50, -26),
(55, -22),(60, -18),(65, -14),(70, -16),(75, -22),(78, -32),(80, -48),
(82, -60),(83, -72),(84, -84),(82, -98),(80, -115),(78, -135),(76, -155),(72, -168),
]),
# Greenland
((220, 228, 235), (160, 172, 186), [
(83, -55),(82, -30),(80, -20),(78, -16),(76, -18),(75, -24),
(76, -34),(78, -46),(80, -58),(82, -62),(83, -55),
]),
# South America
((215, 225, 233), (160, 172, 186), [
(12, -72),(10, -68),(8, -62),(5, -55),(3, -50),(0, -46),(-2, -42),
(-4, -38),(-7, -34),(-10, -32),(-13, -30),(-16, -28),(-18, -26),
(-22, -24),(-25, -26),(-28, -30),(-32, -34),(-34, -38),(-37, -40),
(-40, -44),(-44, -48),(-50, -50),(-54, -52),(-55, -48),(-54, -44),
(-52, -40),(-48, -36),(-44, -34),(-40, -32),(-36, -30),(-32, -28),
(-28, -26),(-24, -24),(-20, -22),(-16, -24),(-12, -28),(-8, -34),
(-4, -40),(0, -46),(4, -52),(8, -60),(12, -72),
]),
# Europe
((218, 226, 234), (160, 172, 186), [
(71, -6),(70, 5),(69, 10),(66, 14),(62, 15),(58, 15),(54, 14),(50, 12),
(46, 8),(44, 2),(43, -3),(44, -6),(46, -9),(48, -10),(50, -8),(53, -6),
(55, -4),(57, -2),(59, -5),(61, -8),(64, -12),(67, -10),(70, -6),
]),
# UK
((218, 226, 234), (160, 172, 186), [
(58.5, -6.5),(57.5, -3),(55.5, -1.5),(54, -3),(52.5, -5),(51.5, -8),
(53, -10),(55, -8),(57, -6),(58.5,-6.5),
]),
# Scandinavia
((218, 226, 234), (160, 172, 186), [
(71, 7),(70, 14),(68, 16),(65, 15),(62, 13),(60, 9),(58, 6),
(56, 8),(55, 12),(56, 17),(58, 23),(60, 30),(62, 32),(65, 30),
(68, 26),(70, 20),(71, 12),(71, 7),
]),
# Africa
((225, 232, 238), (165, 174, 186), [
(37, -9),(35, -2),(33, 5),(30, 12),(28, 18),(25, 24),(22, 30),
(18, 36),(15, 39),(10, 42),(5, 43),(-2, 42),(-6, 38),(-10, 34),
(-14, 30),(-17, 26),(-20, 22),(-24, 18),(-27, 14),(-30, 10),
(-33, 6),(-35, 2),(-33, -3),(-28, -6),(-22, -8),(-18, -6),
(-12, -3),(-6, 0),(0, -2),(6, -4),(12, -6),(18, -8),(24, -10),
(30, -10),(34, -9),(37, -9),
]),
# Asia
((222, 229, 236), (160, 172, 186), [
(72, 42),(70, 55),(68, 72),(66, 90),(64, 105),(62, 122),(60, 138),
(56, 152),(52, 158),(48, 162),(44, 166),(40, 170),(37, 172),(34, 170),
(30, 168),(28, 164),(26, 158),(24, 152),(22, 148),(18, 144),(15, 140),
(12, 135),(10, 130),(8, 125),(5, 120),(2, 115),(-2, 110),(-5, 105),
(-8, 100),(-10, 104),(-13, 108),(-16, 114),(-20, 118),(-24, 112),
(-28, 108),(-32, 104),(-35, 100),(-38, 95),(-40, 90),(-42, 84),
(-45, 78),(-48, 74),(-50, 70),(-56, 66),(-60, 62),(-62, 58),
(-60, 54),(-58, 50),(-54, 46),(-50, 42),(-46, 38),(-44, 36),
(-42, 40),(-40, 46),(-36, 50),(-32, 54),(-28, 55),(-24, 52),
(-20, 48),(-18, 46),(-14, 48),(-10, 52),(-6, 54),(-2, 50),
(0, 46),(3, 42),(6, 38),(9, 35),(12, 38),(15, 44),(18, 50),
(22, 56),(26, 62),(30, 68),(34, 72),(38, 75),(42, 78),
(46, 80),(52, 78),(58, 74),(62, 70),(66, 66),(70, 58),(72, 50),(72, 42),
]),
# Japan
((222, 229, 236), (160, 172, 186), [
(45.5, 142),(43, 141),(40, 140),(37, 139),(34, 137),(32, 136),(30, 138),
(32, 142),(35, 144),(38, 143),(41, 143),(44, 144),(45.5, 142),
]),
# SE Asia islands
((222, 229, 236), (160, 172, 186), [
(7, 116),(-2, 112),(-6, 106),(-8, 108),(-6, 113),(-2, 116),(2, 119),(5, 118),(7, 116),
]),
((222, 229, 236), (160, 172, 186), [
(1, 126),(-4, 130),(-7, 132),(-7, 136),(-4, 140),(0, 141),(3, 139),(4, 135),(1, 126),
]),
# Australia
((222, 229, 236), (160, 172, 186), [
(-12, 130),(-16, 128),(-20, 126),(-26, 128),(-32, 132),(-36, 138),
(-39, 143),(-40, 148),(-38, 152),(-34, 154),(-29, 151),(-24, 146),
(-20, 142),(-17, 138),(-14, 134),(-12, 130),
]),
# New Zealand
((222, 229, 236), (160, 172, 186), [
(-35, 172.5),(-38, 170),(-41, 172),(-45, 173),(-47, 172),
(-45, 169),(-42, 170),(-38, 172),(-35, 172.5),
]),
# Antarctica
((238, 240, 243), (190, 195, 200), [
(-66, -175),(-66, -130),(-68, -80),(-70, -40),(-72, 0),
(-72, 40),(-70, 80),(-68, 130),(-66, 175),
(-75, 175),(-78, 130),(-80, 80),(-82, 40),(-82, 0),
(-80, -40),(-78, -80),(-75, -130),(-72, -175),(-66, -175),
]),
]
def draw_filled_polygon(pixels, fill, outline, pts):
"""Draw a filled polygon using scanline algorithm."""
h = len(pixels); w = len(pixels[0]) if h > 0 else 0
if w == 0: return
# Convert lat/lon to pixel coordinates
px = [lon_to_x(lon) for (lat, lon) in pts]
py = [lat_to_y(lat) for (lat, lon) in pts]
n = len(px)
if n < 3: return
# Bounding box
min_y = max(0, min(py)); max_y = min(h - 1, max(py))
# Scanline fill
for y in range(min_y, max_y + 1):
intersections = []
for i in range(n):
y1, y2 = py[i], py[(i + 1) % n]
x1, x2 = px[i], px[(i + 1) % n]
if y1 == y2: continue
if (y1 <= y < y2) or (y2 <= y < y1):
t = (y - y1) / (y2 - y1)
x = int(x1 + t * (x2 - x1))
intersections.append(x)
intersections.sort()
for i in range(0, len(intersections) - 1, 2):
x_start = max(0, intersections[i])
x_end = min(w - 1, intersections[i + 1] if i + 1 < len(intersections) else w - 1)
for x in range(x_start, x_end + 1):
pixels[y][x] = fill
# Draw outline
for i in range(n):
x1, y1 = px[i], py[i]
x2, y2 = px[(i + 1) % n], py[(i + 1) % n]
# Bresenham line
dx = abs(x2 - x1); dy = -abs(y2 - y1)
sx = 1 if x1 < x2 else -1
sy = 1 if y1 < y2 else -1
err = dx + dy
cx, cy = x1, y1
while True:
if 0 <= cx < w and 0 <= cy < h:
pixels[cy][cx] = outline
if cx == x2 and cy == y2: break
e2 = 2 * err
if e2 >= dy:
if cx == x2: break
err += dy; cx += sx
if e2 <= dx:
if cy == y2: break
err += dx; cy += sy
def draw_grid(pixels, color):
h, w = len(pixels), len(pixels[0])
for lat in range(-60, 61, 30):
y = lat_to_y(lat)
if 0 <= y < h:
for x in range(0, w, 2):
pixels[y][x] = color
for lon in range(-150, 151, 30):
x = lon_to_x(lon)
if 0 <= x < w:
for y in range(0, h, 2):
pixels[y][x] = color
def png_encode(pixels):
"""Encode RGB pixels as PNG bytes."""
h, w = len(pixels), len(pixels[0])
raw = b''
for y in range(h):
raw += b'\x00' # filter none
for x in range(w):
r, g, b = pixels[y][x]
raw += bytes([r, g, b])
def chunk(ctype, data):
c = ctype + data
return struct.pack('>I', len(data)) + c + struct.pack('>I', zlib.crc32(c) & 0xFFFFFFFF)
ihdr = struct.pack('>IIBBBBB', w, h, 8, 2, 0, 0, 0) # 8-bit RGB
return (b'\x89PNG\r\n\x1a\n' +
chunk(b'IHDR', ihdr) +
chunk(b'IDAT', zlib.compress(raw)) +
chunk(b'IEND', b''))
# ── Build picture ──
OCEAN = (185, 200, 218)
GRID_COLOR = (170, 185, 205)
pixels = [[OCEAN for _ in range(W)] for _ in range(H)]
# Draw grid first (under continents)
draw_grid(pixels, GRID_COLOR)
# Draw continents
for fill, outline, pts in CONTINENTS:
draw_filled_polygon(pixels, fill, outline, pts)
# Save
png_data = png_encode(pixels)
out_path = Path(__file__).parent / 'static' / 'simple_analysis' / 'world_map.png'
out_path.parent.mkdir(parents=True, exist_ok=True)
out_path.write_bytes(png_data)
print(f'World map saved: {out_path} ({len(png_data):,} bytes, {W}x{H})')