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
+ *
+ * - 0.5 for the centripetal spline
+ *
- 0.0 for the uniform spline
+ *
- 1.0 for the chordal spline
+ *
+ * @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();