diff --git a/urban-spork-client-gui/resource/console.css b/urban-spork-client-gui/resource/console.css index cf3317d..77ea7b2 100644 --- a/urban-spork-client-gui/resource/console.css +++ b/urban-spork-client-gui/resource/console.css @@ -151,8 +151,8 @@ GridPane { -fx-background-color: #000; } -.chart-series-line { - -fx-stroke-width: 2; +.chart { + -fx-max-height: 250; } .chart-legend { @@ -167,6 +167,10 @@ GridPane { -fx-background-color: transparent; } +.chart-series-line { + -fx-stroke-width: 2; +} + .default-color0.chart-series-line { -fx-stroke: #0a84ff; } diff --git a/urban-spork-client-gui/src/com/urbanspork/client/gui/console/Console.java b/urban-spork-client-gui/src/com/urbanspork/client/gui/console/Console.java index 56b7b41..139718b 100644 --- a/urban-spork-client-gui/src/com/urbanspork/client/gui/console/Console.java +++ b/urban-spork-client-gui/src/com/urbanspork/client/gui/console/Console.java @@ -56,6 +56,7 @@ import javafx.scene.layout.HBox; import javafx.scene.layout.Priority; import javafx.scene.layout.RowConstraints; +import javafx.scene.layout.StackPane; import javafx.stage.Stage; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -332,8 +333,8 @@ private JFXTabPane initTabPane() { tab0.setClosable(false); // tab1 Tab tab1 = newSingleNodeTab(logTextArea, I18N.getString(I18N.CONSOLE_TAB1_TEXT)); - // - Tab tab2 = newSingleNodeTab(new TrafficCounterLineChart(instance).init(), I18N.getString(I18N.CONSOLE_TAB2_TEXT)); + // tab2 + Tab tab2 = initTrafficTab(); // ==================== // main tab pane // ==================== @@ -375,6 +376,16 @@ private Tab newSingleNodeTab(Node node, String tabTitle) { return tab; } + private Tab initTrafficTab() { + StackPane stackPane = new StackPane(); + stackPane.setAlignment(Pos.TOP_CENTER); + stackPane.getChildren().add(new TrafficCounterLineChart(instance).init()); + Tab tab = new Tab(I18N.getString(I18N.CONSOLE_TAB2_TEXT)); + tab.setContent(stackPane); + tab.setClosable(false); + return tab; + } + private void addGridPane0Children(GridPane gridPane0) { // ---------- Grid Children ---------- gridPane0.add(wrap(moveUpServerConfigButton), 1, 13); diff --git a/urban-spork-client-gui/src/com/urbanspork/client/gui/spine/CatmullRom.java b/urban-spork-client-gui/src/com/urbanspork/client/gui/spine/CatmullRom.java new file mode 100644 index 0000000..fbce621 --- /dev/null +++ b/urban-spork-client-gui/src/com/urbanspork/client/gui/spine/CatmullRom.java @@ -0,0 +1,98 @@ +package com.urbanspork.client.gui.spine; + +import javafx.geometry.Point2D; + +/** + * Java implement of Centripetal Catmull–Rom spline + * + * @param alpha The alpha value ranges from 0 to 1 + * + * @author Zmax0 + */ +public record CatmullRom(double alpha) { + /** + * Calculate Catmull-Rom for a sequence of initial points and return the combined curve. + * + * @param points Base points from which the quadruples for the algorithm are taken + * @param segment The number of points include in each curve segment + * @return The combined curve + */ + public Point2D[] interpolate(Point2D[] points, int segment) { + if (points.length < 2) { + throw new IllegalArgumentException("The points parameter must be greater than 2"); + } + if (points.length < 3) { + return points; + } + Point2D[] controls = getControls(points); + Point2D[] curve = new Point2D[points.length + (points.length - 1) * segment]; + for (int i = 0; i < controls.length - 3; i++) { + curve[i * (segment + 1)] = controls[i + 1]; + Point2D[] temp = interpolate(controls[i], controls[i + 1], controls[i + 2], controls[i + 3], segment); + System.arraycopy(temp, 0, curve, i * (segment + 1) + 1, segment); + } + curve[curve.length - 1] = (controls[controls.length - 2]); // last + return curve; + } + + private Point2D[] getControls(Point2D[] point) { + Point2D[] controls = new Point2D[point.length + 2]; + System.arraycopy(point, 0, controls, 1, point.length); + // set two control points at start C1 and end C2 + if (point[0].equals(point[point.length - 1])) { // if is close + // C1 = P(n-1) + controls[0] = point[point.length - 2]; + // C2 = P1 + controls[controls.length - 1] = point[1]; + } else { + // C1 = P0 - (P1 - P0) + controls[0] = point[0].subtract(point[1].subtract(point[0])); + // C2 = Pn + (Pn - P(n-1)) + controls[controls.length - 1] = point[point.length - 1].add(point[point.length - 1].subtract(point[point.length - 2])); + } + return controls; + } + + /** + * Calculate Catmull-Rom spline curve points starts with p1 and ends with p2 + * + * @param p0 The (x,y) point pairs that define the Catmull-Rom spline + * @param p1 The (x,y) point pairs that define the Catmull-Rom spline + * @param p2 The (x,y) point pairs that define the Catmull-Rom spline + * @param p3 The (x,y) point pairs that define the Catmull-Rom spline + * @param segment The number of points to include in each curve segment + * @return points for the resulting curve + */ + public Point2D[] interpolate(Point2D p0, Point2D p1, Point2D p2, Point2D p3, int segment) { + // calculate knots + double t0 = 0; + double t1 = getT(t0, p0, p1); + double t2 = getT(t1, p1, p2); + double t3 = getT(t2, p2, p3); + double step = (t2 - t1) / (segment - 1); + // evaluate the point + Point2D[] curve = new Point2D[segment]; + curve[0] = p1; + for (int i = 1; i < segment - 1; i++) { + double t = t1 + i * step; + Point2D a1 = p0.multiply((t1 - t) / (t1 - t0)).add(p1.multiply((t - t0) / (t1 - t0))); + Point2D a2 = p1.multiply((t2 - t) / (t2 - t1)).add(p2.multiply((t - t1) / (t2 - t1))); + Point2D a3 = p2.multiply((t3 - t) / (t3 - t2)).add(p3.multiply((t - t2) / (t3 - t2))); + Point2D b1 = a1.multiply((t2 - t) / (t2 - t0)).add(a2.multiply((t - t0) / (t2 - t0))); + Point2D b2 = a2.multiply((t3 - t) / (t3 - t1)).add(a3.multiply((t - t1) / (t3 - t1))); + curve[i] = b1.multiply((t2 - t) / (t2 - t1)).add(b2.multiply((t - t1) / (t2 - t1))); + } + curve[curve.length - 1] = p2; + return curve; + } + + // calculate knots + private double getT(double t, Point2D p0, Point2D p1) { + Point2D d = p1.subtract(p0); + return Math.pow(d.dotProduct(d), alpha * .5) + t; + } +} diff --git a/urban-spork-client-gui/src/com/urbanspork/client/gui/traffic/TrafficCounterLineChart.java b/urban-spork-client-gui/src/com/urbanspork/client/gui/traffic/TrafficCounterLineChart.java index c21c25a..ae7e4d7 100644 --- a/urban-spork-client-gui/src/com/urbanspork/client/gui/traffic/TrafficCounterLineChart.java +++ b/urban-spork-client-gui/src/com/urbanspork/client/gui/traffic/TrafficCounterLineChart.java @@ -1,15 +1,20 @@ package com.urbanspork.client.gui.traffic; import com.urbanspork.client.Client; +import com.urbanspork.client.gui.spine.CatmullRom; import io.netty.handler.traffic.TrafficCounter; import javafx.animation.Animation; import javafx.animation.KeyFrame; import javafx.animation.Timeline; import javafx.beans.property.ObjectProperty; import javafx.collections.ObservableList; +import javafx.geometry.Point2D; import javafx.scene.chart.LineChart; import javafx.scene.chart.NumberAxis; import javafx.scene.chart.XYChart; +import javafx.scene.shape.LineTo; +import javafx.scene.shape.Path; +import javafx.scene.shape.PathElement; import javafx.util.Duration; public class TrafficCounterLineChart { @@ -27,7 +32,49 @@ public TrafficCounterLineChart(ObjectProperty instance) { public LineChart init() { NumberAxis xAxis = new NumberAxis(0, WINDOW, WINDOW); NumberAxis yAxis = new NumberAxis(); - LineChart lineChart = new LineChart<>(xAxis, yAxis); + LineChart lineChart = new LineChart<>(xAxis, yAxis) { + @Override + protected void layoutPlotChildren() { + super.layoutPlotChildren(); + ObservableList> data = getData(); + data.stream().map(Series::getNode).mapMulti((node, consumer) -> { + if (node instanceof Path path) { + consumer.accept(path); + } + }).forEach(this::curve); + } + + private void curve(Path path) { + ObservableList elements = path.getElements(); + if (elements.size() > 3) { + curve(elements); + } + } + + private void curve(ObservableList elements) { + int size = elements.size(); + Point2D[] points = new Point2D[size - 1]; + for (int i = 0; i < points.length; i++) { + PathElement e = elements.get(i + 1); + if (e instanceof LineTo lineTo) { + points[i] = new Point2D(lineTo.getX(), lineTo.getY()); + } + } + Point2D[] interpolate = new CatmullRom(0.5).interpolate(points, 32); + // update + for (int i = 1; i < size; i++) { + PathElement e = elements.get(i); + if (e instanceof LineTo lineTo) { + lineTo.setX(interpolate[i - 1].getX()); + lineTo.setY(interpolate[i - 1].getY()); + } + } + // add + for (int i = 0; i < interpolate.length - size; i++) { + elements.add(new LineTo(interpolate[i + size].getX(), interpolate[i + size].getY())); + } + } + }; lineChart.setAnimated(false); lineChart.setCreateSymbols(false); ObservableList> data = lineChart.getData();