66e3649152
- 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>
220 lines
8.8 KiB
Python
220 lines
8.8 KiB
Python
#!/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})')
|