Skip to content

Commit 359fb12

Browse files
committed
Push primitive Chart API.
It has some issues, but otherwise looks neat.
1 parent ea82c09 commit 359fb12

9 files changed

Lines changed: 591 additions & 0 deletions

File tree

src/main/java/de/geolykt/starloader/api/gui/Drawing.java

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import org.jetbrains.annotations.Nullable;
88

99
import com.badlogic.gdx.graphics.Camera;
10+
import com.badlogic.gdx.graphics.Color;
1011
import com.badlogic.gdx.graphics.Texture;
1112
import com.badlogic.gdx.graphics.g2d.BitmapFont;
1213
import com.badlogic.gdx.graphics.g2d.SpriteBatch;
@@ -49,6 +50,29 @@ public static enum TextSize {
4950

5051
private static DrawingImpl implementation;
5152

53+
/**
54+
* Draws a line on the user interface. Due to libGDX not supporting this
55+
* operation in a conventional way this method will by default be implemented by
56+
* stretching and rotating a pixel. As such this method indirectly calls
57+
* {@link Math#atan2(double, double)} and {@link Math#sqrt(double)} - but even
58+
* if libGDX had a .drawLine method, it would more or less involve using one of
59+
* these methods in the first place, so the caller should not worry about
60+
* performance all too much.
61+
*
62+
* @param x1 The X position of the origin point of the line
63+
* @param y1 The Y position of the origin point of the line
64+
* @param x2 The X position of the target point of the line
65+
* @param y2 The Y position of the target point of the line
66+
* @param width The width of the line to draw
67+
* @param color The color of the line that should be drawn
68+
* @param camera The camera, used to move the input positions to the global
69+
* context.
70+
*/
71+
public static void drawLine(double x1, double y1, double x2, double y2, float width, @NotNull Color color,
72+
@NotNull Camera camera) {
73+
implementation.drawLine(x1, y1, x2, y2, width, color, camera);
74+
}
75+
5276
/**
5377
* Draws text at the given location. The default color is used, which under
5478
* normal circumstances that's white, however the exact color is dependent on
@@ -129,6 +153,30 @@ public static float drawText(@NotNull String message, float x, float y, @NotNull
129153
return implementation.drawText(message, x, y, color);
130154
}
131155

156+
/**
157+
* <b>As specified by the APINote, this method has unintended consequences. It does not only operate
158+
* like a fillRect() method, but also draws a frame around the rectangle. More specifically
159+
* this frame is assumed to be linked with {@link TextureProvider#getAlternateWindowNinepatch()}.</b>
160+
*
161+
* <p>Fills a rectangle with a given width and height with a specified color. As a
162+
* friendly reminder, the position 0,0 is the lower left corner and as the
163+
* values increase it moves to the to top right corner.
164+
*
165+
* @param x The X position of the top left corner of the area to fill.
166+
* @param y The Y position of the top left corner of the area to fill.
167+
* @param width The width of the rectangle to fill.
168+
* @param height The height of the rectangle to fill.
169+
* @param color The color used for the operation.
170+
* @param camera The camera used for the operation.
171+
* @apiNote It is not known what the effects are if this method is called by
172+
* anything but the Widget. Act carefully for you may not want to call
173+
* this method.
174+
*/
175+
public static void fillWindow(float x, float y, float width, float height, @NotNull Color color,
176+
@NotNull Camera camera) {
177+
implementation.fillWindow(x, y, width, height, color, camera);
178+
}
179+
132180
/**
133181
* Obtains the main drawing sprite batch. Operations performed on this batch
134182
* will result in them getting displayed on the user interface.
@@ -159,6 +207,17 @@ public static float drawText(@NotNull String message, float x, float y, @NotNull
159207
return implementation.getAvailiableFonts();
160208
}
161209

210+
/**
211+
* Obtains the currently valid instance of {@link DrawingImpl} that is used to delegate static methods of this class.
212+
* Should there be no such instance, then null will be returned.
213+
*
214+
* @return The current instance used as a delegate.
215+
* @see Drawing#requireInstance()
216+
*/
217+
public static @Nullable DrawingImpl getInstance() {
218+
return implementation;
219+
}
220+
162221
/**
163222
* Obtains the instance's {@link TextFactory}.
164223
*
@@ -191,6 +250,22 @@ public static float drawText(@NotNull String message, float x, float y, @NotNull
191250
return implementation.loadTexture(path);
192251
}
193252

253+
/**
254+
* Obtains the currently valid instance of {@link DrawingImpl} that is used to delegate static methods of this class.
255+
* Should there be no such instance, then a {@link IllegalStateException} will be thrown.
256+
*
257+
* @return The current instance used as a delegate.
258+
* @see Drawing#getInstance()
259+
* @throws IllegalStateException if the Drawing implementation has not yet been set.
260+
*/
261+
public static @NotNull DrawingImpl requireInstance() {
262+
DrawingImpl implementation = Drawing.implementation;
263+
if (implementation == null) {
264+
throw new IllegalStateException("Drawing implementation not yet set.");
265+
}
266+
return implementation;
267+
}
268+
194269
/**
195270
* Sends a bulletin to the player which is visible in the bottom left in most
196271
* cases.

src/main/java/de/geolykt/starloader/api/gui/DrawingImpl.java

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import org.jetbrains.annotations.Nullable;
77

88
import com.badlogic.gdx.graphics.Camera;
9+
import com.badlogic.gdx.graphics.Color;
910
import com.badlogic.gdx.graphics.Texture;
1011
import com.badlogic.gdx.graphics.g2d.BitmapFont;
1112
import com.badlogic.gdx.graphics.g2d.SpriteBatch;
@@ -22,6 +23,27 @@
2223
*/
2324
public interface DrawingImpl {
2425

26+
/**
27+
* Draws a line on the user interface. Due to libGDX not supporting this
28+
* operation in a conventional way this method will by default be implemented by
29+
* stretching and rotating a pixel. As such this method indirectly calls
30+
* {@link Math#atan2(double, double)} and {@link Math#sqrt(double)} - but even
31+
* if libGDX had a .drawLine method, it would more or less involve using one of
32+
* these methods in the first place, so the caller should not worry about
33+
* performance all too much.
34+
*
35+
* @param x1 The X position of the origin point of the line
36+
* @param y1 The Y position of the origin point of the line
37+
* @param x2 The X position of the target point of the line
38+
* @param y2 The Y position of the target point of the line
39+
* @param width The width of the line to draw
40+
* @param color The color of the line that should be drawn
41+
* @param camera The camera, used to move the input positions to the global
42+
* context.
43+
*/
44+
public void drawLine(double x1, double y1, double x2, double y2, float width, @NotNull Color color,
45+
@NotNull Camera camera);
46+
2547
/**
2648
* Draws text at the given location. The default color is used, which under
2749
* normal circumstances that's white, however the exact color is dependent on
@@ -93,6 +115,28 @@ public default float drawText(@NotNull String message, float x, float y, @NotNul
93115
return this.drawText(message, x, y, color.toGalimulatorColor());
94116
}
95117

118+
/**
119+
* <b>As specified by the APINote, this method has unintended consequences. It does not only operate
120+
* like a fillRect() method, but also draws a frame around the rectangle. More specifically
121+
* this frame is assumed to be linked with {@link TextureProvider#getAlternateWindowNinepatch()}.</b>
122+
*
123+
* <p>Fills a rectangle with a given width and height with a specified color. As a
124+
* friendly reminder, the position 0,0 is the lower left corner and as the
125+
* values increase it moves to the to top right corner.
126+
*
127+
* @param x The X position of the top left corner of the area to fill.
128+
* @param y The Y position of the top left corner of the area to fill.
129+
* @param width The width of the rectangle to fill.
130+
* @param height The height of the rectangle to fill.
131+
* @param color The color used for the operation.
132+
* @param camera The camera used for the operation.
133+
* @apiNote It is not known what the effects are if this method is called by
134+
* anything but the Widget. Act carefully for you may not want to call
135+
* this method.
136+
*/
137+
public void fillWindow(float x, float y, float width, float height, @NotNull Color color,
138+
@NotNull Camera camera);
139+
96140
/**
97141
* Obtains the font types that are available in this implementation.
98142
*
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
package de.geolykt.starloader.api.gui.graph;
2+
3+
import java.util.Collection;
4+
5+
import org.jetbrains.annotations.NotNull;
6+
7+
/**
8+
* Represents pure chart data that is later used for visualisation.
9+
*
10+
* @param <V> The type of the vertices of the Graph.
11+
*/
12+
public interface ChartData<V> {
13+
14+
/**
15+
* Obtains the edges that should be displayed in the chart.
16+
* The returned collection does not need to be modifiable.
17+
*
18+
* @return The edges used for visualisation.
19+
*/
20+
@NotNull
21+
public Collection<ValueEdge<V>> getEdges();
22+
23+
/**
24+
* Obtains the "height" of the chart. It should have the same scale as the {@link ValueEdge#vertex1Value}
25+
* and {@link ValueEdge#vertex2Value} of the Edges returned by {@link #getEdges()}.
26+
*
27+
* @return The height of the chart.
28+
*/
29+
public int getHeight();
30+
31+
/**
32+
* Obtains the "width" of the chart. It should have the same scale as the {@link ValueEdge#vertex1Position}
33+
* and {@link ValueEdge#vertex2Position} of the Edges returned by {@link #getEdges()}.
34+
*
35+
* @return The width of the chart.
36+
*/
37+
public int getWidth();
38+
}
Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
package de.geolykt.starloader.api.gui.graph;
2+
3+
import java.util.Objects;
4+
5+
import org.jetbrains.annotations.NotNull;
6+
7+
import com.badlogic.gdx.graphics.Camera;
8+
import com.badlogic.gdx.graphics.Color;
9+
10+
import de.geolykt.starloader.api.empire.Alliance;
11+
import de.geolykt.starloader.api.empire.Empire;
12+
import de.geolykt.starloader.api.gui.Drawing;
13+
import de.geolykt.starloader.api.gui.DrawingImpl;
14+
import de.geolykt.starloader.api.gui.screen.LineWrappingInfo;
15+
import de.geolykt.starloader.api.gui.screen.Screen;
16+
import de.geolykt.starloader.api.gui.screen.ScreenComponent;
17+
18+
/**
19+
* A simple visualisation of a {@link ChartData} object.
20+
* This class only depends on API classes, and as such this class is safe to subclass.
21+
*/
22+
public class LineChart implements ScreenComponent {
23+
24+
// FIXME I have no fully tested this class with the retractors that occurred with the screen API.
25+
// However I recall it having some issues with the component rendering too high.
26+
// I have however fixed an error I did, so this issue may no longer be present.
27+
28+
/**
29+
* The parent screen as defined by {@link ScreenComponent#getParentScreen()}.
30+
*/
31+
protected @NotNull Screen parent;
32+
33+
/**
34+
* The {@link LineWrappingInfo} as defined by {@link #getLineWrappingInfo()}.
35+
*/
36+
protected @NotNull LineWrappingInfo lwinfo;
37+
38+
/**
39+
* The chart data that needs to be visualised.
40+
*/
41+
protected @NotNull ChartData<? extends Object> chart;
42+
43+
/**
44+
* The total width of the component.
45+
*/
46+
protected final int height;
47+
48+
/**
49+
* The total height of the component.
50+
*/
51+
protected final int width;
52+
53+
/**
54+
* The constructor.
55+
*
56+
* @param parent The screen this component depends on. Used for {@link ScreenComponent#getParentScreen()}.
57+
* @param wrappingInfo The {@link LineWrappingInfo} that is used for the screen implementation. Returned later on by {@link ScreenComponent#getLineWrappingInfo()}.
58+
* @param chart The chart that should be visualised by this component.
59+
* @param width The width of the component.
60+
* @param height The height of the component.
61+
*/
62+
public LineChart(@NotNull Screen parent, @NotNull LineWrappingInfo wrappingInfo, @NotNull ChartData<? extends Object> chart,
63+
int width, int height) {
64+
this.parent = Objects.requireNonNull(parent, "parent may not be null.");
65+
this.lwinfo = Objects.requireNonNull(wrappingInfo, "wrappingInfo may not be null.");
66+
this.chart = Objects.requireNonNull(chart, "chart may not be null.");
67+
this.width = width;
68+
this.height = height;
69+
}
70+
71+
@Override
72+
public int renderAt(float x, float y, @NotNull Camera camera) {
73+
@SuppressWarnings("null")
74+
@NotNull Color white = Color.WHITE;
75+
76+
DrawingImpl graphics = Drawing.requireInstance();
77+
graphics.fillWindow(x, y, getWidth(), -getHeight(), white, camera);
78+
79+
float pixelsPerValue = getChartHeight() / chart.getHeight();
80+
float pixelsPerIntervall;
81+
if (chart instanceof RollingChartData) {
82+
pixelsPerIntervall = getChartWidth() / (((RollingChartData<?>) chart).getCurrentPositon() - 1);
83+
} else {
84+
pixelsPerIntervall = getChartWidth() / chart.getWidth();
85+
}
86+
float thickness = getLineThickness();
87+
x += getChartXOffset();
88+
y -= getChartYOffset();
89+
90+
for (ValueEdge<? extends Object> edge : chart.getEdges()) {
91+
if (edge.vertex1Position < 0) {
92+
continue;
93+
}
94+
float x1 = x + (edge.vertex1Position - 1) * pixelsPerIntervall;
95+
float x2 = x + (edge.vertex2Position - 1) * pixelsPerIntervall;
96+
float y1 = edge.vertex1Value * pixelsPerValue;
97+
float y2 = edge.vertex2Value * pixelsPerValue;
98+
if (y1 < 0) {
99+
y1 = 0;
100+
}
101+
if (y2 < 0) {
102+
y2 = 0;
103+
}
104+
// Galimulator draws from bottom-to-top, but SLAPI places components top-to-bottom (this should not be true). As such "y" is the MAXIMUM coordinate we may draw to.
105+
// Comment from Nov 11 2021: I have no idea what above comment refers to, other than that the y coordinates of drawing operations are confusing
106+
// It is best to assume that this is the right way.
107+
y1 = y - getHeight() + y1;
108+
y2 = y - getHeight() + y2;
109+
graphics.drawLine(x1, y1, x2, y2, thickness, getColor(edge.vertex1), camera);
110+
}
111+
chart.getEdges();
112+
return getWidth();
113+
}
114+
115+
protected float getChartHeight() {
116+
return getHeight() - getChartYOffset();
117+
}
118+
119+
protected float getChartXOffset() {
120+
return 10.0F;
121+
}
122+
123+
protected float getChartYOffset() {
124+
return 0.0F;
125+
}
126+
127+
protected float getChartWidth() {
128+
return getWidth() - 20.0F;
129+
}
130+
131+
protected float getLineThickness() {
132+
return 2.0F;
133+
}
134+
135+
protected @NotNull Color getColor(Object o) {
136+
if (o instanceof Empire) {
137+
java.awt.Color awtColor = ((Empire) o).getAWTColor();
138+
return new Color(awtColor.getRed() / 255.0F, awtColor.getGreen() / 255.0F, awtColor.getBlue() / 255.0F, awtColor.getAlpha() / 255.0F);
139+
} else if (o instanceof Alliance) {
140+
java.awt.Color awtColor = ((Alliance) o).getAWTColor();
141+
return new Color(awtColor.getRed() / 255.0F, awtColor.getGreen() / 255.0F, awtColor.getBlue() / 255.0F, awtColor.getAlpha() / 255.0F);
142+
} else {
143+
Color c = new Color(o.hashCode());
144+
c.a = 1.0F;
145+
return c;
146+
}
147+
}
148+
149+
@Override
150+
public int getHeight() {
151+
return height;
152+
}
153+
154+
@Override
155+
public @NotNull LineWrappingInfo getLineWrappingInfo() {
156+
return lwinfo;
157+
}
158+
159+
@Override
160+
public @NotNull Screen getParentScreen() {
161+
return parent;
162+
}
163+
164+
@Override
165+
public int getWidth() {
166+
return width;
167+
}
168+
169+
@Override
170+
public boolean isSameType(@NotNull ScreenComponent component) {
171+
return component instanceof LineChart;
172+
}
173+
}

0 commit comments

Comments
 (0)