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>
This commit is contained in:
@@ -0,0 +1,219 @@
|
|||||||
|
#!/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})')
|
||||||
@@ -1166,52 +1166,24 @@ function renderClusterCards(clusters) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ── Simplified continent outlines as Leaflet polygons ──── */
|
/* ── Step 4: Map (static SVG world map + large clickable points) ── */
|
||||||
const CONTINENTS = [
|
|
||||||
// North America
|
|
||||||
[[75,-168],[72,-140],[70,-130],[65,-125],[62,-115],[58,-108],[55,-100],[50,-95],[45,-90],[40,-85],[35,-82],[30,-80],[25,-78],[20,-80],[15,-83],[10,-84],[8,-80],[9,-77],[12,-72],[15,-66],[18,-60],[22,-60],[28,-58],[32,-60],[35,-55],[34,-45],[35,-40],[42,-35],[48,-30],[52,-25],[55,-20],[58,-15],[62,-12],[68,-10],[72,-15],[75,-25],[78,-35],[80,-50],[82,-60],[83,-70],[84,-80],[82,-90],[80,-100],[78,-120],[76,-140],[75,-168]],
|
|
||||||
// Greenland
|
|
||||||
[[83,-50],[82,-25],[80,-18],[78,-15],[76,-18],[75,-25],[76,-35],[78,-45],[80,-55],[83,-60],[84,-55],[83,-50]],
|
|
||||||
// South America
|
|
||||||
[[12,-72],[10,-65],[8,-55],[5,-48],[0,-45],[-5,-40],[-8,-35],[-10,-30],[-12,-25],[-15,-20],[-18,-18],[-22,-20],[-25,-25],[-28,-30],[-32,-35],[-35,-38],[-38,-40],[-40,-45],[-45,-48],[-50,-50],[-54,-52],[-55,-48],[-54,-42],[-52,-38],[-48,-35],[-42,-32],[-38,-30],[-35,-28],[-30,-25],[-25,-22],[-20,-20],[-15,-23],[-10,-30],[-5,-38],[0,-44],[5,-50],[8,-58],[10,-65],[12,-72]],
|
|
||||||
// Europe
|
|
||||||
[[71,-25],[70,-10],[68,5],[65,12],[62,15],[58,15],[55,12],[52,8],[48,5],[45,0],[43,-5],[44,-8],[48,-10],[52,-12],[55,-15],[57,-20],[58,-25],[60,-30],[62,-35],[65,-30],[68,-25],[70,-20],[71,-25]],
|
|
||||||
// UK/Ireland
|
|
||||||
[[59,-8],[58,-2],[56,0],[54,-3],[52,-6],[51,-10],[53,-12],[55,-10],[57,-8],[59,-8]],
|
|
||||||
// Scandinavia
|
|
||||||
[[71,25],[70,18],[68,15],[65,14],[62,12],[60,8],[58,5],[56,8],[55,12],[56,18],[58,25],[60,30],[62,32],[65,30],[68,28],[70,25],[71,25]],
|
|
||||||
// Africa
|
|
||||||
[[37,-8],[35,-2],[33,5],[30,10],[28,15],[25,20],[22,28],[18,35],[15,38],[12,40],[8,42],[3,43],[-2,42],[-5,38],[-8,35],[-12,32],[-15,28],[-18,25],[-20,20],[-22,18],[-24,15],[-27,12],[-30,8],[-32,5],[-35,0],[-33,-5],[-28,-8],[-22,-10],[-18,-8],[-14,-5],[-10,-3],[-5,0],[0,-2],[5,-4],[10,-5],[15,-7],[20,-10],[25,-12],[30,-10],[35,-8],[37,-8]],
|
|
||||||
// Asia
|
|
||||||
[[72,55],[70,80],[68,110],[65,130],[62,145],[58,155],[55,160],[50,165],[45,168],[40,170],[35,172],[30,170],[28,165],[25,160],[22,155],[18,150],[15,145],[12,140],[10,135],[8,130],[5,125],[2,120],[0,115],[-2,110],[-5,105],[-8,100],[-10,105],[-12,110],[-15,115],[-18,120],[-20,118],[-25,112],[-28,108],[-32,105],[-35,100],[-38,95],[-40,90],[-42,85],[-45,80],[-48,75],[-50,70],[-55,65],[-58,60],[-60,55],[-58,50],[-55,45],[-52,42],[-50,38],[-48,35],[-45,38],[-42,42],[-40,48],[-35,52],[-30,55],[-25,52],[-22,48],[-18,45],[-15,48],[-12,52],[-8,55],[-5,52],[-2,48],[0,45],[3,42],[5,38],[8,35],[10,38],[12,42],[15,48],[20,55],[25,62],[30,68],[35,72],[40,75],[45,78],[50,80],[55,78],[60,75],[65,70],[68,65],[70,60],[72,55]],
|
|
||||||
// Japan
|
|
||||||
[[45,142],[42,140],[38,138],[35,136],[32,135],[30,137],[28,140],[30,143],[33,145],[35,143],[38,142],[42,144],[45,143],[45,142]],
|
|
||||||
// SE Asia islands
|
|
||||||
[[8,115],[-2,110],[-6,105],[-8,108],[-5,112],[-2,115],[0,118],[2,120],[5,118],[8,115]],
|
|
||||||
[[0,125],[-5,128],[-8,130],[-6,135],[-3,138],[0,140],[3,138],[5,135],[3,130],[0,125]],
|
|
||||||
// Australia
|
|
||||||
[[-12,130],[-15,128],[-20,126],[-25,128],[-30,132],[-35,138],[-38,142],[-40,148],[-38,152],[-35,155],[-30,153],[-25,148],[-20,145],[-18,140],[-15,138],[-12,135],[-10,132],[-12,130]],
|
|
||||||
// New Zealand
|
|
||||||
[[-35,172],[-38,170],[-42,172],[-45,174],[-47,172],[-45,168],[-42,170],[-38,172],[-35,172]],
|
|
||||||
// Antarctica (hint)
|
|
||||||
[[-65,-170],[-65,-120],[-68,-60],[-70,0],[-72,60],[-70,120],[-65,170],[-75,170],[-78,120],[-80,60],[-80,0],[-78,-60],[-75,-120],[-72,-170],[-65,-170]],
|
|
||||||
];
|
|
||||||
|
|
||||||
// Subnet color palette — one color per /24 prefix
|
// World map image path
|
||||||
|
const WORLD_MAP_URL = '/static/simple_analysis/world_map.svg';
|
||||||
|
|
||||||
|
// Subnet color palette (high-contrast against map background)
|
||||||
const SUBNET_COLORS = [
|
const SUBNET_COLORS = [
|
||||||
'#4361ee','#43aa8b','#f72585','#f4a261','#e76f51','#264653','#2a9d8f',
|
'#e63946','#2a9d8f','#f77f00','#7209b7','#4cc9f0','#f72585',
|
||||||
'#e9c46a','#8ecae6','#219ebc','#023047','#ffb703','#fb8500','#6a4c93',
|
'#e76f51','#264653','#2ec4b6','#ff9f1c','#8338ec','#00b4d8',
|
||||||
'#ff6b6b','#4ecdc4','#45b7d1','#96ceb4','#d4a5a5','#a445b2',
|
'#d00000','#1b4332','#e36414','#3c096c','#0077b6','#bc4742',
|
||||||
'#e17055','#00b894','#0984e3','#6c5ce7','#fd79a8','#00cec9','#e66767',
|
'#606c38','#bc6c25','#b5838d','#6d6875','#457b9d','#e07a5f',
|
||||||
'#3dc1d3','#e77f67','#778beb','#cf6a87','#63cdda','#ea8685','#596275',
|
|
||||||
];
|
];
|
||||||
|
|
||||||
function getSubnetColor(ip) {
|
function getSubnetColor(ip) {
|
||||||
// Hash the /24 subnet to a color
|
|
||||||
const parts = ip.split('.');
|
const parts = ip.split('.');
|
||||||
if (parts.length >= 3) {
|
if (parts.length >= 3) {
|
||||||
const hash = (parseInt(parts[0]) * 31 + parseInt(parts[1]) * 17 + parseInt(parts[2]) * 7) % SUBNET_COLORS.length;
|
const h = (parseInt(parts[0]) * 31 + parseInt(parts[1]) * 17 + parseInt(parts[2]) * 7);
|
||||||
return SUBNET_COLORS[Math.abs(hash)];
|
return SUBNET_COLORS[Math.abs(h) % SUBNET_COLORS.length];
|
||||||
}
|
}
|
||||||
return SUBNET_COLORS[0];
|
return SUBNET_COLORS[0];
|
||||||
}
|
}
|
||||||
@@ -1221,20 +1193,12 @@ function initMap() {
|
|||||||
if (!container) return;
|
if (!container) return;
|
||||||
container.style.height = '550px';
|
container.style.height = '550px';
|
||||||
container.style.width = '100%';
|
container.style.width = '100%';
|
||||||
container.style.background = '#e8ecf2';
|
container.style.background = '#b9c8da';
|
||||||
|
|
||||||
if (SA.map) {
|
if (SA.map) { SA.map.invalidateSize(); renderMapData(); return; }
|
||||||
SA.map.invalidateSize();
|
|
||||||
renderMapData();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Use requestAnimationFrame to ensure container is visible and has dimensions
|
|
||||||
function createMap() {
|
function createMap() {
|
||||||
if (container.offsetHeight === 0) {
|
if (container.offsetHeight === 0) { requestAnimationFrame(createMap); return; }
|
||||||
requestAnimationFrame(createMap);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
SA.map = L.map('saMap', {
|
SA.map = L.map('saMap', {
|
||||||
center: [20, 0], zoom: 2, minZoom: 2, maxZoom: 14,
|
center: [20, 0], zoom: 2, minZoom: 2, maxZoom: 14,
|
||||||
@@ -1242,26 +1206,8 @@ function initMap() {
|
|||||||
attributionControl: false, zoomControl: true,
|
attributionControl: false, zoomControl: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Draw continent outlines with visible colors
|
// High-quality static world map as image overlay
|
||||||
CONTINENTS.forEach(ring => {
|
L.imageOverlay(WORLD_MAP_URL, [[-90, -180], [90, 180]], {opacity: 1}).addTo(SA.map);
|
||||||
L.polygon(ring, {
|
|
||||||
color: '#9aa8b8', weight: 1, opacity: 0.8,
|
|
||||||
fillColor: '#d5dce3', fillOpacity: 1,
|
|
||||||
interactive: false,
|
|
||||||
}).addTo(SA.map);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Grid lines
|
|
||||||
for (let lat = -60; lat <= 60; lat += 30) {
|
|
||||||
L.polyline([[lat, -180], [lat, 180]], {
|
|
||||||
color: '#c5cad4', weight: 0.5, interactive: false,
|
|
||||||
}).addTo(SA.map);
|
|
||||||
}
|
|
||||||
for (let lon = -150; lon <= 150; lon += 30) {
|
|
||||||
L.polyline([[-90, lon], [90, lon]], {
|
|
||||||
color: '#c5cad4', weight: 0.5, interactive: false,
|
|
||||||
}).addTo(SA.map);
|
|
||||||
}
|
|
||||||
|
|
||||||
SA.map.setMaxBounds([[-85, -180], [85, 180]]);
|
SA.map.setMaxBounds([[-85, -180], [85, 180]]);
|
||||||
SA.map.on('zoomend moveend', () => renderMapData());
|
SA.map.on('zoomend moveend', () => renderMapData());
|
||||||
@@ -1271,7 +1217,7 @@ function initMap() {
|
|||||||
requestAnimationFrame(createMap);
|
requestAnimationFrame(createMap);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Render all IP nodes (colored by subnet) ───────────────
|
// ── Render IP nodes (large, clickable, high-contrast colors) ──
|
||||||
function renderMapData() {
|
function renderMapData() {
|
||||||
if (!SA.map) return;
|
if (!SA.map) return;
|
||||||
if (SA.pointLayer) SA.map.removeLayer(SA.pointLayer);
|
if (SA.pointLayer) SA.map.removeLayer(SA.pointLayer);
|
||||||
@@ -1282,28 +1228,22 @@ function renderMapData() {
|
|||||||
if (!nodes.length) return;
|
if (!nodes.length) return;
|
||||||
|
|
||||||
const bounds = SA.map.getBounds();
|
const bounds = SA.map.getBounds();
|
||||||
const zoom = SA.map.getZoom();
|
|
||||||
const markers = [];
|
const markers = [];
|
||||||
const centerMarkers = [];
|
const centerMarkers = [];
|
||||||
|
|
||||||
// Per-subnet color cache
|
|
||||||
const subnetColorCache = {};
|
|
||||||
|
|
||||||
nodes.forEach(n => {
|
nodes.forEach(n => {
|
||||||
if (n.lat == null || n.lon == null) return;
|
if (n.lat == null || n.lon == null) return;
|
||||||
if (!bounds.contains([n.lat, n.lon])) return;
|
if (!bounds.contains([n.lat, n.lon])) return;
|
||||||
if (SA.hiddenClusters.has(n.cluster_id)) return;
|
if (SA.hiddenClusters.has(n.cluster_id)) return;
|
||||||
|
|
||||||
// Color by /24 subnet
|
const color = getSubnetColor(n.ip);
|
||||||
if (!subnetColorCache[n.ip]) subnetColorCache[n.ip] = getSubnetColor(n.ip);
|
// Larger circles, easy to click
|
||||||
const color = subnetColorCache[n.ip];
|
|
||||||
|
|
||||||
const circle = L.circleMarker([n.lat, n.lon], {
|
const circle = L.circleMarker([n.lat, n.lon], {
|
||||||
radius: 4, fillColor: color, color: '#fff', weight: 0.5,
|
radius: 7, fillColor: color, color: '#fff', weight: 2,
|
||||||
opacity: 0.85, fillOpacity: 0.75,
|
opacity: 0.95, fillOpacity: 0.85,
|
||||||
interactive: true, bubblingMouseEvents: false,
|
interactive: true, bubblingMouseEvents: false,
|
||||||
});
|
});
|
||||||
circle.bindTooltip(buildNodeTooltip(n), {sticky: true, direction: 'top'});
|
circle.bindTooltip(buildNodeTooltip(n), {sticky: true, direction: 'top', offset: [0, -8]});
|
||||||
circle.on('click', function(e) {
|
circle.on('click', function(e) {
|
||||||
L.DomEvent.stopPropagation(e);
|
L.DomEvent.stopPropagation(e);
|
||||||
loadNodeDetail(n.ip);
|
loadNodeDetail(n.ip);
|
||||||
@@ -1311,7 +1251,7 @@ function renderMapData() {
|
|||||||
markers.push(circle);
|
markers.push(circle);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Community center markers (larger dots for multi-node communities)
|
// Community center markers
|
||||||
const colors = generateColors(20);
|
const colors = generateColors(20);
|
||||||
const clusterColorMap = {};
|
const clusterColorMap = {};
|
||||||
Object.values(clusters).sort((a, b) => b.size - a.size).forEach((c, i) => {
|
Object.values(clusters).sort((a, b) => b.size - a.size).forEach((c, i) => {
|
||||||
@@ -1326,18 +1266,15 @@ function renderMapData() {
|
|||||||
|
|
||||||
const color = clusterColorMap[cidInt] || '#4361ee';
|
const color = clusterColorMap[cidInt] || '#4361ee';
|
||||||
const circle = L.circleMarker([cinfo.center_lat, cinfo.center_lon], {
|
const circle = L.circleMarker([cinfo.center_lat, cinfo.center_lon], {
|
||||||
radius: 8, fillColor: color, color: '#fff', weight: 2,
|
radius: 10, fillColor: color, color: '#fff', weight: 3,
|
||||||
opacity: 0.95, fillOpacity: 0.8,
|
opacity: 0.95, fillOpacity: 0.7,
|
||||||
interactive: true, bubblingMouseEvents: false,
|
interactive: true, bubblingMouseEvents: false,
|
||||||
});
|
});
|
||||||
circle.bindTooltip(
|
circle.bindTooltip(
|
||||||
`<div class="sa-point-tooltip"><b>社区 ${cidInt}</b><br>${cinfo.size} 个IP · ${cinfo.subnet || ''}<br>点击查看详情</div>`,
|
`<div class="sa-point-tooltip"><b>社区 ${cidInt}</b><br>${cinfo.size} IP · ${cinfo.subnet||''}<br>点击查看详情</div>`,
|
||||||
{direction: 'top'}
|
{direction: 'top'}
|
||||||
);
|
);
|
||||||
circle.on('click', function(e) {
|
circle.on('click', function(e) { L.DomEvent.stopPropagation(e); loadClusterDetail(cidInt); });
|
||||||
L.DomEvent.stopPropagation(e);
|
|
||||||
loadClusterDetail(cidInt);
|
|
||||||
});
|
|
||||||
centerMarkers.push(circle);
|
centerMarkers.push(circle);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1347,15 +1284,14 @@ function renderMapData() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function buildNodeTooltip(n) {
|
function buildNodeTooltip(n) {
|
||||||
const serverLabel = n.is_server ? ' 🖥' : '';
|
const p = n.ip ? n.ip.split('.') : [];
|
||||||
const parts = n.ip ? n.ip.split('.') : [];
|
const sn = p.length >= 3 ? p[0]+'.'+p[1]+'.'+p[2]+'.0/24' : '';
|
||||||
const subnet = parts.length >= 3 ? parts[0] + '.' + parts[1] + '.' + parts[2] + '.0/24' : '';
|
const srv = n.is_server ? ' 🖥' : '';
|
||||||
return `<div class="sa-point-tooltip">
|
return `<div class="sa-point-tooltip"><b>${escapeHtml(n.ip)}</b>${srv}<br>` +
|
||||||
<b>${escapeHtml(n.ip)}</b>${serverLabel}<br>
|
(sn ? `<span style="color:#888;">子网: ${sn}</span><br>` : '') +
|
||||||
${subnet ? '<span style="color:#888;">子网: ' + subnet + '</span><br>' : ''}
|
`${escapeHtml(n.city||'')}, ${escapeHtml(n.country||'')}<br>` +
|
||||||
${n.city ? '<span>' + escapeHtml(n.city) + '</span>, ' : ''}${escapeHtml(n.country || '')}<br>
|
(n.isp ? `<span style="color:#666;">${escapeHtml(n.isp)}</span><br>` : '') +
|
||||||
${n.isp ? '<span style="color:#666;">' + escapeHtml(n.isp) + '</span><br>' : ''}
|
`<span style="color:#4361ee;">🖱 点击查看详情</span></div>`;
|
||||||
<span style="color:#4361ee;">🖱 点击查看详情</span></div>`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderLegend() {
|
function renderLegend() {
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 2048 1024" width="2048" height="1024">
|
||||||
|
<rect width="2048" height="1024" fill="#b9c8da"/>
|
||||||
|
<g stroke="#a0b2c8" stroke-width="1.5" stroke-dasharray="6,6" opacity="0.6"><line x1="0.0" y1="853.3" x2="2048.0" y2="853.3"/><line x1="0.0" y1="682.7" x2="2048.0" y2="682.7"/><line x1="0.0" y1="512.0" x2="2048.0" y2="512.0"/><line x1="0.0" y1="341.3" x2="2048.0" y2="341.3"/><line x1="0.0" y1="170.7" x2="2048.0" y2="170.7"/><line x1="170.7" y1="1024.0" x2="170.7" y2="0.0"/><line x1="341.3" y1="1024.0" x2="341.3" y2="0.0"/><line x1="512.0" y1="1024.0" x2="512.0" y2="0.0"/><line x1="682.7" y1="1024.0" x2="682.7" y2="0.0"/><line x1="853.3" y1="1024.0" x2="853.3" y2="0.0"/><line x1="1024.0" y1="1024.0" x2="1024.0" y2="0.0"/><line x1="1194.7" y1="1024.0" x2="1194.7" y2="0.0"/><line x1="1365.3" y1="1024.0" x2="1365.3" y2="0.0"/><line x1="1536.0" y1="1024.0" x2="1536.0" y2="0.0"/><line x1="1706.7" y1="1024.0" x2="1706.7" y2="0.0"/><line x1="1877.3" y1="1024.0" x2="1877.3" y2="0.0"/></g>
|
||||||
|
<g fill="#e8ecf2" stroke="#9aa8ba" stroke-width="2" stroke-linejoin="round"><path d="M68.3,102.4L136.5,108.1L204.8,113.8L250.3,125.2L284.4,136.5L318.6,147.9L352.7,159.3L375.5,170.7L398.2,182.0L421.0,193.4L455.1,204.8L477.9,216.2L500.6,227.6L523.4,238.9L546.1,250.3L557.5,261.7L568.9,267.4L574.6,278.8L586.0,295.8L580.3,312.9L568.9,330.0L557.5,341.3L546.1,352.7L546.1,364.1L557.5,375.5L580.3,386.8L591.6,398.2L586.0,409.6L568.9,421.0L546.1,432.4L523.4,443.7L551.8,460.8L568.9,466.5L580.3,466.5L591.6,460.8L608.7,443.7L625.8,438.0L642.8,426.7L665.6,415.3L688.4,398.2L694.0,375.5L694.0,352.7L699.7,330.0L716.8,312.9L750.9,307.2L785.1,312.9L813.5,312.9L836.3,295.8L853.3,267.4L864.7,244.6L876.1,221.9L898.8,199.1L927.3,182.0L944.4,153.6L938.7,125.2L910.2,102.4L859.0,79.6L785.1,62.6L705.4,51.2L637.2,39.8L557.5,34.1L489.2,39.8L398.2,51.2L284.4,62.6L182.0,74.0L91.0,91.0Z"/>
|
||||||
|
<path d="M711.1,39.8L824.9,45.5L898.8,51.2L927.3,62.6L933.0,74.0L910.2,85.3L870.4,91.0L807.8,85.3L739.6,74.0L682.7,56.9L654.2,45.5Z"/>
|
||||||
|
<path d="M614.4,443.7L637.2,455.1L671.3,472.2L699.7,483.6L739.6,500.6L762.3,512.0L785.1,523.4L813.5,540.4L830.6,557.5L842.0,580.3L853.3,597.3L870.4,614.4L887.5,631.5L876.1,648.5L853.3,665.6L836.3,682.7L813.5,699.7L796.4,716.8L779.4,739.6L756.6,762.3L739.6,790.8L728.2,819.2L750.9,824.9L773.7,819.2L796.4,807.8L819.2,785.1L836.3,762.3L847.6,739.6L859.0,716.8L870.4,694.0L881.8,671.3L893.2,648.5L904.5,625.8L893.2,603.0L876.1,586.0L853.3,563.2L819.2,540.4L785.1,523.4L750.9,500.6L711.1,477.9L659.9,460.8Z"/>
|
||||||
|
<path d="M995.6,108.1L1046.8,113.8L1080.9,125.2L1098.0,136.5L1109.3,159.3L1109.3,182.0L1103.6,204.8L1092.3,227.6L1075.2,244.6L1046.8,261.7L1012.6,267.4L989.9,261.7L972.8,244.6L984.2,227.6L995.6,210.5L1006.9,199.1L995.6,187.7L978.5,176.4L961.4,153.6L972.8,136.5L989.9,119.5Z"/>
|
||||||
|
<path d="M984.2,176.4L1001.2,182.0L1012.6,193.4L1012.6,204.8L995.6,216.2L978.5,221.9L967.1,216.2L967.1,204.8L978.5,193.4L984.2,187.7Z"/>
|
||||||
|
<path d="M1063.8,108.1L1103.6,113.8L1120.7,125.2L1109.3,142.2L1098.0,159.3L1075.2,176.4L1052.4,187.7L1069.5,199.1L1098.0,199.1L1132.1,187.7L1171.9,176.4L1200.4,159.3L1194.7,142.2L1171.9,125.2L1137.8,113.8L1103.6,108.1Z"/>
|
||||||
|
<path d="M972.8,301.5L1012.6,312.9L1052.4,324.3L1086.6,341.3L1120.7,352.7L1154.8,369.8L1189.0,386.8L1223.1,409.6L1245.9,426.7L1262.9,455.1L1274.3,483.6L1274.3,506.3L1268.6,529.1L1240.2,557.5L1217.4,580.3L1194.7,597.3L1177.6,614.4L1154.8,631.5L1137.8,648.5L1115.0,671.3L1080.9,688.4L1052.4,705.4L1035.4,711.1L1012.6,699.7L995.6,671.3L984.2,648.5L984.2,620.1L1001.2,591.6L1024.0,557.5L1012.6,523.4L1001.2,489.2L989.9,455.1L978.5,421.0L967.1,386.8L967.1,352.7L967.1,324.3Z"/>
|
||||||
|
<path d="M1262.9,102.4L1336.9,113.8L1416.5,125.2L1518.9,136.5L1592.9,147.9L1695.3,159.3L1792.0,170.7L1877.3,187.7L1922.8,210.5L1951.3,233.2L1974.0,261.7L1991.1,284.4L2002.5,301.5L1991.1,318.6L1974.0,341.3L1951.3,352.7L1922.8,375.5L1888.7,392.5L1860.3,409.6L1820.4,432.4L1792.0,449.4L1752.2,466.5L1706.7,483.6L1678.2,500.6L1649.8,517.7L1621.3,534.8L1592.9,551.8L1615.6,568.9L1644.1,586.0L1678.2,608.7L1695.3,631.5L1661.2,648.5L1638.4,671.3L1615.6,694.0L1592.9,711.1L1564.4,728.2L1536.0,739.6L1507.6,750.9L1467.7,773.7L1445.0,785.1L1410.8,807.8L1388.1,830.6L1365.3,853.3L1342.6,864.7L1319.8,853.3L1297.1,836.3L1274.3,813.5L1251.6,790.8L1228.8,768.0L1240.2,756.6L1268.6,745.2L1302.8,722.5L1325.5,699.7L1336.9,677.0L1319.8,654.2L1297.1,631.5L1285.7,614.4L1297.1,597.3L1319.8,574.6L1331.2,551.8L1314.1,529.1L1291.4,506.3L1262.9,489.2L1240.2,472.2L1234.5,455.1L1251.6,438.0L1285.7,421.0L1319.8,398.2L1354.0,369.8L1399.5,341.3L1433.6,312.9L1456.4,284.4L1479.1,250.3L1473.4,221.9L1462.0,199.1L1433.6,170.7L1405.2,136.5L1354.0,113.8L1308.4,102.4Z"/>
|
||||||
|
<path d="M1831.8,250.3L1826.1,267.4L1820.4,278.8L1814.8,295.8L1803.4,312.9L1797.7,324.3L1803.4,335.6L1820.4,330.0L1837.5,318.6L1843.2,307.2L1843.2,290.1L1837.5,273.1L1837.5,256.0Z"/>
|
||||||
|
<path d="M1683.9,472.2L1661.2,523.4L1627.0,546.1L1638.4,557.5L1666.8,546.1L1689.6,523.4L1701.0,494.9L1695.3,483.6Z"/>
|
||||||
|
<path d="M1740.8,506.3L1763.6,534.8L1774.9,551.8L1797.7,557.5L1814.8,540.4L1826.1,512.0L1814.8,494.9L1792.0,489.2Z"/>
|
||||||
|
<path d="M1763.6,580.3L1752.2,603.0L1740.8,631.5L1746.5,665.6L1769.2,694.0L1803.4,716.8L1831.8,733.9L1866.0,739.6L1888.7,728.2L1900.1,705.4L1883.0,677.0L1860.3,654.2L1831.8,631.5L1809.1,614.4L1786.3,591.6Z"/>
|
||||||
|
<path d="M2008.2,711.1L1991.1,728.2L2002.5,745.2L2013.9,768.0L2002.5,773.7L1991.1,762.3L1991.1,750.9L2002.5,728.2Z"/>
|
||||||
|
<path d="M28.4,887.5L284.4,887.5L568.9,898.8L796.4,910.2L1024.0,921.6L1251.6,921.6L1479.1,910.2L1763.6,898.8L2019.6,887.5L2019.6,938.7L1763.6,955.7L1479.1,967.1L1251.6,978.5L1024.0,978.5L796.4,967.1L568.9,955.7L284.4,938.7L28.4,921.6Z"/></g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 5.6 KiB |
Reference in New Issue
Block a user