Skip to content

Commit 47a613a

Browse files
committed
add some metadata for better output handling
1 parent 4dca669 commit 47a613a

4 files changed

Lines changed: 365 additions & 24 deletions

File tree

apps/src/main/java/org/hortonmachine/database/addons/whetgeo/WhetgeoStateChartData.java

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,15 +32,26 @@
3232
*/
3333
public class WhetgeoStateChartData {
3434
public double[] gridEta = new double[0];
35+
/** Per-cell parameter-set label, same order as {@link #gridEta}; empty if the output
36+
* wasn't written with {@code parameter_id} (see {@code Whetgeo1DOutputsHandler}). */
37+
public int[] gridParameterID = new int[0];
3538

3639
public long[] topBCTimes = new long[0];
3740
public double[] topBCValues = new double[0];
41+
/** e.g. "TOP_COUPLED"; null if the output wasn't written with one
42+
* (see {@code Whetgeo1DOutputsHandler.TABLE_OUTPUT_METADATA}). */
43+
public String topBCType;
3844

3945
public long[] bottomBCTimes = new long[0];
4046
public double[] bottomBCValues = new double[0];
47+
public String bottomBCType;
4148

4249
public List<DepthSeries> depthSeries = new ArrayList<>();
4350

51+
/** SWRC parameter snapshot per parameter set id, if the output was written with one
52+
* (see {@code Whetgeo1DOutputsHandler.TABLE_OUTPUT_SWRC_PARAMETERS}); empty otherwise. */
53+
public List<SwrcParams> swrcParameters = new ArrayList<>();
54+
4455
/**
4556
* One state variable's full (timestamp, eta) -&gt; value grid, flattened into
4657
* three parallel arrays (one triple per row of {@code output_state}).
@@ -57,4 +68,23 @@ public DepthSeries( String name, String axisLabel ) {
5768
this.axisLabel = axisLabel;
5869
}
5970
}
71+
72+
/** One row of {@code output_swrc_parameters}: the soil properties for one parameter set. */
73+
public static class SwrcParams {
74+
public final int id;
75+
public final double thetaS;
76+
public final double thetaR;
77+
public final double ks;
78+
public final double n;
79+
public final double alpha;
80+
81+
public SwrcParams( int id, double thetaS, double thetaR, double ks, double n, double alpha ) {
82+
this.id = id;
83+
this.thetaS = thetaS;
84+
this.thetaR = thetaR;
85+
this.ks = ks;
86+
this.n = n;
87+
this.alpha = alpha;
88+
}
89+
}
6090
}

apps/src/main/java/org/hortonmachine/database/addons/whetgeo/WhetgeoStateChartDataLoader.java

Lines changed: 57 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
import java.util.Map;
2424

2525
import org.hortonmachine.database.addons.whetgeo.WhetgeoStateChartData.DepthSeries;
26+
import org.hortonmachine.database.addons.whetgeo.WhetgeoStateChartData.SwrcParams;
2627
import org.hortonmachine.dbs.compat.ADb;
2728
import org.hortonmachine.dbs.compat.IHMResultSet;
2829
import org.hortonmachine.dbs.compat.IHMStatement;
@@ -60,7 +61,17 @@ private WhetgeoStateChartDataLoader() {
6061
public static WhetgeoStateChartData load( ADb db ) throws Exception {
6162
WhetgeoStateChartData data = new WhetgeoStateChartData();
6263

63-
data.gridEta = loadGridEta(db);
64+
boolean withParameterID = hasColumn(db, Whetgeo1DOutputsHandler.TABLE_OUTPUT_GRID,
65+
Whetgeo1DOutputsHandler.COL_PARAMETER_ID);
66+
loadGrid(db, data, withParameterID);
67+
68+
if (withParameterID && db.hasTable(Whetgeo1DOutputsHandler.TABLE_OUTPUT_SWRC_PARAMETERS)) {
69+
data.swrcParameters = loadSwrcParameters(db);
70+
}
71+
72+
if (db.hasTable(Whetgeo1DOutputsHandler.TABLE_OUTPUT_METADATA)) {
73+
loadBCTypes(db, data);
74+
}
6475

6576
if (hasColumn(db, Whetgeo1DOutputsHandler.TABLE_OUTPUT_SCALARS, Whetgeo1DOutputsHandler.COL_TOP_BC)) {
6677
ScalarSeries topBC = loadScalarSeries(db, Whetgeo1DOutputsHandler.COL_TOP_BC);
@@ -85,19 +96,60 @@ public static WhetgeoStateChartData load( ADb db ) throws Exception {
8596
return data;
8697
}
8798

88-
private static double[] loadGridEta( ADb db ) throws Exception {
89-
String sql = "SELECT " + Whetgeo1DOutputsHandler.COL_ETA + " FROM " + Whetgeo1DOutputsHandler.TABLE_OUTPUT_GRID
90-
+ " ORDER BY " + Whetgeo1DOutputsHandler.COL_ETA;
99+
private static void loadGrid( ADb db, WhetgeoStateChartData data, boolean withParameterID ) throws Exception {
100+
String sql = "SELECT " + Whetgeo1DOutputsHandler.COL_ETA + (withParameterID
101+
? ", " + Whetgeo1DOutputsHandler.COL_PARAMETER_ID
102+
: "") + " FROM " + Whetgeo1DOutputsHandler.TABLE_OUTPUT_GRID + " ORDER BY "
103+
+ Whetgeo1DOutputsHandler.COL_ETA;
91104
List<Double> eta = new ArrayList<>();
105+
List<Integer> parameterID = new ArrayList<>();
92106
db.<Void>execOnConnection(connection -> {
93107
try (IHMStatement stmt = connection.createStatement(); IHMResultSet rs = stmt.executeQuery(sql)) {
94108
while( rs.next() ) {
95109
eta.add(rs.getDouble(1));
110+
if (withParameterID) {
111+
parameterID.add(rs.getInt(2));
112+
}
113+
}
114+
}
115+
return null;
116+
});
117+
data.gridEta = eta.stream().mapToDouble(Double::doubleValue).toArray();
118+
if (withParameterID) {
119+
data.gridParameterID = parameterID.stream().mapToInt(Integer::intValue).toArray();
120+
}
121+
}
122+
123+
private static List<SwrcParams> loadSwrcParameters( ADb db ) throws Exception {
124+
String sql = "SELECT " + Whetgeo1DOutputsHandler.COL_ID + ", " + Whetgeo1DOutputsHandler.COL_THETA_S + ", "
125+
+ Whetgeo1DOutputsHandler.COL_THETA_R + ", " + Whetgeo1DOutputsHandler.COL_KS + ", "
126+
+ Whetgeo1DOutputsHandler.COL_N + ", " + Whetgeo1DOutputsHandler.COL_ALPHA + " FROM "
127+
+ Whetgeo1DOutputsHandler.TABLE_OUTPUT_SWRC_PARAMETERS + " ORDER BY " + Whetgeo1DOutputsHandler.COL_ID;
128+
List<SwrcParams> params = new ArrayList<>();
129+
db.<Void>execOnConnection(connection -> {
130+
try (IHMStatement stmt = connection.createStatement(); IHMResultSet rs = stmt.executeQuery(sql)) {
131+
while( rs.next() ) {
132+
params.add(new SwrcParams(rs.getInt(1), rs.getDouble(2), rs.getDouble(3), rs.getDouble(4),
133+
rs.getDouble(5), rs.getDouble(6)));
134+
}
135+
}
136+
return null;
137+
});
138+
return params;
139+
}
140+
141+
private static void loadBCTypes( ADb db, WhetgeoStateChartData data ) throws Exception {
142+
String sql = "SELECT " + Whetgeo1DOutputsHandler.COL_TOP_BC_TYPE + ", "
143+
+ Whetgeo1DOutputsHandler.COL_BOTTOM_BC_TYPE + " FROM " + Whetgeo1DOutputsHandler.TABLE_OUTPUT_METADATA;
144+
db.<Void>execOnConnection(connection -> {
145+
try (IHMStatement stmt = connection.createStatement(); IHMResultSet rs = stmt.executeQuery(sql)) {
146+
if (rs.next()) {
147+
data.topBCType = rs.getString(1);
148+
data.bottomBCType = rs.getString(2);
96149
}
97150
}
98151
return null;
99152
});
100-
return eta.stream().mapToDouble(Double::doubleValue).toArray();
101153
}
102154

103155
private static ScalarSeries loadScalarSeries( ADb db, String column ) throws Exception {

apps/src/main/java/org/hortonmachine/database/addons/whetgeo/WhetgeoStateChartPanelBuilder.java

Lines changed: 129 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -17,27 +17,34 @@
1717
*/
1818
package org.hortonmachine.database.addons.whetgeo;
1919

20+
import java.awt.BasicStroke;
2021
import java.awt.BorderLayout;
2122
import java.awt.Color;
23+
import java.awt.FlowLayout;
2224
import java.awt.Font;
2325
import java.text.DecimalFormat;
2426
import java.util.ArrayList;
2527
import java.util.Arrays;
28+
import java.util.HashMap;
2629
import java.util.List;
30+
import java.util.Map;
2731

2832
import javax.swing.BorderFactory;
2933
import javax.swing.BoxLayout;
34+
import javax.swing.JCheckBox;
3035
import javax.swing.JLabel;
3136
import javax.swing.JPanel;
3237
import javax.swing.SwingConstants;
3338

3439
import org.hortonmachine.database.addons.whetgeo.WhetgeoStateChartData.DepthSeries;
40+
import org.hortonmachine.database.addons.whetgeo.WhetgeoStateChartData.SwrcParams;
3541
import org.hortonmachine.gears.utils.colors.ColorUtilities;
3642
import org.jfree.chart.ChartPanel;
3743
import org.jfree.chart.JFreeChart;
3844
import org.jfree.chart.axis.DateAxis;
3945
import org.jfree.chart.axis.NumberAxis;
4046
import org.jfree.chart.plot.CombinedDomainXYPlot;
47+
import org.jfree.chart.plot.ValueMarker;
4148
import org.jfree.chart.plot.XYPlot;
4249
import org.jfree.chart.renderer.PaintScale;
4350
import org.jfree.chart.renderer.xy.XYBarRenderer;
@@ -46,8 +53,10 @@
4653
import org.jfree.data.xy.DefaultIntervalXYDataset;
4754
import org.jfree.data.xy.DefaultXYZDataset;
4855
import org.jfree.data.xy.IntervalXYDataset;
56+
import org.jfree.ui.RectangleAnchor;
4957
import org.jfree.ui.RectangleEdge;
5058
import org.jfree.ui.RectangleInsets;
59+
import org.jfree.ui.TextAnchor;
5160

5261
/**
5362
* Builds the WHETGEO 1D state (Hovmoller-style) chart: an optional top/bottom
@@ -115,38 +124,66 @@ public static JPanel build( WhetgeoStateChartData data, String title ) {
115124
combinedPlot.setGap(12);
116125
boolean hasChartRow = false;
117126

127+
String topLabel = bcLabel("Top Boundary Condition", data.topBCType);
118128
double[] topDistinct = distinctSorted(data.topBCTimes.length > 0 ? data.topBCValues : new double[0]);
119129
if (data.topBCTimes.length > 0 && topDistinct.length > 1) {
120-
combinedPlot.add(buildBCPlot("Top Boundary Condition", data.topBCTimes, data.topBCValues, TOP_BC_COLOR), 1);
130+
combinedPlot.add(buildBCPlot(topLabel, data.topBCTimes, data.topBCValues, TOP_BC_COLOR), 1);
121131
hasChartRow = true;
122132
} else if (topDistinct.length == 1) {
123-
addConstantValueRow(constantRows, "Top Boundary Condition", topDistinct[0]);
133+
addConstantValueRow(constantRows, topLabel, topDistinct[0]);
124134
}
125135

136+
String bottomLabel = bcLabel("Bottom Boundary Condition", data.bottomBCType);
126137
double[] bottomDistinct = distinctSorted(data.bottomBCTimes.length > 0 ? data.bottomBCValues : new double[0]);
127138
if (data.bottomBCTimes.length > 0 && bottomDistinct.length > 1) {
128-
combinedPlot.add(buildBCPlot("Bottom Boundary Condition", data.bottomBCTimes, data.bottomBCValues,
129-
BOTTOM_BC_COLOR), 1);
139+
combinedPlot.add(buildBCPlot(bottomLabel, data.bottomBCTimes, data.bottomBCValues, BOTTOM_BC_COLOR), 1);
130140
hasChartRow = true;
131141
} else if (bottomDistinct.length == 1) {
132-
addConstantValueRow(constantRows, "Bottom Boundary Condition", bottomDistinct[0]);
142+
addConstantValueRow(constantRows, bottomLabel, bottomDistinct[0]);
133143
}
134144

145+
List<LayerBoundary> layerBoundaries = computeLayerBoundaries(data);
146+
List<XYPlot> heatmapPlots = new ArrayList<>();
147+
135148
List<PaintScaleLegend> legends = new ArrayList<>();
136149
for( int i = 0; i < data.depthSeries.size(); i++ ) {
137150
DepthSeries series = data.depthSeries.get(i);
138151
Color[] ramp = HEATMAP_RAMPS[i % HEATMAP_RAMPS.length];
139152
double[] bounds = valueBounds(series.values);
140153
PaintScale scale = new TwoColorPaintScale(bounds[0], bounds[1], ramp[0], ramp[1]);
141154

142-
combinedPlot.add(buildHeatmapPlot(series, scale), 2);
155+
XYPlot heatmapPlot = buildHeatmapPlot(series, scale, layerBoundaries);
156+
heatmapPlots.add(heatmapPlot);
157+
combinedPlot.add(heatmapPlot, 2);
143158
hasChartRow = true;
144159
legends.add(buildLegend(series, scale, bounds));
145160
}
146161

147-
JPanel panel = new JPanel(new BorderLayout());
162+
JPanel topArea = new JPanel();
163+
topArea.setLayout(new BoxLayout(topArea, BoxLayout.Y_AXIS));
164+
if (!layerBoundaries.isEmpty()) {
165+
JCheckBox showAnnotationsCheck = new JCheckBox("Show layer annotations", true);
166+
showAnnotationsCheck.setAlignmentX(JCheckBox.LEFT_ALIGNMENT);
167+
showAnnotationsCheck.addActionListener(e -> {
168+
boolean show = showAnnotationsCheck.isSelected();
169+
for( XYPlot plot : heatmapPlots ) {
170+
plot.clearRangeMarkers();
171+
if (show) {
172+
addLayerBoundaryMarkers(plot, layerBoundaries);
173+
}
174+
}
175+
});
176+
JPanel checkRow = new JPanel(new FlowLayout(FlowLayout.LEFT, 8, 4));
177+
checkRow.add(showAnnotationsCheck);
178+
topArea.add(checkRow);
179+
}
148180
if (constantRows.getComponentCount() > 0) {
149-
panel.add(constantRows, BorderLayout.NORTH);
181+
topArea.add(constantRows);
182+
}
183+
184+
JPanel panel = new JPanel(new BorderLayout());
185+
if (topArea.getComponentCount() > 0) {
186+
panel.add(topArea, BorderLayout.NORTH);
150187
}
151188
if (hasChartRow) {
152189
JFreeChart chart = new JFreeChart(title, JFreeChart.DEFAULT_TITLE_FONT, combinedPlot, false);
@@ -185,7 +222,7 @@ private static XYPlot buildBCPlot( String axisLabel, long[] times, double[] valu
185222
return plot;
186223
}
187224

188-
private static XYPlot buildHeatmapPlot( DepthSeries series, PaintScale scale ) {
225+
private static XYPlot buildHeatmapPlot( DepthSeries series, PaintScale scale, List<LayerBoundary> layerBoundaries ) {
189226
double[] xValues = series.times.length > 0 ? toDoubleArray(series.times) : new double[0];
190227
double[] yValues = series.eta;
191228
double[] zValues = series.values;
@@ -210,9 +247,84 @@ private static XYPlot buildHeatmapPlot( DepthSeries series, PaintScale scale ) {
210247
plot.setRenderer(0, renderer);
211248
plot.setRangeAxis(0, depthAxis);
212249
plot.mapDatasetToRangeAxis(0, 0);
250+
251+
addLayerBoundaryMarkers(plot, layerBoundaries);
252+
213253
return plot;
214254
}
215255

256+
/** Draws one dashed, labeled {@link ValueMarker} per layer boundary onto {@code plot} -
257+
* factored out so the "Show layer annotations" checkbox can call it again after {@code
258+
* plot.clearRangeMarkers()} without rebuilding the whole chart. */
259+
private static void addLayerBoundaryMarkers( XYPlot plot, List<LayerBoundary> layerBoundaries ) {
260+
for( LayerBoundary boundary : layerBoundaries ) {
261+
ValueMarker marker = new ValueMarker(boundary.topEta);
262+
marker.setPaint(Color.DARK_GRAY);
263+
marker.setStroke(
264+
new BasicStroke(1f, BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER, 1f, new float[]{4f, 4f}, 0f));
265+
marker.setLabel(boundary.label);
266+
marker.setLabelFont(marker.getLabelFont().deriveFont(Font.PLAIN, 9f));
267+
marker.setLabelPaint(Color.DARK_GRAY);
268+
marker.setLabelAnchor(RectangleAnchor.TOP_LEFT);
269+
marker.setLabelTextAnchor(TextAnchor.BOTTOM_LEFT);
270+
marker.setLabelOffset(new RectangleInsets(2, 4, 2, 4));
271+
plot.addRangeMarker(marker);
272+
}
273+
}
274+
275+
/**
276+
* One value per internal transition between parameter sets in {@link
277+
* WhetgeoStateChartData#gridEta}, each labeled with the SWRC parameters of the
278+
* layer above that boundary - empty if the output wasn't written with {@code
279+
* parameter_id}/{@code output_swrc_parameters} (see {@code
280+
* Whetgeo1DOutputsHandler}), so older outputs just render without annotations.
281+
*
282+
* <p>
283+
* {@code gridEta} holds cell *centers*, not layer edges, so the boundary itself
284+
* is the midpoint between the last cell of the lower layer and the first cell
285+
* of the layer above it - not either cell's own eta - otherwise the drawn line
286+
* lands half a cell-thickness away from the real boundary (e.g. -1.01 instead
287+
* of the true -1.00 for two 0.02 m-thick layers).
288+
*/
289+
private static List<LayerBoundary> computeLayerBoundaries( WhetgeoStateChartData data ) {
290+
List<LayerBoundary> boundaries = new ArrayList<>();
291+
if (data.gridParameterID.length != data.gridEta.length || data.swrcParameters.isEmpty()) {
292+
return boundaries;
293+
}
294+
Map<Integer, SwrcParams> byId = new HashMap<>();
295+
for( SwrcParams p : data.swrcParameters ) {
296+
byId.put(p.id, p);
297+
}
298+
299+
// gridEta is ascending; a boundary exists wherever parameterID changes
300+
// between two consecutive cells
301+
for( int i = 1; i < data.gridEta.length; i++ ) {
302+
int lowerID = data.gridParameterID[i - 1];
303+
int upperID = data.gridParameterID[i];
304+
if (lowerID == upperID) {
305+
continue;
306+
}
307+
SwrcParams p = byId.get(upperID);
308+
if (p == null) {
309+
continue;
310+
}
311+
double boundaryEta = (data.gridEta[i - 1] + data.gridEta[i]) / 2.0;
312+
String label = String.format("θS=%.3f θR=%.3f Ks=%.2e", p.thetaS, p.thetaR, p.ks);
313+
boundaries.add(new LayerBoundary(boundaryEta, label));
314+
}
315+
return boundaries;
316+
}
317+
318+
private static class LayerBoundary {
319+
final double topEta;
320+
final String label;
321+
322+
LayerBoundary( double topEta, String label ) {
323+
this.topEta = topEta;
324+
this.label = label;
325+
}
326+
}
327+
216328
/**
217329
* Forces the same reserved width and the same tick number format on every sub-plot's range
218330
* axis - see the class javadoc for why that's needed for the shared time axis to actually
@@ -257,6 +369,14 @@ private static String formatConstant( double value ) {
257369
return String.valueOf(value);
258370
}
259371

372+
/** Appends the BC type (e.g. "TOP_COUPLED") to the label if the output was written with
373+
* one (see {@code Whetgeo1DOutputsHandler.TABLE_OUTPUT_METADATA}); a plain fallback
374+
* label otherwise, so a value alone doesn't have to stand in for what kind of condition
375+
* produced it. */
376+
private static String bcLabel( String base, String bcType ) {
377+
return bcType == null ? base : base + " (" + bcType + ")";
378+
}
379+
260380
private static double[] valueBounds( double[] values ) {
261381
double lowerBound = Arrays.stream(values).min().orElse(0);
262382
double upperBound = Arrays.stream(values).max().orElse(1);

0 commit comments

Comments
 (0)