- private static final Font FONT = new Font("Tahoma", Font.PLAIN, (int)(DPIUtil.upscale(9) * MapScalingTransform.getScaleY() + 0.5));
-
- private static final long serialVersionUID = 1L;
-
- public static final String NODE_KEY = "DISTRICT_NETWORK_HOVER_INFO";
-
- private static final int W1 = 50;
- private static final int W2 = 30;
- private static final int PAD = 5;
-
- private List<Tuple3> labels;
-
- @SuppressWarnings("unused")
- private Point2D origin;
-
- private boolean hover = false;
-
- private Point2D mousePosition;
-
- private static AtomicReference<DistrictNetworkHoverInfoNode> activeNode = new AtomicReference<>();
-
- @Override
- public void render(Graphics2D g) {
- if (!hover || activeNode.get() != this)
- return;
-
- ParentNode<?> root = (ParentNode<?>) NodeUtil.getNearestParentOfType(this, RTreeNode.class);
- DeferredRenderingNode deferred = root != null ? (DeferredRenderingNode) root.getNode(DistrictNetworkHoverInfoStyle.HOVER_INFO_DEFERRED) : null;
- if (deferred != null)
- deferred.deferNode(g.getTransform(), this);
- else
- renderDeferred(g);
- }
-
- @Override
- public void renderDeferred(Graphics2D g) {
- AffineTransform ot = g.getTransform();
- Font of = g.getFont();
- doRender(g);
- g.setFont(of);
- g.setTransform(ot);
- }
-
- private void doRender(Graphics2D g) {
- g.transform(MapScalingTransform.INVERSE);
- g.translate(mousePosition.getX(), mousePosition.getY());
- //g.translate(origin.getX(), origin.getY());
- double scale = 1.5 / g.getTransform().getScaleX();
- g.scale(scale, scale);
-
- g.setFont(FONT);
- double rowHeight = g.getFontMetrics().getHeight() * 1.1;
-
- // let's calculate the max width
- Optional<Integer> max = labels.stream().map(t -> g.getFontMetrics().stringWidth((String) t.c2)).max(Comparator.naturalOrder());
- int width = max.orElse(10);
+ private static final long serialVersionUID = 1L;
+
+ private static final String HOVER_INFO_DEFERRED = "hoverInfo";
+
+ public static final String NODE_KEY = "DISTRICT_NETWORK_HOVER_INFO";
+
+ private static final int PAD = 15;
+
+ private List<Tuple3> labels;
+
+ private Font font = new Font(Font.SANS_SERIF, Font.PLAIN, DPIUtil.upscale(14));
+
+ @SuppressWarnings("unused")
+ private Point2D origin;
+
+ private boolean hover = false;
+
+ private Point2D mousePosition;
+
+ /**
+ * For rendering and clipping the hover info base rectangle.
+ */
+ private Rectangle2D bgRect = new Rectangle2D.Double();
+
+ private static AtomicReference<INode> activeNode = new AtomicReference<>();
+
+ @Override
+ public void render(Graphics2D g) {
+ ParentNode<?> root = (ParentNode<?>) NodeUtil.getNearestParentOfType(this, RTreeNode.class);
+ DeferredRenderingNode deferred = root != null ? (DeferredRenderingNode) root.getNode(HOVER_INFO_DEFERRED) : null;
+ if (deferred != null)
+ deferred.deferNode(g.getTransform(), this);
+ else
+ renderDeferred(g);
+ }
+
+ @Override
+ public void renderDeferred(Graphics2D g) {
+ if (!hover || activeNode.get() == null)
+ return;
+ if (labels == null || labels.isEmpty() || mousePosition == null)
+ return;
+ AffineTransform ot = g.getTransform();
+ Font of = g.getFont();
+ doRender(g);
+ g.setFont(of);
+ g.setTransform(ot);
+ }
+
+ private ToIntFunction<String> widthMeasurer(FontMetrics fm) {
+ return s -> fm.stringWidth(s);
+ }
+
+ private void doRender(Graphics2D g) {
+ AffineTransform tt = getTransform();
+ g.transform(tt);
+
+ g.translate(mousePosition.getX(), mousePosition.getY());
+ //g.translate(origin.getX(), origin.getY());
+ double scale = 1.0 / GeometryUtils.getScale(g.getTransform());
+ g.scale(scale, scale);
+ g.setFont(font);
+
+ FontMetrics fm = g.getFontMetrics();
+ ToIntFunction<String> sizer = widthMeasurer(fm);
+
+ double rowHeight = fm.getHeight() * 1.1;
+
+ // Let's calculate the max widths of each column.
+ // However, the last label is assumed to be a title row.
+ List<Tuple3> values = labels.subList(0, labels.size() - 1);
+ Tuple3 title = labels.get(labels.size() - 1);
+
+ int maxLabelWidth = values.stream().map(t -> (String) t.c0).mapToInt(sizer).max().orElse(PAD);
+ int[] valueWidths = values.stream().map(t -> (String) t.c1).mapToInt(sizer).toArray();
+ int maxValueWidth = Arrays.stream(valueWidths).max().orElse(PAD);
+ int maxUnitWidth = values.stream().map(t -> (String) t.c2).mapToInt(sizer).max().orElse(PAD);
+
+ String titleString =
+ (Objects.toString(title.c0, "")
+ + " " + Objects.toString(title.c1, "")
+ + " " + Objects.toString(title.c2, ""))
+ .trim();
+ int titleWidth = sizer.applyAsInt(titleString);
+
+ int titleRowWidth = PAD + titleWidth + PAD;
+ int maxValueRowWidth = PAD + maxLabelWidth + PAD + maxValueWidth + PAD + maxUnitWidth + PAD;
+
+ int totalWidth = Math.max(maxValueRowWidth, titleRowWidth);
+ int totalHeight = (int) Math.round((rowHeight) * labels.size());
+
+ double minX = -(PAD + maxLabelWidth + PAD + maxValueWidth + PAD);
+ double minY = -(totalHeight + (int)Math.round(rowHeight));
+
+ bgRect.setRect(minX, minY, totalWidth, totalHeight + PAD);
+ confineIn(bgRect, g.getClip().getBounds2D());
+