mirror of
https://github.com/ajvpot/meshexplorer.git
synced 2026-03-28 17:42:58 +01:00
path magic
This commit is contained in:
@@ -10,12 +10,13 @@ export async function GET(req: Request) {
|
||||
const maxLng = searchParams.get("maxLng");
|
||||
const nodeTypes = searchParams.getAll("nodeTypes");
|
||||
const lastSeen = searchParams.get("lastSeen");
|
||||
const region = searchParams.get("region");
|
||||
const includeNeighbors = searchParams.get("includeNeighbors") === "true";
|
||||
|
||||
const positions = await getNodePositions({ minLat, maxLat, minLng, maxLng, nodeTypes, lastSeen });
|
||||
|
||||
if (includeNeighbors) {
|
||||
const neighbors = await getAllNodeNeighbors(lastSeen, minLat, maxLat, minLng, maxLng, nodeTypes);
|
||||
const neighbors = await getAllNodeNeighbors(lastSeen, minLat, maxLat, minLng, maxLng, nodeTypes, region || undefined);
|
||||
return NextResponse.json({
|
||||
nodes: positions,
|
||||
neighbors: neighbors
|
||||
|
||||
@@ -10,8 +10,9 @@ export async function GET(req: Request) {
|
||||
const maxLng = searchParams.get("maxLng");
|
||||
const nodeTypes = searchParams.getAll("nodeTypes");
|
||||
const lastSeen = searchParams.get("lastSeen");
|
||||
const region = searchParams.get("region");
|
||||
|
||||
const neighbors = await getAllNodeNeighbors(lastSeen, minLat, maxLat, minLng, maxLng, nodeTypes);
|
||||
const neighbors = await getAllNodeNeighbors(lastSeen, minLat, maxLat, minLng, maxLng, nodeTypes, region || undefined);
|
||||
|
||||
return NextResponse.json(neighbors);
|
||||
} catch (error) {
|
||||
|
||||
@@ -372,6 +372,42 @@ function AllNeighborLines({
|
||||
visibleNodeIds.has(connection.source_node) && visibleNodeIds.has(connection.target_node)
|
||||
);
|
||||
|
||||
// Calculate logarithmic thresholds based on packet counts for path connections
|
||||
const pathConnections = visibleConnections.filter(conn => conn.connection_type === 'path');
|
||||
const packetCounts = pathConnections.map(conn => conn.packet_count).sort((a, b) => a - b);
|
||||
|
||||
const getLogThresholds = (counts: number[]) => {
|
||||
if (counts.length === 0) return { min: 1, t1: 1, t2: 1, t3: 1, t4: 1, max: 1 };
|
||||
|
||||
const min = Math.max(1, counts[0]); // Ensure minimum is at least 1 for log calculation
|
||||
const max = counts[counts.length - 1];
|
||||
|
||||
if (min === max) {
|
||||
return { min, t1: min, t2: min, t3: min, t4: min, max };
|
||||
}
|
||||
|
||||
// Use logarithmic scale to create thresholds
|
||||
const logMin = Math.log10(min);
|
||||
const logMax = Math.log10(max);
|
||||
const logRange = logMax - logMin;
|
||||
|
||||
const t1 = Math.pow(10, logMin + logRange * 0.2);
|
||||
const t2 = Math.pow(10, logMin + logRange * 0.4);
|
||||
const t3 = Math.pow(10, logMin + logRange * 0.6);
|
||||
const t4 = Math.pow(10, logMin + logRange * 0.8);
|
||||
|
||||
return {
|
||||
min,
|
||||
t1: Math.round(t1),
|
||||
t2: Math.round(t2),
|
||||
t3: Math.round(t3),
|
||||
t4: Math.round(t4),
|
||||
max
|
||||
};
|
||||
};
|
||||
|
||||
const thresholds = getLogThresholds(packetCounts);
|
||||
|
||||
return (
|
||||
<>
|
||||
{visibleConnections.map((connection) => {
|
||||
@@ -380,14 +416,34 @@ function AllNeighborLines({
|
||||
[connection.target_latitude, connection.target_longitude]
|
||||
];
|
||||
|
||||
// Different colors based on connection type and logarithmic packet count
|
||||
const getConnectionColor = (connectionType: string, packetCount: number) => {
|
||||
if (connectionType === 'direct') {
|
||||
return '#8b5cf6'; // Purple for direct connections
|
||||
}
|
||||
|
||||
// For path connections, use logarithmic thresholds for color intensity
|
||||
if (packetCount >= thresholds.t4) return '#dc2626'; // Red for highest log range
|
||||
if (packetCount >= thresholds.t3) return '#ea580c'; // Dark orange
|
||||
if (packetCount >= thresholds.t2) return '#f59e0b'; // Orange
|
||||
if (packetCount >= thresholds.t1) return '#eab308'; // Yellow
|
||||
if (packetCount > thresholds.min) return '#84cc16'; // Light green for above minimum
|
||||
return '#6b7280'; // Gray for minimum traffic
|
||||
};
|
||||
|
||||
const lineColor = getConnectionColor(connection.connection_type, connection.packet_count);
|
||||
|
||||
// Consistent line weight for all connections
|
||||
const lineWeight = connection.connection_type === 'direct' ? 2 : 1;
|
||||
|
||||
return (
|
||||
<Polyline
|
||||
key={`${connection.source_node}-${connection.target_node}`}
|
||||
key={`${connection.source_node}-${connection.target_node}-${connection.connection_type}`}
|
||||
positions={positions}
|
||||
pathOptions={{
|
||||
color: '#8b5cf6', // Purple color for all-neighbors view
|
||||
weight: 1,
|
||||
opacity: 0.5,
|
||||
color: lineColor,
|
||||
weight: lineWeight,
|
||||
opacity: 0.7,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
@@ -490,6 +546,9 @@ export default function MapView({ target = '_self' }: MapViewProps = {}) {
|
||||
if (config?.lastSeen !== null && config?.lastSeen !== undefined) {
|
||||
params.push(`lastSeen=${config.lastSeen}`);
|
||||
}
|
||||
if (config?.selectedRegion) {
|
||||
params.push(`region=${encodeURIComponent(config.selectedRegion)}`);
|
||||
}
|
||||
if (includeNeighbors) {
|
||||
params.push('includeNeighbors=true');
|
||||
}
|
||||
@@ -656,7 +715,7 @@ export default function MapView({ target = '_self' }: MapViewProps = {}) {
|
||||
return () => {
|
||||
fetchController.current?.abort();
|
||||
};
|
||||
}, [bounds, config?.nodeTypes, config?.lastSeen, fetchNodes, showAllNeighbors]);
|
||||
}, [bounds, config?.nodeTypes, config?.lastSeen, config?.selectedRegion, fetchNodes, showAllNeighbors]);
|
||||
|
||||
return (
|
||||
<div style={{ width: "100%", height: "100%", position: "relative" }}>
|
||||
@@ -736,6 +795,83 @@ export default function MapView({ target = '_self' }: MapViewProps = {}) {
|
||||
/>
|
||||
)}
|
||||
</MapContainer>
|
||||
|
||||
{/* Traffic Legend */}
|
||||
{showAllNeighbors && allNeighborConnections.length > 0 && (() => {
|
||||
// Calculate logarithmic thresholds for legend display
|
||||
const pathConnections = allNeighborConnections.filter(conn => conn.connection_type === 'path');
|
||||
const packetCounts = pathConnections.map(conn => conn.packet_count).sort((a, b) => a - b);
|
||||
const legendThresholds = packetCounts.length > 0 ? (() => {
|
||||
const min = Math.max(1, packetCounts[0]);
|
||||
const max = packetCounts[packetCounts.length - 1];
|
||||
|
||||
if (min === max) {
|
||||
return { min, t1: min, t2: min, t3: min, t4: min, max };
|
||||
}
|
||||
|
||||
const logMin = Math.log10(min);
|
||||
const logMax = Math.log10(max);
|
||||
const logRange = logMax - logMin;
|
||||
|
||||
return {
|
||||
min,
|
||||
t1: Math.round(Math.pow(10, logMin + logRange * 0.2)),
|
||||
t2: Math.round(Math.pow(10, logMin + logRange * 0.4)),
|
||||
t3: Math.round(Math.pow(10, logMin + logRange * 0.6)),
|
||||
t4: Math.round(Math.pow(10, logMin + logRange * 0.8)),
|
||||
max
|
||||
};
|
||||
})() : null;
|
||||
|
||||
return legendThresholds && (
|
||||
<div style={{
|
||||
position: "absolute",
|
||||
bottom: 16,
|
||||
left: 16,
|
||||
zIndex: 1000,
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.9)',
|
||||
padding: '12px',
|
||||
borderRadius: '8px',
|
||||
boxShadow: '0 2px 10px rgba(0,0,0,0.1)',
|
||||
fontSize: '12px',
|
||||
fontFamily: 'monospace'
|
||||
}}>
|
||||
<div style={{ fontWeight: 'bold', marginBottom: '8px' }}>
|
||||
Path Traffic - Log Scale ({pathConnections.length} connections)
|
||||
</div>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '4px' }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
|
||||
<div style={{ width: '20px', height: '2px', backgroundColor: '#dc2626' }}></div>
|
||||
<span>High: {legendThresholds.t4}+ packets</span>
|
||||
</div>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
|
||||
<div style={{ width: '20px', height: '2px', backgroundColor: '#ea580c' }}></div>
|
||||
<span>Med-High: {legendThresholds.t3}-{legendThresholds.t4 - 1}</span>
|
||||
</div>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
|
||||
<div style={{ width: '20px', height: '2px', backgroundColor: '#f59e0b' }}></div>
|
||||
<span>Medium: {legendThresholds.t2}-{legendThresholds.t3 - 1}</span>
|
||||
</div>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
|
||||
<div style={{ width: '20px', height: '2px', backgroundColor: '#eab308' }}></div>
|
||||
<span>Low-Med: {legendThresholds.t1}-{legendThresholds.t2 - 1}</span>
|
||||
</div>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
|
||||
<div style={{ width: '20px', height: '2px', backgroundColor: '#84cc16' }}></div>
|
||||
<span>Low: {legendThresholds.min + 1}-{legendThresholds.t1 - 1}</span>
|
||||
</div>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
|
||||
<div style={{ width: '20px', height: '2px', backgroundColor: '#6b7280' }}></div>
|
||||
<span>Minimal: {legendThresholds.min}</span>
|
||||
</div>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '8px', marginTop: '4px', paddingTop: '4px', borderTop: '1px solid #e5e7eb' }}>
|
||||
<div style={{ width: '20px', height: '2px', backgroundColor: '#8b5cf6' }}></div>
|
||||
<span>Direct connections</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})()}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -4,6 +4,8 @@ import { buildApiUrl } from '@/lib/api';
|
||||
export interface AllNeighborsConnection {
|
||||
source_node: string;
|
||||
target_node: string;
|
||||
connection_type: string;
|
||||
packet_count: number;
|
||||
source_name: string;
|
||||
source_latitude: number;
|
||||
source_longitude: number;
|
||||
@@ -21,6 +23,7 @@ interface UseAllNeighborsParams {
|
||||
maxLng?: number | null;
|
||||
nodeTypes?: string[];
|
||||
lastSeen?: number | null;
|
||||
region?: string;
|
||||
enabled?: boolean;
|
||||
}
|
||||
|
||||
@@ -31,10 +34,11 @@ export function useAllNeighbors({
|
||||
maxLng,
|
||||
nodeTypes,
|
||||
lastSeen,
|
||||
region,
|
||||
enabled = true
|
||||
}: UseAllNeighborsParams) {
|
||||
return useQuery({
|
||||
queryKey: ['allNeighbors', minLat, maxLat, minLng, maxLng, nodeTypes, lastSeen],
|
||||
queryKey: ['allNeighbors', minLat, maxLat, minLng, maxLng, nodeTypes, lastSeen, region],
|
||||
queryFn: async (): Promise<AllNeighborsConnection[]> => {
|
||||
const params = new URLSearchParams();
|
||||
|
||||
@@ -56,6 +60,9 @@ export function useAllNeighbors({
|
||||
if (lastSeen !== null && lastSeen !== undefined) {
|
||||
params.append('lastSeen', lastSeen.toString());
|
||||
}
|
||||
if (region) {
|
||||
params.append('region', region);
|
||||
}
|
||||
|
||||
const url = `/api/neighbors/all${params.toString() ? `?${params.toString()}` : ''}`;
|
||||
|
||||
|
||||
@@ -278,7 +278,7 @@ export async function getMeshcoreNodeInfo(publicKey: string, limit: number = 50)
|
||||
}
|
||||
}
|
||||
|
||||
export async function getAllNodeNeighbors(lastSeen: string | null = null, minLat?: string | null, maxLat?: string | null, minLng?: string | null, maxLng?: string | null, nodeTypes?: string[]) {
|
||||
export async function getAllNodeNeighbors(lastSeen: string | null = null, minLat?: string | null, maxLat?: string | null, minLng?: string | null, maxLng?: string | null, nodeTypes?: string[], region?: string) {
|
||||
try {
|
||||
// Build where conditions for visible nodes
|
||||
let visibleNodeWhereConditions = [
|
||||
@@ -321,6 +321,10 @@ export async function getAllNodeNeighbors(lastSeen: string | null = null, minLat
|
||||
|
||||
const meshcoreWhere = meshcoreWhereConditions.length > 0 ? `AND ${meshcoreWhereConditions.join(" AND ")}` : '';
|
||||
|
||||
// Build region filtering for meshcore_packets
|
||||
const regionFilter = generateRegionWhereClause(region);
|
||||
const packetsRegionWhere = regionFilter.whereClause ? `AND ${regionFilter.whereClause}` : '';
|
||||
|
||||
const allNeighborsQuery = `
|
||||
WITH visible_nodes AS (
|
||||
-- Get only nodes visible on the current map view
|
||||
@@ -353,11 +357,28 @@ export async function getAllNodeNeighbors(lastSeen: string | null = null, minLat
|
||||
${meshcoreWhere}
|
||||
GROUP BY public_key
|
||||
),
|
||||
neighbor_connections AS (
|
||||
repeater_prefixes AS (
|
||||
-- Get repeater prefixes info, excluding collisions (multiple repeaters per prefix)
|
||||
-- Only include repeaters from the selected region
|
||||
SELECT
|
||||
substring(public_key, 1, 2) as prefix,
|
||||
count() as node_count,
|
||||
any(public_key) as representative_key,
|
||||
any(node_name) as representative_name
|
||||
FROM meshcore_adverts_latest
|
||||
WHERE is_repeater = 1
|
||||
AND last_seen >= now() - INTERVAL 2 DAY
|
||||
${regionFilter.whereClause ? `AND ${regionFilter.whereClause}` : ''}
|
||||
GROUP BY prefix
|
||||
HAVING node_count = 1 -- Only include prefixes with exactly one repeater
|
||||
),
|
||||
direct_connections AS (
|
||||
-- Get all direct connections (path_len = 0) but only between visible nodes
|
||||
SELECT DISTINCT
|
||||
hex(origin_pubkey) as source_node,
|
||||
public_key as target_node
|
||||
public_key as target_node,
|
||||
'direct' as connection_type,
|
||||
1 as packet_count -- Direct connections don't have packet counts, use 1 as default
|
||||
FROM meshcore_adverts
|
||||
WHERE path_len = 0
|
||||
AND hex(origin_pubkey) != public_key
|
||||
@@ -365,10 +386,82 @@ export async function getAllNodeNeighbors(lastSeen: string | null = null, minLat
|
||||
AND hex(origin_pubkey) IN (SELECT node_id FROM visible_nodes)
|
||||
AND public_key IN (SELECT node_id FROM visible_nodes)
|
||||
${meshcoreWhere}
|
||||
),
|
||||
path_neighbors AS (
|
||||
-- Extract neighbors from routing paths with packet counts
|
||||
SELECT
|
||||
source_prefix,
|
||||
target_prefix,
|
||||
'path' as connection_type,
|
||||
count() as packet_count
|
||||
FROM (
|
||||
SELECT
|
||||
upper(hex(substring(path, i, 1))) as source_prefix,
|
||||
upper(hex(substring(path, i + 1, 1))) as target_prefix
|
||||
FROM (
|
||||
SELECT
|
||||
path,
|
||||
path_len
|
||||
FROM meshcore_packets
|
||||
WHERE path_len >= 2
|
||||
AND ingest_timestamp >= now() - INTERVAL 1 DAY
|
||||
${packetsRegionWhere}
|
||||
) p
|
||||
ARRAY JOIN range(1, path_len) as i
|
||||
WHERE i < path_len
|
||||
) path_pairs
|
||||
WHERE source_prefix IN (SELECT prefix FROM repeater_prefixes)
|
||||
AND target_prefix IN (SELECT prefix FROM repeater_prefixes)
|
||||
AND source_prefix != target_prefix
|
||||
GROUP BY source_prefix, target_prefix
|
||||
),
|
||||
prefix_to_key_map AS (
|
||||
-- Map prefixes back to full public keys for visible nodes
|
||||
SELECT
|
||||
rp.prefix,
|
||||
rp.representative_key as public_key,
|
||||
rp.representative_name as node_name
|
||||
FROM repeater_prefixes rp
|
||||
WHERE rp.representative_key IN (SELECT node_id FROM visible_nodes)
|
||||
),
|
||||
path_connections AS (
|
||||
-- Convert prefix-based path neighbors to public key connections
|
||||
-- Include all path connections (no exclusion of direct connections)
|
||||
SELECT
|
||||
source_map.public_key as source_node,
|
||||
target_map.public_key as target_node,
|
||||
'path' as connection_type,
|
||||
pn.packet_count
|
||||
FROM path_neighbors pn
|
||||
JOIN prefix_to_key_map source_map ON pn.source_prefix = source_map.prefix
|
||||
JOIN prefix_to_key_map target_map ON pn.target_prefix = target_map.prefix
|
||||
),
|
||||
direct_connections_filtered AS (
|
||||
-- Get direct connections but exclude pairs that already have path connections
|
||||
SELECT
|
||||
source_node,
|
||||
target_node,
|
||||
connection_type,
|
||||
packet_count
|
||||
FROM direct_connections
|
||||
WHERE (source_node, target_node) NOT IN (
|
||||
SELECT source_node, target_node FROM path_connections
|
||||
)
|
||||
AND (target_node, source_node) NOT IN (
|
||||
SELECT source_node, target_node FROM path_connections
|
||||
)
|
||||
),
|
||||
neighbor_connections AS (
|
||||
-- Combine path connections and filtered direct connections (path connections take precedence)
|
||||
SELECT source_node, target_node, connection_type, packet_count FROM path_connections
|
||||
UNION ALL
|
||||
SELECT source_node, target_node, connection_type, packet_count FROM direct_connections_filtered
|
||||
)
|
||||
SELECT
|
||||
connections.source_node,
|
||||
connections.target_node,
|
||||
connections.connection_type,
|
||||
connections.packet_count,
|
||||
source_details.node_name as source_name,
|
||||
source_details.latitude as source_latitude,
|
||||
source_details.longitude as source_longitude,
|
||||
@@ -388,7 +481,7 @@ export async function getAllNodeNeighbors(lastSeen: string | null = null, minLat
|
||||
AND source_details.longitude IS NOT NULL
|
||||
AND target_details.latitude IS NOT NULL
|
||||
AND target_details.longitude IS NOT NULL
|
||||
ORDER BY connections.source_node, connections.target_node
|
||||
ORDER BY connections.connection_type, connections.source_node, connections.target_node
|
||||
`;
|
||||
|
||||
const neighborsResult = await clickhouse.query({
|
||||
@@ -401,6 +494,8 @@ export async function getAllNodeNeighbors(lastSeen: string | null = null, minLat
|
||||
return neighbors as Array<{
|
||||
source_node: string;
|
||||
target_node: string;
|
||||
connection_type: string;
|
||||
packet_count: number;
|
||||
source_name: string;
|
||||
source_latitude: number;
|
||||
source_longitude: number;
|
||||
|
||||
Reference in New Issue
Block a user