Network map enhancements and fixes
This commit is contained in:
@@ -0,0 +1,248 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../../../models/network/network_device.dart';
|
||||
import '../../../models/network/topology_graph.dart';
|
||||
|
||||
/// Scaled-down overview of the topology canvas with a viewport indicator.
|
||||
///
|
||||
/// Shows all device nodes as small colour-coded rectangles and all edges as
|
||||
/// thin lines. A semi-transparent white rectangle indicates the current
|
||||
/// viewport within the full canvas. Tapping on the minimap pans the main
|
||||
/// canvas to centre on that point.
|
||||
class TopologyMinimap extends StatefulWidget {
|
||||
const TopologyMinimap({
|
||||
super.key,
|
||||
required this.graph,
|
||||
required this.nodePositions,
|
||||
required this.canvasSize,
|
||||
required this.controller,
|
||||
required this.viewportSize,
|
||||
});
|
||||
|
||||
final TopologyGraph graph;
|
||||
|
||||
/// Current interpolated node positions (top-left corners) from the canvas.
|
||||
final Map<String, Offset> nodePositions;
|
||||
|
||||
final Size canvasSize;
|
||||
final TransformationController controller;
|
||||
final Size viewportSize;
|
||||
|
||||
static const Size _minimapSize = Size(160, 100);
|
||||
|
||||
@override
|
||||
State<TopologyMinimap> createState() => _TopologyMinimapState();
|
||||
}
|
||||
|
||||
class _TopologyMinimapState extends State<TopologyMinimap> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
widget.controller.addListener(_onTransformChanged);
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(covariant TopologyMinimap oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (oldWidget.controller != widget.controller) {
|
||||
oldWidget.controller.removeListener(_onTransformChanged);
|
||||
widget.controller.addListener(_onTransformChanged);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
widget.controller.removeListener(_onTransformChanged);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _onTransformChanged() => setState(() {});
|
||||
|
||||
double get _scaleX {
|
||||
if (widget.canvasSize.width == 0) return 1;
|
||||
return TopologyMinimap._minimapSize.width / widget.canvasSize.width;
|
||||
}
|
||||
|
||||
double get _scaleY {
|
||||
if (widget.canvasSize.height == 0) return 1;
|
||||
return TopologyMinimap._minimapSize.height / widget.canvasSize.height;
|
||||
}
|
||||
|
||||
double get _scale => _scaleX < _scaleY ? _scaleX : _scaleY;
|
||||
|
||||
Offset _toMinimap(Offset canvasPoint) {
|
||||
final s = _scale;
|
||||
final offsetX = (TopologyMinimap._minimapSize.width - widget.canvasSize.width * s) / 2;
|
||||
final offsetY = (TopologyMinimap._minimapSize.height - widget.canvasSize.height * s) / 2;
|
||||
return Offset(canvasPoint.dx * s + offsetX, canvasPoint.dy * s + offsetY);
|
||||
}
|
||||
|
||||
/// Convert a tap on the minimap to a canvas point, then pan the main canvas
|
||||
/// so that point appears at the viewport centre.
|
||||
void _onTap(Offset localPos) {
|
||||
final s = _scale;
|
||||
if (s == 0) return;
|
||||
final offsetX = (TopologyMinimap._minimapSize.width - widget.canvasSize.width * s) / 2;
|
||||
final offsetY = (TopologyMinimap._minimapSize.height - widget.canvasSize.height * s) / 2;
|
||||
final canvasX = (localPos.dx - offsetX) / s;
|
||||
final canvasY = (localPos.dy - offsetY) / s;
|
||||
|
||||
final currentScale = widget.controller.value.getMaxScaleOnAxis();
|
||||
final tx = widget.viewportSize.width / 2 - canvasX * currentScale;
|
||||
final ty = widget.viewportSize.height / 2 - canvasY * currentScale;
|
||||
final result = Matrix4.translationValues(tx, ty, 0);
|
||||
result.multiply(Matrix4.diagonal3Values(currentScale, currentScale, 1.0));
|
||||
widget.controller.value = result;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final cs = Theme.of(context).colorScheme;
|
||||
|
||||
return GestureDetector(
|
||||
onTapDown: (d) => _onTap(d.localPosition),
|
||||
child: Material(
|
||||
color: cs.surfaceContainerHighest,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
elevation: 2,
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child: SizedBox(
|
||||
width: TopologyMinimap._minimapSize.width,
|
||||
height: TopologyMinimap._minimapSize.height,
|
||||
child: CustomPaint(
|
||||
painter: _MinimapPainter(
|
||||
graph: widget.graph,
|
||||
nodePositions: widget.nodePositions,
|
||||
canvasSize: widget.canvasSize,
|
||||
controller: widget.controller,
|
||||
viewportSize: widget.viewportSize,
|
||||
colorScheme: cs,
|
||||
scale: _scale,
|
||||
toMinimap: _toMinimap,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _MinimapPainter extends CustomPainter {
|
||||
_MinimapPainter({
|
||||
required this.graph,
|
||||
required this.nodePositions,
|
||||
required this.canvasSize,
|
||||
required this.controller,
|
||||
required this.viewportSize,
|
||||
required this.colorScheme,
|
||||
required this.scale,
|
||||
required this.toMinimap,
|
||||
});
|
||||
|
||||
final TopologyGraph graph;
|
||||
final Map<String, Offset> nodePositions;
|
||||
final Size canvasSize;
|
||||
final TransformationController controller;
|
||||
final Size viewportSize;
|
||||
final ColorScheme colorScheme;
|
||||
final double scale;
|
||||
final Offset Function(Offset) toMinimap;
|
||||
|
||||
static const Size _nodeRectSize = Size(8, 5);
|
||||
|
||||
Color _roleColor(NetworkDeviceRole? role) {
|
||||
switch (role) {
|
||||
case NetworkDeviceRole.core:
|
||||
return colorScheme.error;
|
||||
case NetworkDeviceRole.distribution:
|
||||
return colorScheme.tertiary;
|
||||
case NetworkDeviceRole.access:
|
||||
return colorScheme.primary;
|
||||
case NetworkDeviceRole.edge:
|
||||
return colorScheme.secondary;
|
||||
case NetworkDeviceRole.endpoint:
|
||||
case null:
|
||||
return colorScheme.outline;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Size size) {
|
||||
// Edges
|
||||
final edgePaint = Paint()
|
||||
..color = colorScheme.outline.withValues(alpha: 0.4)
|
||||
..strokeWidth = 0.6
|
||||
..style = PaintingStyle.stroke
|
||||
..strokeCap = StrokeCap.round;
|
||||
|
||||
for (final edge in graph.edges) {
|
||||
final fromPos = nodePositions[edge.fromDeviceId];
|
||||
final toPos = nodePositions[edge.toDeviceId];
|
||||
if (fromPos == null || toPos == null) continue;
|
||||
|
||||
// Use node centres
|
||||
final from = toMinimap(fromPos + const Offset(80, 55));
|
||||
final to = toMinimap(toPos + const Offset(80, 55));
|
||||
canvas.drawLine(from, to, edgePaint);
|
||||
}
|
||||
|
||||
// Device nodes
|
||||
for (final node in graph.nodes) {
|
||||
final pos = nodePositions[node.device.id];
|
||||
if (pos == null) continue;
|
||||
final minimapPos = toMinimap(pos);
|
||||
final rect = Rect.fromLTWH(
|
||||
minimapPos.dx - _nodeRectSize.width / 2,
|
||||
minimapPos.dy - _nodeRectSize.height / 2,
|
||||
_nodeRectSize.width,
|
||||
_nodeRectSize.height,
|
||||
);
|
||||
canvas.drawRRect(
|
||||
RRect.fromRectAndRadius(rect, const Radius.circular(1.5)),
|
||||
Paint()
|
||||
..color = _roleColor(node.device.role)
|
||||
..style = PaintingStyle.fill,
|
||||
);
|
||||
}
|
||||
|
||||
// Viewport rectangle
|
||||
final m = controller.value;
|
||||
final currentScale = m.getMaxScaleOnAxis();
|
||||
if (currentScale > 0) {
|
||||
// The translation is in the 3rd column of the matrix
|
||||
final tx = m.getTranslation().x;
|
||||
final ty = m.getTranslation().y;
|
||||
|
||||
// Viewport in canvas coordinates
|
||||
final vpLeft = -tx / currentScale;
|
||||
final vpTop = -ty / currentScale;
|
||||
final vpWidth = viewportSize.width / currentScale;
|
||||
final vpHeight = viewportSize.height / currentScale;
|
||||
|
||||
final tl = toMinimap(Offset(vpLeft, vpTop));
|
||||
final br = toMinimap(Offset(vpLeft + vpWidth, vpTop + vpHeight));
|
||||
final vpRect = Rect.fromPoints(tl, br);
|
||||
|
||||
// Fill
|
||||
canvas.drawRect(
|
||||
vpRect,
|
||||
Paint()
|
||||
..color = colorScheme.primary.withValues(alpha: 0.08)
|
||||
..style = PaintingStyle.fill,
|
||||
);
|
||||
// Border
|
||||
canvas.drawRect(
|
||||
vpRect,
|
||||
Paint()
|
||||
..color = colorScheme.primary.withValues(alpha: 0.6)
|
||||
..strokeWidth = 1.0
|
||||
..style = PaintingStyle.stroke,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldRepaint(covariant _MinimapPainter old) => true;
|
||||
}
|
||||
Reference in New Issue
Block a user