Skip to content

Commit fd73f58

Browse files
committed
feat(vis): add Cesium World Terrain integration with clamping modes for exported features
1 parent 93a2dbd commit fd73f58

15 files changed

Lines changed: 1320 additions & 20 deletions

File tree

citydb-cli/src/main/java/org/citydb/cli/visExporter/VisExportController.java

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
import org.citydb.query.executor.QueryResult;
3838
import org.citydb.query.filter.encoding.FilterParseException;
3939
import org.citydb.vis.VisExportException;
40+
import org.citydb.vis.config.ClampMode;
4041
import org.citydb.vis.config.VisFormatOptions;
4142
import org.citydb.vis.geometry.ImplicitReferencePointReprojector;
4243
import picocli.CommandLine;
@@ -162,7 +163,7 @@ private void applySceneOptions(VisFormatOptions options, SchemaMapping schemaMap
162163
}
163164

164165
if (Command.hasMatchedOption("--clamp-to-ground", commandSpec)) {
165-
options.setClampToGround(sceneOptions.isClampToGround());
166+
options.setClampMode(sceneOptions.getClampMode());
166167
}
167168

168169
if (Command.hasMatchedOption("--texture-scale", commandSpec)) {
@@ -201,6 +202,26 @@ private void applySceneOptions(VisFormatOptions options, SchemaMapping schemaMap
201202
}
202203
}
203204

205+
// Resolve the ion token with the same precedence as the database
206+
// password: CLI flag > config file > CESIUM_ION_TOKEN env var. This runs
207+
// outside the sceneOptions guard above because the clamp mode (and the
208+
// config token) can come from the config file alone, with no scene CLI
209+
// flags present — in which case sceneOptions is null. The config token,
210+
// if any, is already deserialized into options; the CLI flag overrides
211+
// it, and the env var fills in when neither CLI nor config supplied one.
212+
if (options.getClampMode() == ClampMode.CESIUM_WORLD_TERRAIN) {
213+
if (sceneOptions != null && Command.hasMatchedOption("--cesium-ion-token", commandSpec)) {
214+
options.setCesiumIonToken(sceneOptions.getCesiumIonToken());
215+
} else if (options.getCesiumIonToken() == null || options.getCesiumIonToken().isBlank()) {
216+
options.setCesiumIonToken(System.getenv("CESIUM_ION_TOKEN"));
217+
}
218+
if (options.getCesiumIonToken() == null || options.getCesiumIonToken().isBlank()) {
219+
throw new ExecutionException("Error: --clamp-to-ground=cesium-world-terrain " +
220+
"requires a Cesium ion token via --cesium-ion-token, the config file, " +
221+
"or the CESIUM_ION_TOKEN environment variable.");
222+
}
223+
}
224+
204225
// Build the derived runtime objects from the final string inputs
205226
// (whatever their source). The library exceptions name the offending
206227
// concept but not the CLI flag; we add the flag attribution here when

citydb-cli/src/main/java/org/citydb/cli/visExporter/options/SceneOptions.java

Lines changed: 63 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,15 @@
88
import org.citydb.cli.common.Option;
99
import org.citydb.vis.appearance.AtlasFallbackStrategy;
1010
import org.citydb.vis.appearance.AtlasOverflowMode;
11+
import org.citydb.vis.config.ClampMode;
1112
import org.citydb.vis.styling.DefaultObjectStyle;
1213
import picocli.CommandLine;
1314

15+
import java.util.Arrays;
16+
import java.util.Iterator;
1417
import java.util.List;
1518
import java.util.Map;
19+
import java.util.stream.Collectors;
1620

1721
public class SceneOptions implements Option {
1822
@CommandLine.Option(names = "--grid-edge-length", paramLabel = "<meters>",
@@ -34,10 +38,28 @@ public class SceneOptions implements Option {
3438
"on city-scale datasets and is intended for small exports or debugging.")
3539
private double screenPixelThreshold;
3640

37-
@CommandLine.Option(names = "--clamp-to-ground",
38-
description = "Place each building on the ellipsoid surface (height 0). " +
39-
"Useful when no terrain is loaded in the viewer.")
40-
private boolean clampToGround;
41+
@CommandLine.Option(names = "--clamp-to-ground", paramLabel = "<mode>",
42+
converter = ClampModeConverter.class,
43+
completionCandidates = ClampModeCandidates.class,
44+
description = "Vertically clamp each feature before tiling: " +
45+
"${COMPLETION-CANDIDATES}. 'ellipsoid' shifts each feature so " +
46+
"its lowest point sits on the WGS84 ellipsoid (height 0) — " +
47+
"useful when no terrain is loaded in the viewer. " +
48+
"'cesium-world-terrain' samples the Cesium World Terrain height " +
49+
"at each feature's centroid at export time (requires " +
50+
"--cesium-ion-token) and bakes that as the ground height, so the " +
51+
"export lines up with Cesium World Terrain in the viewer even " +
52+
"when the source heights are unreliable or relative. When omitted, " +
53+
"features keep their absolute database height (no clamping).")
54+
private ClampMode clampMode;
55+
56+
@CommandLine.Option(names = "--cesium-ion-token", paramLabel = "<token>",
57+
description = "Cesium ion access token used to fetch Cesium World Terrain for " +
58+
"--clamp-to-ground=cesium-world-terrain. May also be set in the config " +
59+
"file or via the CESIUM_ION_TOKEN environment variable; precedence is " +
60+
"command line > config file > environment variable. Like the database " +
61+
"password, a token set in the config file is stored there in plain text.")
62+
private String cesiumIonToken;
4163

4264
@CommandLine.Option(names = "--texture-scale", paramLabel = "<factor>",
4365
defaultValue = "1.0",
@@ -172,8 +194,12 @@ public double getScreenPixelThreshold() {
172194
return screenPixelThreshold;
173195
}
174196

175-
public boolean isClampToGround() {
176-
return clampToGround;
197+
public ClampMode getClampMode() {
198+
return clampMode;
199+
}
200+
201+
public String getCesiumIonToken() {
202+
return cesiumIonToken;
177203
}
178204

179205
public double getTextureScale() {
@@ -276,5 +302,36 @@ public void preprocess(CommandLine commandLine) {
276302
}
277303
}
278304
}
305+
306+
// The presence check for the Cesium ion token lives in the controller,
307+
// not here: the token may come from the config file (CLI flag > config >
308+
// CESIUM_ION_TOKEN), and validate() runs at parse time before the config
309+
// is merged, so it cannot see a config-supplied token.
310+
}
311+
312+
/**
313+
* Maps the kebab-case CLI spelling ({@code ellipsoid},
314+
* {@code cesium-world-terrain}) onto {@link ClampMode}; picocli's default
315+
* enum matching only accepts the constant names, which carry an underscore.
316+
*/
317+
static class ClampModeConverter implements CommandLine.ITypeConverter<ClampMode> {
318+
@Override
319+
public ClampMode convert(String value) {
320+
return ClampMode.fromValue(value);
321+
}
322+
}
323+
324+
/**
325+
* Supplies the kebab-case values for {@code ${COMPLETION-CANDIDATES}} and
326+
* shell completion, instead of the enum constant names.
327+
*/
328+
static class ClampModeCandidates implements Iterable<String> {
329+
@Override
330+
public Iterator<String> iterator() {
331+
return Arrays.stream(ClampMode.values())
332+
.map(ClampMode::getValue)
333+
.collect(Collectors.toList())
334+
.iterator();
335+
}
279336
}
280337
}

citydb-vis/src/main/java/module-info.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
requires org.slf4j;
66
requires texture.atlas.creator;
77
requires java.desktop;
8+
requires java.net.http;
89

910
exports org.citydb.vis;
1011
exports org.citydb.vis.appearance;
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
/*
2+
* SPDX-License-Identifier: Apache-2.0
3+
* Copyright Stuttgart University of Applied Sciences (HFT Stuttgart) <https://www.hft-stuttgart.de>
4+
*/
5+
6+
package org.citydb.vis.config;
7+
8+
import com.alibaba.fastjson2.JSONReader;
9+
import com.alibaba.fastjson2.JSONWriter;
10+
import com.alibaba.fastjson2.reader.ObjectReader;
11+
import com.alibaba.fastjson2.writer.ObjectWriter;
12+
13+
import java.lang.reflect.Type;
14+
15+
/**
16+
* How the exporter places each feature vertically before tiling.
17+
* <p>
18+
* The default (no {@code --clamp-to-ground}) is <em>no clamping</em>: features
19+
* keep the absolute height that comes out of the database. This enum only
20+
* describes the two <em>active</em> clamp targets; the "do nothing" state is
21+
* modelled by a {@code null} {@code ClampMode} on the format options, not by a
22+
* constant here.
23+
* <ul>
24+
* <li>{@link #ELLIPSOID} — shift each mesh so its lowest vertex sits on the
25+
* WGS84 ellipsoid (height 0). Useful when the viewer has no terrain
26+
* loaded. Purely local, no network access.</li>
27+
* <li>{@link #CESIUM_WORLD_TERRAIN} — sample the Cesium World Terrain height
28+
* at the feature's centroid (via a Cesium ion token) at export time and
29+
* shift each mesh so its lowest vertex sits on that terrain height. The
30+
* baked offsets make the export line up with Cesium World Terrain in the
31+
* viewer even when the source heights are unreliable or relative.</li>
32+
* </ul>
33+
*/
34+
public enum ClampMode {
35+
ELLIPSOID("ellipsoid"),
36+
CESIUM_WORLD_TERRAIN("cesium-world-terrain");
37+
38+
private final String value;
39+
40+
ClampMode(String value) {
41+
this.value = value;
42+
}
43+
44+
/**
45+
* The CLI / config spelling of this mode (kebab-case), e.g.
46+
* {@code cesium-world-terrain}.
47+
*/
48+
public String getValue() {
49+
return value;
50+
}
51+
52+
/**
53+
* Resolve a CLI / config token (case-insensitive, kebab-case) to a mode.
54+
*
55+
* @throws IllegalArgumentException if the token matches no mode
56+
*/
57+
public static ClampMode fromValue(String value) {
58+
if (value != null) {
59+
for (ClampMode mode : values()) {
60+
if (mode.value.equalsIgnoreCase(value.trim())) {
61+
return mode;
62+
}
63+
}
64+
}
65+
throw new IllegalArgumentException("unknown clamp mode '" + value +
66+
"' (expected one of: ellipsoid, cesium-world-terrain)");
67+
}
68+
69+
@Override
70+
public String toString() {
71+
return value;
72+
}
73+
74+
/**
75+
* fastjson2 serializer that writes a {@code ClampMode} as its kebab-case
76+
* {@link #getValue() value} (e.g. {@code "cesium-world-terrain"}) instead of
77+
* the enum constant name, so the JSON config spelling matches the CLI
78+
* {@code --clamp-to-ground} spelling exactly. Wired up via
79+
* {@code @JSONField(serializeUsing = ...)} on the format-options field.
80+
*/
81+
public static class JsonWriter implements ObjectWriter<ClampMode> {
82+
@Override
83+
public void write(JSONWriter jsonWriter, Object object, Object fieldName,
84+
Type fieldType, long features) {
85+
if (object == null) {
86+
jsonWriter.writeNull();
87+
} else {
88+
jsonWriter.writeString(((ClampMode) object).value);
89+
}
90+
}
91+
}
92+
93+
/**
94+
* fastjson2 deserializer that reads a {@code ClampMode} from its kebab-case
95+
* {@link #getValue() value}, the counterpart of {@link JsonWriter}. Wired up
96+
* via {@code @JSONField(deserializeUsing = ...)} on the format-options field.
97+
*/
98+
public static class JsonReader implements ObjectReader<ClampMode> {
99+
@Override
100+
public ClampMode readObject(JSONReader jsonReader, Type fieldType,
101+
Object fieldName, long features) {
102+
String value = jsonReader.readString();
103+
return value != null ? fromValue(value) : null;
104+
}
105+
}
106+
}

citydb-vis/src/main/java/org/citydb/vis/config/VisFormatOptions.java

Lines changed: 38 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,13 @@ public abstract class VisFormatOptions implements OutputFormatOptions {
3535
// small exports / debugging but easily crashes the viewer on large
3636
// datasets, so users must opt in explicitly with --screen-pixel-threshold=0.
3737
private double screenPixelThreshold = 56.0;
38-
private boolean clampToGround;
38+
// null = no vertical clamping (features keep their absolute database height).
39+
// ELLIPSOID / CESIUM_WORLD_TERRAIN select an active clamp target. Serialized
40+
// by its kebab-case value ("ellipsoid" / "cesium-world-terrain") so the JSON
41+
// config spelling matches the CLI --clamp-to-ground spelling exactly, rather
42+
// than fastjson2's default enum-constant-name form (CESIUM_WORLD_TERRAIN).
43+
@JSONField(serializeUsing = ClampMode.JsonWriter.class, deserializeUsing = ClampMode.JsonReader.class)
44+
private ClampMode clampMode;
3945
private double textureScale = 1.0;
4046
private int maxAtlasSize = 1024;
4147
private AtlasOverflowMode atlasOverflowMode = AtlasOverflowMode.HYBRID;
@@ -59,6 +65,12 @@ public abstract class VisFormatOptions implements OutputFormatOptions {
5965
@JSONField(serialize = false, deserialize = false)
6066
private AttributeProjection attributeProjection;
6167

68+
// Cesium ion access token for Cesium World Terrain clamping. Stored in
69+
// plaintext in the config file, matching how the database password is
70+
// handled (org.citydb.database.connection.ConnectionDetails#password) — the
71+
// CLI controller merges it with precedence CLI flag > config > CESIUM_ION_TOKEN.
72+
private String cesiumIonToken;
73+
6274
public double getGridEdgeLength() {
6375
return gridEdgeLength;
6476
}
@@ -77,12 +89,33 @@ public VisFormatOptions setScreenPixelThreshold(double screenPixelThreshold) {
7789
return this;
7890
}
7991

80-
public boolean isClampToGround() {
81-
return clampToGround;
92+
/**
93+
* Active vertical clamp target, or {@code null} for no clamping (features
94+
* keep their absolute database height — the default).
95+
*/
96+
public ClampMode getClampMode() {
97+
return clampMode;
98+
}
99+
100+
public VisFormatOptions setClampMode(ClampMode clampMode) {
101+
this.clampMode = clampMode;
102+
return this;
103+
}
104+
105+
/**
106+
* Cesium ion access token used to fetch Cesium World Terrain when
107+
* {@link #getClampMode()} is {@link ClampMode#CESIUM_WORLD_TERRAIN}. Stored
108+
* in plaintext in the config file (like the database password); the CLI
109+
* controller resolves the effective value with precedence
110+
* {@code --cesium-ion-token} > config > {@code CESIUM_ION_TOKEN}.
111+
* {@code null}/blank when not supplied.
112+
*/
113+
public String getCesiumIonToken() {
114+
return cesiumIonToken;
82115
}
83116

84-
public VisFormatOptions setClampToGround(boolean clampToGround) {
85-
this.clampToGround = clampToGround;
117+
public VisFormatOptions setCesiumIonToken(String cesiumIonToken) {
118+
this.cesiumIonToken = cesiumIonToken;
86119
return this;
87120
}
88121

citydb-vis/src/main/java/org/citydb/vis/geometry/TriangleMesh.java

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -542,19 +542,23 @@ public double[] computeCenter() {
542542
}
543543

544544
/**
545-
* Shift all vertex Z values so the mesh's bottom sits at height 0.
545+
* Shift all vertex Z values so the mesh's lowest point sits at
546+
* {@code groundHeight} (metres above the WGS84 ellipsoid). Pass {@code 0}
547+
* to place the mesh on the ellipsoid surface; pass a sampled terrain height
548+
* to place it on terrain.
546549
*/
547-
public void clampToGround() {
550+
public void clampToGround(double groundHeight) {
548551
if (positions.isEmpty()) {
549552
return;
550553
}
551554
double minZ = Double.MAX_VALUE;
552555
for (double[] pos : positions) {
553556
if (pos[2] < minZ) minZ = pos[2];
554557
}
555-
if (minZ != 0) {
558+
double shift = groundHeight - minZ;
559+
if (shift != 0) {
556560
for (double[] pos : positions) {
557-
pos[2] -= minZ;
561+
pos[2] += shift;
558562
}
559563
}
560564
}

0 commit comments

Comments
 (0)