feat(NetworkMapUnifi): enhance label placement logic and update country flag rendering with CDN support
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 1m57s
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 1m57s
This commit is contained in:
@@ -1,6 +1,7 @@
|
|||||||
import { useMemo, useCallback, useState, useRef, useEffect } from 'react';
|
import { useMemo, useCallback, useState, useRef, useEffect } from 'react';
|
||||||
import { IconRefresh, IconMaximize, IconMinimize } from '@tabler/icons-react';
|
import { IconRefresh, IconMaximize, IconMinimize } from '@tabler/icons-react';
|
||||||
import { countryToFlag } from './utils/serverUtils.js';
|
|
||||||
|
const FLAG_CDN = 'https://flagcdn.com';
|
||||||
|
|
||||||
const UNIFI_BG = '#0f172a';
|
const UNIFI_BG = '#0f172a';
|
||||||
const UNIFI_CARD_BG = '#1e293b';
|
const UNIFI_CARD_BG = '#1e293b';
|
||||||
@@ -270,8 +271,11 @@ export default function NetworkMapUnifi({ servers = [], connections = [], pingMa
|
|||||||
const perpY = (ddx / len) * baseOffset;
|
const perpY = (ddx / len) * baseOffset;
|
||||||
const cpx = mx + perpX;
|
const cpx = mx + perpX;
|
||||||
const cpy = my + perpY;
|
const cpy = my + perpY;
|
||||||
|
const perpLen = Math.hypot(perpX, perpY) || 1;
|
||||||
|
const ux = perpX / perpLen;
|
||||||
|
const uy = perpY / perpLen;
|
||||||
|
|
||||||
return { sx, sy, ex, ey, cpx, cpy };
|
return { sx, sy, ex, ey, cpx, cpy, mx, my, perpX: ux, perpY: uy };
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const pathD = useCallback((e) => {
|
const pathD = useCallback((e) => {
|
||||||
@@ -279,12 +283,14 @@ export default function NetworkMapUnifi({ servers = [], connections = [], pingMa
|
|||||||
return `M ${sx} ${sy} Q ${cpx} ${cpy} ${ex} ${ey}`;
|
return `M ${sx} ${sy} Q ${cpx} ${cpy} ${ex} ${ey}`;
|
||||||
}, [computeCurvePoints]);
|
}, [computeCurvePoints]);
|
||||||
|
|
||||||
const labelPoint = useCallback((e) => {
|
/** Точка для подписи: середина ребра + смещение по перпендикуляру (чтобы подпись была у ребра, а не уезжала) */
|
||||||
const { sx, sy, ex, ey, cpx, cpy } = computeCurvePoints(e);
|
const labelPlacement = useCallback((e, level = 0) => {
|
||||||
const t = 0.5;
|
const { mx, my, perpX, perpY } = computeCurvePoints(e);
|
||||||
const lx = (1 - t) * (1 - t) * sx + 2 * (1 - t) * t * cpx + t * t * ex;
|
const baseOffset = 18;
|
||||||
const ly = (1 - t) * (1 - t) * sy + 2 * (1 - t) * t * cpy + t * t * ey;
|
const stepOffset = 26;
|
||||||
return { x: lx, y: ly };
|
const x = mx + perpX * (baseOffset + level * stepOffset);
|
||||||
|
const y = my + perpY * (baseOffset + level * stepOffset);
|
||||||
|
return { x, y };
|
||||||
}, [computeCurvePoints]);
|
}, [computeCurvePoints]);
|
||||||
|
|
||||||
if (servers.length === 0) return null;
|
if (servers.length === 0) return null;
|
||||||
@@ -342,24 +348,22 @@ export default function NetworkMapUnifi({ servers = [], connections = [], pingMa
|
|||||||
opacity={dimFactor}
|
opacity={dimFactor}
|
||||||
/>
|
/>
|
||||||
{(() => {
|
{(() => {
|
||||||
const base = labelPoint(e);
|
|
||||||
let x = base.x;
|
|
||||||
let y = base.y;
|
|
||||||
const labels = [e.pingLabel, e.speedLabel].filter(Boolean);
|
const labels = [e.pingLabel, e.speedLabel].filter(Boolean);
|
||||||
if (labels.length === 0) return null;
|
if (labels.length === 0) return null;
|
||||||
let level = 0;
|
|
||||||
const totalH = labels.length * LABEL_H + (labels.length - 1) * LABEL_GAP;
|
const totalH = labels.length * LABEL_H + (labels.length - 1) * LABEL_GAP;
|
||||||
while (
|
const minDist = 70;
|
||||||
level < 10 &&
|
let level = 0;
|
||||||
used.some(
|
let x; let y;
|
||||||
(p) =>
|
while (level < 10) {
|
||||||
Math.abs(p.x - x) < LABEL_W &&
|
const pos = labelPlacement(e, level);
|
||||||
Math.abs(p.y - (y + level * (totalH + 8))) < totalH + 16
|
x = pos.x;
|
||||||
)
|
y = pos.y;
|
||||||
) {
|
const overlaps = used.some(
|
||||||
|
(p) => Math.hypot(p.x - x, p.y - y) < minDist
|
||||||
|
);
|
||||||
|
if (!overlaps) break;
|
||||||
level += 1;
|
level += 1;
|
||||||
}
|
}
|
||||||
y = y + level * (totalH + 8);
|
|
||||||
used.push({ x, y });
|
used.push({ x, y });
|
||||||
|
|
||||||
const pad = 8;
|
const pad = 8;
|
||||||
@@ -418,20 +422,27 @@ export default function NetworkMapUnifi({ servers = [], connections = [], pingMa
|
|||||||
strokeWidth={1}
|
strokeWidth={1}
|
||||||
/>
|
/>
|
||||||
<g transform={`translate(${NODE_WIDTH / 2}, ${NODE_HEIGHT / 2})`}>
|
<g transform={`translate(${NODE_WIDTH / 2}, ${NODE_HEIGHT / 2})`}>
|
||||||
{/* Флаг страны в левом верхнем углу карточки */}
|
{/* Флаг страны в левом верхнем углу карточки (картинка, т.к. эмодзи в SVG часто отображаются как буквы) */}
|
||||||
{s.country && (
|
{s.country && (() => {
|
||||||
<text
|
const cc = String(s.country).trim().toLowerCase();
|
||||||
x={-NODE_WIDTH / 2 + 14}
|
if (cc.length !== 2) return null;
|
||||||
y={-NODE_HEIGHT / 2 + 14}
|
const flagW = 22;
|
||||||
textAnchor="middle"
|
const flagH = 16;
|
||||||
dominantBaseline="middle"
|
return (
|
||||||
fontSize={14}
|
<image
|
||||||
style={{ pointerEvents: 'none' }}
|
href={`${FLAG_CDN}/w40/${cc}.png`}
|
||||||
title={s.country}
|
x={-NODE_WIDTH / 2 + 6}
|
||||||
>
|
y={-NODE_HEIGHT / 2 + 6}
|
||||||
{countryToFlag(s.country)}
|
width={flagW}
|
||||||
</text>
|
height={flagH}
|
||||||
)}
|
preserveAspectRatio="xMidYMid meet"
|
||||||
|
style={{ pointerEvents: 'none' }}
|
||||||
|
className="network-map-node-flag"
|
||||||
|
>
|
||||||
|
<title>{s.country.toUpperCase()}</title>
|
||||||
|
</image>
|
||||||
|
);
|
||||||
|
})()}
|
||||||
{/* Иконка сервера в стиле UNIFI: прямоугольник с «портами» */}
|
{/* Иконка сервера в стиле UNIFI: прямоугольник с «портами» */}
|
||||||
<g transform="translate(-10, -16)">
|
<g transform="translate(-10, -16)">
|
||||||
<rect x={2} y={0} width={16} height={12} rx={2} fill="none" stroke={UNIFI_GREEN} strokeWidth={1.5} />
|
<rect x={2} y={0} width={16} height={12} rx={2} fill="none" stroke={UNIFI_GREEN} strokeWidth={1.5} />
|
||||||
|
|||||||
Reference in New Issue
Block a user