java/com.sap.sailing.polars.test/src/com/sap/sailing/polars/jaxrs/api/test/PolarDataResourceTest.java
... ...
@@ -58,7 +58,6 @@ public class PolarDataResourceTest {
58 58
BoatClass boatClass = domainFactory.getOrCreateBoatClass(BOAT_CLASS);
59 59
PolynomialFunction angleDownwindFunction = new PolynomialFunction(ANGLE_FUNCTION_COEFFS_DOWNWIND);
60 60
PolynomialFunction speedDownwindFunction = new PolynomialFunction(SPEED_FUNCTIONS_COEFFS_DOWNWIND);
61
-
62 61
assertThat(polarService.getSpeedRegressionsPerAngle().size(), is(68));
63 62
assertThat(polarService.getCubicRegressionsPerCourse().size(), is(4));
64 63
assertThat(polarService.getFixCountPerBoatClass().get(boatClass), is(9330L));
... ...
@@ -67,5 +66,4 @@ public class PolarDataResourceTest {
67 66
assertThat(polarService.getAngleRegressionFunction(boatClass, LegType.DOWNWIND), is(angleDownwindFunction));
68 67
assertThat(polarService.getSpeedRegressionFunction(boatClass, LegType.DOWNWIND), is(speedDownwindFunction));
69 68
}
70
-
71 69
}
... ...
\ No newline at end of file
java/com.sap.sailing.selenium.test/pom.xml
... ...
@@ -17,7 +17,7 @@
17 17
<!-- Note: The actual value for "parameters.integration-tests" and "parameters.proxy" should be defined in the local
18 18
settings.xml, because of the differences between the CI-Build and the local setup. -->
19 19
<tycho.tests.argLine>
20
- ${parameters.integration-tests} ${parameters.proxy} ${parameters.jetty} -Xmx4096m
20
+ ${parameters.integration-tests} ${parameters.proxy} ${parameters.jetty} -Xmx4096m -Digtimi.base.url=http://127.0.0.1:8885
21 21
</tycho.tests.argLine>
22 22
</properties>
23 23
java/com.sap.sailing.server.gateway/src/com/sap/sailing/server/gateway/impl/rc/RaceGroupJsonExportServlet.java
... ...
@@ -79,7 +79,6 @@ public class RaceGroupJsonExportServlet extends AbstractJsonHttpServlet {
79 79
.checkPermission(leaderboard.getIdentifier().getStringPermission(DefaultActions.READ));
80 80
if (leaderboard instanceof RegattaLeaderboard && !(leaderboard instanceof RegattaLeaderboardWithEliminations)) {
81 81
result.add(serializer.serialize(raceGroupFactory.convert((RegattaLeaderboard) leaderboard)));
82
-
83 82
final Regatta regatta = ((RegattaLeaderboard) leaderboard).getRegatta();
84 83
SecurityUtils.getSubject()
85 84
.checkPermission(regatta.getIdentifier().getStringPermission(DefaultActions.READ));
java/com.sap.sailing.server/SailingServer (No Proxy, winddbTest Axel).launch
... ...
@@ -27,355 +27,349 @@
27 27
<stringAttribute key="profilingTraceType-PERFORMANCE_HOTSPOT_TRACE" value="KEY_SESSION_FILTER%CTX_KEY%*%CTX_ENTRY%KEY_USER_FILTER%CTX_KEY%*%CTX_ENTRY%KEY_REQUEST_FILTER%CTX_KEY%*%CTX_ENTRY%KEY_IGNORE_SLEEPING_THREADS%CTX_KEY%true%CTX_ENTRY%KEY_APPLICATION_FILTER%CTX_KEY%*%CTX_ENTRY%KEY_ENABLEMENT%CTX_KEY%false%CTX_ENTRY%"/>
28 28
<stringAttribute key="profilingTraceType-SYNCHRONIZATION_TRACE" value="KEY_SESSION_FILTER%CTX_KEY%*%CTX_ENTRY%KEY_USER_FILTER%CTX_KEY%*%CTX_ENTRY%KEY_REQUEST_FILTER%CTX_KEY%*%CTX_ENTRY%KEY_APPLICATION_FILTER%CTX_KEY%*%CTX_ENTRY%KEY_ENABLEMENT%CTX_KEY%false%CTX_ENTRY%"/>
29 29
<setAttribute key="selected_target_bundles">
30
-<setEntry value="routeconverter@default:default"/>
31
-<setEntry value="org.apache.commons.codec@default:default"/>
32
-<setEntry value="org.apache.poi@default:default"/>
33
-<setEntry value="org.apache.poi.ooxml@default:default"/>
34
-<setEntry value="org.apache.poi.ooxml.schemas@default:default"/>
35
-<setEntry value="org.apache.commons.collections4@default:default"/>
36
-<setEntry value="org.apache.commons.compress@default:default"/>
37
-<setEntry value="org.apache.poi.source@default:default"/>
38
-<setEntry value="org.apache.poi.ooxml.source@default:default"/>
39
-<setEntry value="org.apache.commons.collections4.source@default:default"/>
40
-<setEntry value="org.apache.commons.compress.source@default:default"/>
41
-<setEntry value="org.dom4j@default:default"/>
42
-<setEntry value="org.apache.xmlbeans@default:default"/>
43
-<setEntry value="org.apache.commons.math@default:default"/>
44
-<setEntry value="org.apache.httpcomponents.httpclient@default:default"/>
45
-<setEntry value="org.apache.httpcomponents.httpcore@default:default"/>
46
-<setEntry value="org.hyperic.sigar@default:default"/>
47
-<setEntry value="com.sun.jersey.contribs.jersey-multipart@default:default"/>
48
-<setEntry value="javax.validation@default:default"/>
49
-<setEntry value="org.apache.commons.fileupload@default:default"/>
50
-<setEntry value="org.jvnet.mimepull@default:default"/>
51
-<setEntry value="org.apache.commons.math3@default:default"/>
52
-<setEntry value="org.mongodb.driver-core@default:default"/>
53
-<setEntry value="org.mongodb.driver-core.source@default:default"/>
54
-<setEntry value="org.mongodb.bson@default:default"/>
55
-<setEntry value="org.mongodb.bson.source@default:default"/>
56
-<setEntry value="org.mongodb.driver-sync@default:default"/>
57
-<setEntry value="org.mongodb.driver-sync@default:default"/>
58
-<setEntry value="org.eclipse.jetty.osgi.boot@3:true"/>
59
-<setEntry value="org.eclipse.jetty.osgi.boot.warurl@default:default"/>
60
-<setEntry value="org.hyperic.sigar@default:default"/>
61
-<setEntry value="slf4j.jdk14@default:default"/>
62
-<setEntry value="lz4-java@default:default"/>
63
-<setEntry value="org.apache.felix.gogo.command@default:default"/>
64
-<setEntry value="org.apache.felix.gogo.runtime@default:default"/>
65
-<setEntry value="org.apache.felix.gogo.shell@default:default"/>
66
-<setEntry value="org.eclipse.jetty.deploy@default:default"/>
67
-<setEntry value="org.eclipse.jetty.deploy.source@default:default"/>
68
-<setEntry value="org.eclipse.jetty.http@3:true"/>
69
-<setEntry value="org.eclipse.jetty.http.source@default:default"/>
70
-<setEntry value="org.eclipse.jetty.io@default:default"/>
71
-<setEntry value="org.eclipse.jetty.io.source@default:default"/>
72
-<setEntry value="org.eclipse.jetty.jmx@default:default"/>
73
-<setEntry value="org.eclipse.jetty.jmx.source@default:default"/>
74
-<setEntry value="org.eclipse.jetty.security@default:default"/>
75
-<setEntry value="org.eclipse.jetty.security.source@default:default"/>
76
-<setEntry value="org.eclipse.jetty.server@default:default"/>
77
-<setEntry value="org.eclipse.jetty.server.source@default:default"/>
78
-<setEntry value="org.eclipse.jetty.servlet@default:default"/>
79
-<setEntry value="org.eclipse.jetty.servlet.source@default:default"/>
80
-<setEntry value="org.eclipse.jetty.util@default:default"/>
81
-<setEntry value="org.eclipse.jetty.util.source@default:default"/>
82
-<setEntry value="org.eclipse.jetty.util.ajax@default:default"/>
83
-<setEntry value="org.eclipse.jetty.util.ajax.source@default:default"/>
84
-<setEntry value="org.eclipse.jetty.webapp@default:default"/>
85
-<setEntry value="org.eclipse.jetty.webapp.source@default:default"/>
86
-<setEntry value="org.eclipse.jetty.websocket.api@default:default"/>
87
-<setEntry value="org.eclipse.jetty.websocket.api.source@default:default"/>
88
-<setEntry value="org.eclipse.jetty.websocket.client@default:default"/>
89
-<setEntry value="org.eclipse.jetty.websocket.client.source@default:default"/>
90
-<setEntry value="org.eclipse.jetty.websocket.common@default:default"/>
91
-<setEntry value="org.eclipse.jetty.websocket.common.source@default:default"/>
92
-<setEntry value="org.eclipse.jetty.websocket.server@default:default"/>
93
-<setEntry value="org.eclipse.jetty.websocket.server.source@default:default"/>
94
-<setEntry value="org.eclipse.jetty.websocket.servlet@default:default"/>
95
-<setEntry value="org.eclipse.jetty.websocket.servlet.source@default:default"/>
96
-<setEntry value="org.eclipse.jetty.xml@default:default"/>
97
-<setEntry value="org.eclipse.jetty.xml.source@default:default"/>
98
-<setEntry value="slf4j.api@default:default"/>
99
-<setEntry value="org.apache.servicemix.bundles.zxing@default:default"/>
100
-<setEntry value="org.apache.commons.io@default:default"/>
101
-<setEntry value="jcl.over.slf4j@default:default"/>
102
-<setEntry value="jul.to.slf4j@default:default"/>
103
-<setEntry value="com.sun.mail.javax.mail@default:default"/>
104
-<setEntry value="com.rabbitmq.client@default:default"/>
105
-<setEntry value="com.rabbitmq.client.source@default:default"/>
106
-<setEntry value="org.apache.commons.lang@default:default"/>
107
-<setEntry value="org.apache.commons.logging@default:default"/>
108
-<setEntry value="jackson-jaxrs@default:default"/>
109
-<setEntry value="com.sun.jersey@default:default"/>
110
-<setEntry value="javax.ws.rs@default:default"/>
111
-<setEntry value="org.apache.commons.beanutils@default:default"/>
112
-<setEntry value="org.apache.commons.beanutils.source@default:default"/>
113
-<setEntry value="org.apache.servicemix.bundles.ehcache@default:default"/>
114
-<setEntry value="org.apache.servicemix.bundles.scribe@default:default"/>
115
-<setEntry value="org.owasp.encoder@default:default"/>
116
-<setEntry value="org.owasp.encoder.source@default:default"/>
117
-<setEntry value="org.apache.shiro.core@default:default"/>
118
-<setEntry value="org.apache.shiro.core.source@default:default"/>
119
-<setEntry value="org.apache.shiro.ehcache@default:default"/>
120
-<setEntry value="org.apache.shiro.ehcache.source@default:default"/>
121
-<setEntry value="org.apache.shiro.web@default:default"/>
122
-<setEntry value="org.apache.shiro.web.source@default:default"/>
123
-<setEntry value="jackson-core-asl@default:default"/>
124
-<setEntry value="jackson-mapper-asl@default:default"/>
125
-<setEntry value="com.fasterxml.jackson.core.jackson-core@default:default"/>
126
-<setEntry value="org.apache.commons.collections@default:default"/>
127
-<setEntry value="org.eclipse.jetty.client@default:default"/>
128
-<setEntry value="javax.xml@default:default"/>
129
-<setEntry value="com.sun.activation.javax.activation@default:default"/>
130
-<setEntry value="org.eclipse.equinox.common@2:true"/>
131
-<setEntry value="org.eclipse.equinox.console@default:default"/>
132
-<setEntry value="org.eclipse.equinox.launcher@default:default"/>
133
-<setEntry value="org.eclipse.equinox.simpleconfigurator@2:true"/>
134
-<setEntry value="org.eclipse.osgi@-1:true"/>
135
-<setEntry value="org.eclipse.osgi.source@default:default"/>
136
-<setEntry value="org.osgi.util.promise@default:default"/>
137
-<setEntry value="org.osgi.util.promise.source@default:default"/>
138
-<setEntry value="org.osgi.util.function@default:default"/>
139
-<setEntry value="org.osgi.util.function.source@default:default"/>
140
-<setEntry value="org.osgi.util.measurement@default:default"/>
141
-<setEntry value="org.osgi.util.measurement.source@default:default"/>
142
-<setEntry value="org.osgi.util.position@default:default"/>
143
-<setEntry value="org.osgi.util.position.source@default:default"/>
144
-<setEntry value="org.osgi.util.xml@default:default"/>
145
-<setEntry value="org.osgi.util.xml.source@default:default"/>
146
-<setEntry value="org.eclipse.osgi.services@default:default"/>
147
-<setEntry value="org.eclipse.osgi.services.source@default:default"/>
148
-<setEntry value="org.eclipse.equinox.cm@default:default"/>
149
-<setEntry value="com.sun.istack.commons-runtime@default:default"/>
150
-<setEntry value="com.sun.xml.bind.jaxb-impl@default:default"/>
151
-<setEntry value="javax.xml.stream@default:default"/>
152
-<setEntry value="javax.xml.ws@default:default"/>
153
-<setEntry value="javax.xml.soap@default:default"/>
154
-<setEntry value="org.eclipse.osgi.util@default:default"/>
155
-<setEntry value="org.eclipse.osgi.util.source@default:default"/>
156
-<setEntry value="com.chargebee.chargebee-java@default:default"/>
157
-<setEntry value="com.github.mwiede.jsch@default:default"/>
158
-<setEntry value="com.github.mwiede.jsch.source@default:default"/>
159
-<setEntry value="bcprov-ext@default:default"/>
160
-<setEntry value="com.amazon.aws.aws-java-api@default:default"/>
161
-<setEntry value="com.amazon.aws.aws-java-api.source@default:default"/>
162
-<setEntry value="org.objectweb.asm@default:default"/>
163
-<setEntry value="org.objectweb.asm.commons@default:default"/>
164
-<setEntry value="org.objectweb.asm.tree.analysis@default:default"/>
165
-<setEntry value="org.objectweb.asm.tree.analysis.source@default:default"/>
166
-<setEntry value="org.objectweb.asm.tree@default:default"/>
167
-<setEntry value="org.objectweb.asm.tree.source@default:default"/>
168
-<setEntry value="org.objectweb.asm.tree@default:default"/>
169
-<setEntry value="org.objectweb.asm.util@default:default"/>
170
-<setEntry value="org.objectweb.asm.source@default:default"/>
171
-<setEntry value="org.objectweb.asm.commons.source@default:default"/>
172
-<setEntry value="org.objectweb.asm.tree.source@default:default"/>
173
-<setEntry value="org.objectweb.asm.util.source@default:default"/>
174
-<setEntry value="org.eclipse.jetty.apache-jsp@4:true"/>
175
-<setEntry value="org.eclipse.jetty.apache-jsp.source@default:default"/>
176
-<setEntry value="org.eclipse.jetty.osgi.boot.jsp@default:default"/>
177
-<setEntry value="org.eclipse.jetty.osgi.boot.jsp.source@default:default"/>
178
-<setEntry value="org.apache.geronimo.specs.geronimo-jta_1.1_spec@default:default"/>
179
-<setEntry value="org.apache.aries.spifly.dynamic.bundle@3:true"/>
180
-<setEntry value="org.apache.aries.spifly.dynamic.bundle.source@default:default"/>
181
-<setEntry value="org.mortbay.jasper.apache-el@default:default"/>
182
-<setEntry value="org.mortbay.jasper.apache-jsp@default:default"/>
183
-<setEntry value="org.mortbay.jasper.apache-el.source@default:default"/>
184
-<setEntry value="org.mortbay.jasper.apache-jsp.source@default:default"/>
185
-<setEntry value="org.eclipse.jdt.core.compiler.batch@default:default"/>
186
-<setEntry value="org.apache.taglibs.standard-impl@default:default"/>
187
-<setEntry value="org.apache.taglibs.taglibs-standard-spec@default:default"/>
188
-<setEntry value="javax.annotation@default:default"/>
189
-<setEntry value="org.eclipse.jetty.osgi-servlet-api@default:default"/>
190
-<setEntry value="org.apache.xalan@default:default"/>
191
-<setEntry value="org.apache.xml.serializer@default:default"/>
192
-<setEntry value="org.eclipse.equinox.event@default:default"/>
193
-<setEntry value="org.eclipse.jetty.annotations@4:true"/>
194
-<setEntry value="org.eclipse.jetty.jndi@default:default"/>
195
-<setEntry value="org.eclipse.jetty.plus@default:default"/>
196
-<setEntry value="com.sap.db.jdbc@default:default"/>
197
-<setEntry value="com.sap.db.jdbc.source@default:default"/>
198
-<setEntry value="org.osgi.service.component@default:default"/>
199
-<setEntry value="org.osgi.service.cm@default:default"/>
200
-<setEntry value="org.osgi.util.tracker@default:default"/>
201
-<setEntry value="org.osgi.service.event@default:default"/>
202
-<setEntry value="org.osgi.service.packageadmin@default:default"/>
203
-<setEntry value="org.osgi.service.url@default:default"/>
204
-<setEntry value="redisson@default:default"/>
205
-<setEntry value="com.esotericsoftware.kryo@default:default"/>
206
-<setEntry value="com.esotericsoftware.reflectasm@default:default"/>
207
-<setEntry value="com.esotericsoftware.minlog@default:default"/>
208
-<setEntry value="com.fasterxml.jackson.core.jackson-annotations@default:default"/>
209
-<setEntry value="com.fasterxml.jackson.core.jackson-databind@default:default"/>
210
-<setEntry value="com.fasterxml.jackson.dataformat.jackson-dataformat-yaml@default:default"/>
211
-<setEntry value="org.yaml.snakeyaml@default:default"/>
212
-<setEntry value="io.netty.transport@default:default"/>
213
-<setEntry value="io.netty.buffer@default:default"/>
214
-<setEntry value="io.netty.common@default:default"/>
215
-<setEntry value="io.netty.resolver@default:default"/>
216
-<setEntry value="io.netty.transport-classes-epoll@default:default"/>
217
-<setEntry value="io.netty.transport-native-unix-common@default:default"/>
218
-<setEntry value="io.netty.transport-classes-kqueue@default:default"/>
219
-<setEntry value="io.netty.codec@default:default"/>
220
-<setEntry value="io.netty.handler@default:default"/>
221
-<setEntry value="io.netty.resolver-dns@default:default"/>
222
-<setEntry value="io.netty.codec-dns@default:default"/>
223
-<setEntry value="io.netty.incubator.netty-incubator-transport-classes-io_uring@default:default"/>
224
-<setEntry value="io.reactivex.rxjava3.rxjava@default:default"/>
225
-<setEntry value="reactive-streams@default:default"/>
226
-<setEntry value="javax.cache.api@default:default"/>
227
-<setEntry value="com.github.haifengl.smile-core@default:default"/>
228
-<setEntry value="com.github.haifengl.smile-core.source@default:default"/>
229
-<setEntry value="com.github.haifengl.smile-data@default:default"/>
230
-<setEntry value="com.github.haifengl.smile-data.source@default:default"/>
231
-<setEntry value="com.github.haifengl.smile-graph@default:default"/>
232
-<setEntry value="com.github.haifengl.smile-graph.source@default:default"/>
233
-<setEntry value="com.github.haifengl.smile-math@default:default"/>
234
-<setEntry value="com.github.haifengl.smile-math.source@default:default"/>
235
-<setEntry value="com.google.gson@default:default"/>
236
-<setEntry value="com.google.gson.source@default:default"/>
237
-<setEntry value="jodd-bean@default:default"/>
238
-<setEntry value="net.bytebuddy.byte-buddy@default:default"/>
239
-<setEntry value="net.bytebuddy.byte-buddy.source@default:default"/>
240
-<setEntry value="net.bytebuddy.byte-buddy-agent@default:default"/>
241
-<setEntry value="net.bytebuddy.byte-buddy-agent.source@default:default"/>
242
-<setEntry value="org.jboss.marshalling.jboss-marshalling-osgi@default:default"/>
243
-<setEntry value="io.projectreactor.reactor-core@default:default"/>
244
-<setEntry value="com.diffplug.osgi.extension.sun.misc@default:default"/>
245
-<setEntry value="com.google.protobuf@default:default"/>
246
-<setEntry value="com.google.protobuf.source@default:default"/>
247
-<setEntry value="com.sun.jna@default:default"/>
248
-<setEntry value="com.sun.jna.source@default:default"/>
249
-<setEntry value="com.sun.jna.platform@default:default"/>
250
-<setEntry value="com.sun.jna.platform.source@default:default"/>
251
-<setEntry value="com.nulab-inc.zxcvbn@default:default"/>
252
-<setEntry value="com.nulab-inc.zxcvbn.source@default:default"/>
253
-</setAttribute>
30
+ <setEntry value="bcprov-ext@default:default"/>
31
+ <setEntry value="com.amazon.aws.aws-java-api.source"/>
32
+ <setEntry value="com.amazon.aws.aws-java-api@default:default"/>
33
+ <setEntry value="com.chargebee.chargebee-java@default:default"/>
34
+ <setEntry value="com.diffplug.osgi.extension.sun.misc@default:false"/>
35
+ <setEntry value="com.esotericsoftware.kryo@default:default"/>
36
+ <setEntry value="com.esotericsoftware.minlog@default:default"/>
37
+ <setEntry value="com.esotericsoftware.reflectasm@default:default"/>
38
+ <setEntry value="com.fasterxml.jackson.core.jackson-annotations@default:default"/>
39
+ <setEntry value="com.fasterxml.jackson.core.jackson-core@default:default"/>
40
+ <setEntry value="com.fasterxml.jackson.core.jackson-databind@default:default"/>
41
+ <setEntry value="com.fasterxml.jackson.dataformat.jackson-dataformat-yaml@default:default"/>
42
+ <setEntry value="com.github.haifengl.smile-core.source"/>
43
+ <setEntry value="com.github.haifengl.smile-core@default:default"/>
44
+ <setEntry value="com.github.haifengl.smile-data.source"/>
45
+ <setEntry value="com.github.haifengl.smile-data@default:default"/>
46
+ <setEntry value="com.github.haifengl.smile-graph.source"/>
47
+ <setEntry value="com.github.haifengl.smile-graph@default:default"/>
48
+ <setEntry value="com.github.haifengl.smile-math.source"/>
49
+ <setEntry value="com.github.haifengl.smile-math@default:default"/>
50
+ <setEntry value="com.github.mwiede.jsch.source"/>
51
+ <setEntry value="com.github.mwiede.jsch@default:default"/>
52
+ <setEntry value="com.google.gson.source"/>
53
+ <setEntry value="com.google.gson@default:default"/>
54
+ <setEntry value="com.google.protobuf.source"/>
55
+ <setEntry value="com.google.protobuf@default:default"/>
56
+ <setEntry value="com.nulab-inc.zxcvbn.source"/>
57
+ <setEntry value="com.nulab-inc.zxcvbn@default:default"/>
58
+ <setEntry value="com.rabbitmq.client.source"/>
59
+ <setEntry value="com.rabbitmq.client@default:default"/>
60
+ <setEntry value="com.sap.db.jdbc.source"/>
61
+ <setEntry value="com.sap.db.jdbc@default:default"/>
62
+ <setEntry value="com.sun.activation.javax.activation@default:default"/>
63
+ <setEntry value="com.sun.istack.commons-runtime@default:default"/>
64
+ <setEntry value="com.sun.jersey.contribs.jersey-multipart@default:default"/>
65
+ <setEntry value="com.sun.jersey@default:default"/>
66
+ <setEntry value="com.sun.jna.platform.source"/>
67
+ <setEntry value="com.sun.jna.platform@default:default"/>
68
+ <setEntry value="com.sun.jna.source"/>
69
+ <setEntry value="com.sun.jna@default:default"/>
70
+ <setEntry value="com.sun.mail.javax.mail@default:default"/>
71
+ <setEntry value="com.sun.xml.bind.jaxb-impl@default:default"/>
72
+ <setEntry value="io.netty.buffer@default:default"/>
73
+ <setEntry value="io.netty.codec-dns@default:default"/>
74
+ <setEntry value="io.netty.codec@default:default"/>
75
+ <setEntry value="io.netty.common@default:default"/>
76
+ <setEntry value="io.netty.handler@default:default"/>
77
+ <setEntry value="io.netty.incubator.netty-incubator-transport-classes-io_uring@default:default"/>
78
+ <setEntry value="io.netty.resolver-dns@default:default"/>
79
+ <setEntry value="io.netty.resolver@default:default"/>
80
+ <setEntry value="io.netty.transport-classes-epoll@default:default"/>
81
+ <setEntry value="io.netty.transport-classes-kqueue@default:default"/>
82
+ <setEntry value="io.netty.transport-native-unix-common@default:default"/>
83
+ <setEntry value="io.netty.transport@default:default"/>
84
+ <setEntry value="io.projectreactor.reactor-core@default:default"/>
85
+ <setEntry value="io.reactivex.rxjava3.rxjava@default:default"/>
86
+ <setEntry value="jackson-core-asl@default:default"/>
87
+ <setEntry value="jackson-jaxrs@default:default"/>
88
+ <setEntry value="jackson-mapper-asl@default:default"/>
89
+ <setEntry value="javax.annotation@default:default"/>
90
+ <setEntry value="javax.cache.api@default:default"/>
91
+ <setEntry value="javax.validation@default:default"/>
92
+ <setEntry value="javax.ws.rs@default:default"/>
93
+ <setEntry value="javax.xml.soap@default:default"/>
94
+ <setEntry value="javax.xml.stream@default:default"/>
95
+ <setEntry value="javax.xml.ws@default:default"/>
96
+ <setEntry value="javax.xml@default:default"/>
97
+ <setEntry value="jcl.over.slf4j@default:default"/>
98
+ <setEntry value="jodd-bean@default:default"/>
99
+ <setEntry value="jul.to.slf4j@default:default"/>
100
+ <setEntry value="lz4-java@default:default"/>
101
+ <setEntry value="net.bytebuddy.byte-buddy-agent.source"/>
102
+ <setEntry value="net.bytebuddy.byte-buddy-agent@default:default"/>
103
+ <setEntry value="net.bytebuddy.byte-buddy.source"/>
104
+ <setEntry value="net.bytebuddy.byte-buddy@default:default"/>
105
+ <setEntry value="org.apache.aries.spifly.dynamic.bundle.source"/>
106
+ <setEntry value="org.apache.aries.spifly.dynamic.bundle@3:true"/>
107
+ <setEntry value="org.apache.commons.beanutils.source@default:default"/>
108
+ <setEntry value="org.apache.commons.beanutils@default:default"/>
109
+ <setEntry value="org.apache.commons.codec@default:default"/>
110
+ <setEntry value="org.apache.commons.collections4.source"/>
111
+ <setEntry value="org.apache.commons.collections4@default:default"/>
112
+ <setEntry value="org.apache.commons.collections@default:default"/>
113
+ <setEntry value="org.apache.commons.compress.source"/>
114
+ <setEntry value="org.apache.commons.compress@default:default"/>
115
+ <setEntry value="org.apache.commons.fileupload@default:default"/>
116
+ <setEntry value="org.apache.commons.io@default:default"/>
117
+ <setEntry value="org.apache.commons.lang@default:default"/>
118
+ <setEntry value="org.apache.commons.logging@default:default"/>
119
+ <setEntry value="org.apache.commons.math3@default:default"/>
120
+ <setEntry value="org.apache.commons.math@default:default"/>
121
+ <setEntry value="org.apache.felix.gogo.command@default:default"/>
122
+ <setEntry value="org.apache.felix.gogo.runtime@default:default"/>
123
+ <setEntry value="org.apache.felix.gogo.shell@default:default"/>
124
+ <setEntry value="org.apache.geronimo.specs.geronimo-jta_1.1_spec@default:default"/>
125
+ <setEntry value="org.apache.httpcomponents.httpclient@default:default"/>
126
+ <setEntry value="org.apache.httpcomponents.httpcore@default:default"/>
127
+ <setEntry value="org.apache.poi.ooxml.schemas@default:default"/>
128
+ <setEntry value="org.apache.poi.ooxml.source"/>
129
+ <setEntry value="org.apache.poi.ooxml@default:default"/>
130
+ <setEntry value="org.apache.poi.source"/>
131
+ <setEntry value="org.apache.poi@default:default"/>
132
+ <setEntry value="org.apache.servicemix.bundles.ehcache@default:default"/>
133
+ <setEntry value="org.apache.servicemix.bundles.scribe@default:default"/>
134
+ <setEntry value="org.apache.servicemix.bundles.zxing@default:default"/>
135
+ <setEntry value="org.apache.shiro.core.source"/>
136
+ <setEntry value="org.apache.shiro.core@default:default"/>
137
+ <setEntry value="org.apache.shiro.ehcache.source"/>
138
+ <setEntry value="org.apache.shiro.ehcache@default:default"/>
139
+ <setEntry value="org.apache.shiro.web.source"/>
140
+ <setEntry value="org.apache.shiro.web@default:default"/>
141
+ <setEntry value="org.apache.taglibs.standard-impl@default:default"/>
142
+ <setEntry value="org.apache.taglibs.taglibs-standard-spec@default:default"/>
143
+ <setEntry value="org.apache.xalan@default:default"/>
144
+ <setEntry value="org.apache.xml.serializer@default:default"/>
145
+ <setEntry value="org.apache.xmlbeans@default:default"/>
146
+ <setEntry value="org.dom4j@default:default"/>
147
+ <setEntry value="org.eclipse.equinox.cm@default:default"/>
148
+ <setEntry value="org.eclipse.equinox.common@2:true"/>
149
+ <setEntry value="org.eclipse.equinox.console@default:default"/>
150
+ <setEntry value="org.eclipse.equinox.event@default:default"/>
151
+ <setEntry value="org.eclipse.equinox.launcher@default:default"/>
152
+ <setEntry value="org.eclipse.equinox.simpleconfigurator@2:true"/>
153
+ <setEntry value="org.eclipse.jdt.core.compiler.batch@default:default"/>
154
+ <setEntry value="org.eclipse.jetty.annotations@4:true"/>
155
+ <setEntry value="org.eclipse.jetty.apache-jsp.source"/>
156
+ <setEntry value="org.eclipse.jetty.apache-jsp@4:true"/>
157
+ <setEntry value="org.eclipse.jetty.client@default:default"/>
158
+ <setEntry value="org.eclipse.jetty.deploy.source"/>
159
+ <setEntry value="org.eclipse.jetty.deploy@default:default"/>
160
+ <setEntry value="org.eclipse.jetty.http.source"/>
161
+ <setEntry value="org.eclipse.jetty.http@3:true"/>
162
+ <setEntry value="org.eclipse.jetty.io.source"/>
163
+ <setEntry value="org.eclipse.jetty.io@default:default"/>
164
+ <setEntry value="org.eclipse.jetty.jmx.source"/>
165
+ <setEntry value="org.eclipse.jetty.jmx@default:default"/>
166
+ <setEntry value="org.eclipse.jetty.jndi@default:default"/>
167
+ <setEntry value="org.eclipse.jetty.osgi-servlet-api@default:default"/>
168
+ <setEntry value="org.eclipse.jetty.osgi.boot.jsp.source"/>
169
+ <setEntry value="org.eclipse.jetty.osgi.boot.jsp@default:false"/>
170
+ <setEntry value="org.eclipse.jetty.osgi.boot.warurl@default:default"/>
171
+ <setEntry value="org.eclipse.jetty.osgi.boot@3:true"/>
172
+ <setEntry value="org.eclipse.jetty.plus@default:default"/>
173
+ <setEntry value="org.eclipse.jetty.security.source"/>
174
+ <setEntry value="org.eclipse.jetty.security@default:default"/>
175
+ <setEntry value="org.eclipse.jetty.server.source"/>
176
+ <setEntry value="org.eclipse.jetty.server@default:default"/>
177
+ <setEntry value="org.eclipse.jetty.servlet.source"/>
178
+ <setEntry value="org.eclipse.jetty.servlet@default:default"/>
179
+ <setEntry value="org.eclipse.jetty.util.ajax.source"/>
180
+ <setEntry value="org.eclipse.jetty.util.ajax@default:default"/>
181
+ <setEntry value="org.eclipse.jetty.util.source"/>
182
+ <setEntry value="org.eclipse.jetty.util@default:default"/>
183
+ <setEntry value="org.eclipse.jetty.webapp.source"/>
184
+ <setEntry value="org.eclipse.jetty.webapp@default:default"/>
185
+ <setEntry value="org.eclipse.jetty.websocket.api.source"/>
186
+ <setEntry value="org.eclipse.jetty.websocket.api@default:default"/>
187
+ <setEntry value="org.eclipse.jetty.websocket.client.source"/>
188
+ <setEntry value="org.eclipse.jetty.websocket.client@default:default"/>
189
+ <setEntry value="org.eclipse.jetty.websocket.common.source"/>
190
+ <setEntry value="org.eclipse.jetty.websocket.common@default:default"/>
191
+ <setEntry value="org.eclipse.jetty.websocket.server.source"/>
192
+ <setEntry value="org.eclipse.jetty.websocket.server@default:default"/>
193
+ <setEntry value="org.eclipse.jetty.websocket.servlet.source"/>
194
+ <setEntry value="org.eclipse.jetty.websocket.servlet@default:default"/>
195
+ <setEntry value="org.eclipse.jetty.xml.source"/>
196
+ <setEntry value="org.eclipse.jetty.xml@default:default"/>
197
+ <setEntry value="org.eclipse.osgi.services.source"/>
198
+ <setEntry value="org.eclipse.osgi.services@default:default"/>
199
+ <setEntry value="org.eclipse.osgi.source"/>
200
+ <setEntry value="org.eclipse.osgi.util.source"/>
201
+ <setEntry value="org.eclipse.osgi.util@default:default"/>
202
+ <setEntry value="org.eclipse.osgi@-1:true"/>
203
+ <setEntry value="org.hyperic.sigar@default:default"/>
204
+ <setEntry value="org.jboss.marshalling.jboss-marshalling-osgi@default:default"/>
205
+ <setEntry value="org.jvnet.mimepull@default:default"/>
206
+ <setEntry value="org.mongodb.bson.source"/>
207
+ <setEntry value="org.mongodb.bson@default:default"/>
208
+ <setEntry value="org.mongodb.driver-core.source"/>
209
+ <setEntry value="org.mongodb.driver-core@default:default"/>
210
+ <setEntry value="org.mongodb.driver-sync@default:default"/>
211
+ <setEntry value="org.mortbay.jasper.apache-el.source"/>
212
+ <setEntry value="org.mortbay.jasper.apache-el@default:default"/>
213
+ <setEntry value="org.mortbay.jasper.apache-jsp.source"/>
214
+ <setEntry value="org.mortbay.jasper.apache-jsp@default:default"/>
215
+ <setEntry value="org.objectweb.asm.commons.source"/>
216
+ <setEntry value="org.objectweb.asm.commons@default:default"/>
217
+ <setEntry value="org.objectweb.asm.source"/>
218
+ <setEntry value="org.objectweb.asm.tree.analysis.source"/>
219
+ <setEntry value="org.objectweb.asm.tree.analysis@default:default"/>
220
+ <setEntry value="org.objectweb.asm.tree.source"/>
221
+ <setEntry value="org.objectweb.asm.tree@default:default"/>
222
+ <setEntry value="org.objectweb.asm.util.source"/>
223
+ <setEntry value="org.objectweb.asm.util@default:default"/>
224
+ <setEntry value="org.objectweb.asm@default:default"/>
225
+ <setEntry value="org.osgi.service.cm@default:default"/>
226
+ <setEntry value="org.osgi.service.component@default:default"/>
227
+ <setEntry value="org.osgi.service.event@default:default"/>
228
+ <setEntry value="org.osgi.service.packageadmin@default:default"/>
229
+ <setEntry value="org.osgi.service.url@default:default"/>
230
+ <setEntry value="org.osgi.util.function.source"/>
231
+ <setEntry value="org.osgi.util.function@default:default"/>
232
+ <setEntry value="org.osgi.util.measurement.source"/>
233
+ <setEntry value="org.osgi.util.measurement@default:default"/>
234
+ <setEntry value="org.osgi.util.position.source"/>
235
+ <setEntry value="org.osgi.util.position@default:default"/>
236
+ <setEntry value="org.osgi.util.promise.source"/>
237
+ <setEntry value="org.osgi.util.promise@default:default"/>
238
+ <setEntry value="org.osgi.util.tracker@default:default"/>
239
+ <setEntry value="org.osgi.util.xml.source"/>
240
+ <setEntry value="org.osgi.util.xml@default:default"/>
241
+ <setEntry value="org.owasp.encoder.source"/>
242
+ <setEntry value="org.owasp.encoder@default:default"/>
243
+ <setEntry value="org.yaml.snakeyaml@default:default"/>
244
+ <setEntry value="reactive-streams@default:default"/>
245
+ <setEntry value="redisson@default:default"/>
246
+ <setEntry value="routeconverter@default:default"/>
247
+ <setEntry value="slf4j.api@default:default"/>
248
+ <setEntry value="slf4j.jdk14@default:default"/>
249
+ </setAttribute>
254 250
<setAttribute key="selected_workspace_bundles">
255
-<setEntry value="com.sap.sailing.geocoding@default:default"/>
256
-<setEntry value="com.sap.sailing.domain.common@default:default"/>
257
-<setEntry value="com.sap.sailing.domain@default:default"/>
258
-<setEntry value="com.sap.sailing.news@4:true"/>
259
-<setEntry value="com.sap.sailing.domain.tractracadapter@5:true"/>
260
-<setEntry value="com.sap.sailing.expeditionconnector@default:default"/>
261
-<setEntry value="com.sap.sailing.domain.vakarosadapter@4:true"/>
262
-<setEntry value="com.sap.sailing.domain.windfinderadapter@4:true"/>
263
-<setEntry value="com.sap.sailing.server@5:true"/>
264
-<setEntry value="com.sap.sailing.server.gateway@5:true"/>
265
-<setEntry value="com.sap.sailing.server.gateway.interfaces@default:default"/>
266
-<setEntry value="com.sap.sailing.declination@default:default"/>
267
-<setEntry value="com.sap.sailing.domain.persistence@default:default"/>
268
-<setEntry value="com.sap.sailing.domain.swisstimingadapter@5:true"/>
269
-<setEntry value="com.sap.sailing.domain.swisstimingadapter.persistence@4:true"/>
270
-<setEntry value="com.sap.sailing.domain.swisstimingreplayadapter@4:true"/>
271
-<setEntry value="com.sap.sailing.domain.tractracadapter.persistence@4:true"/>
272
-<setEntry value="com.sap.sailing.gwt.ui@6:true"/>
273
-<setEntry value="com.sap.sailing.udpconnector@default:default"/>
274
-<setEntry value="com.sap.sailing.simulator@default:default"/>
275
-<setEntry value="com.sap.sailing.www@5:true"/>
276
-<setEntry value="com.sap.sailing.resultimport@4:true"/>
277
-<setEntry value="com.sap.sailing.kiworesultimport@4:true"/>
278
-<setEntry value="com.sap.sailing.ess40.resultimport@4:true"/>
279
-<setEntry value="com.sap.sailing.freg.resultimport@4:true"/>
280
-<setEntry value="com.sap.sailing.barbados.resultimport@4:true"/>
281
-<setEntry value="com.sap.sailing.sailwave.resultimport@4:true"/>
282
-<setEntry value="com.sap.sailing.sailwave.html.resultimport@4:true"/>
283
-<setEntry value="com.sap.sailing.manage2sail.resultimport@4:true"/>
284
-<setEntry value="com.sap.sailing.sailti.resultimport@4:true"/>
285
-<setEntry value="com.sap.sailing.yachtscoring.resultimport@4:true"/>
286
-<setEntry value="com.sap.sailing.velum.resultimport@4:true"/>
287
-<setEntry value="com.sap.sailing.monitoring@7:true"/>
288
-<setEntry value="com.sap.sailing.xrr.resultimport@4:true"/>
289
-<setEntry value="com.sap.sailing.domain.igtimiadapter@default:default"/>
290
-<setEntry value="com.sap.sailing.domain.igtimiadapter.server@4:true"/>
291
-<setEntry value="com.sap.sailing.domain.igtimiadapter.persistence@default:default"/>
292
-<setEntry value="com.sap.sailing.domain.racelogtrackingadapter@4:true"/>
293
-<setEntry value="com.sap.sailing.domain.deckmanadapter@5:true"/>
294
-<setEntry value="com.sap.sailing.domain.oceanraceadapter@5:true"/>
295
-<setEntry value="com.sap.sailing.domain.yellowbrickadapter@5:true"/>
296
-<setEntry value="com.sap.sailing.domain.yellowbrickadapter.persistence@4:true"/>
297
-<setEntry value="com.sap.sailing.xrr.structureimport@default:default"/>
298
-<setEntry value="com.sap.sailing.server.gateway.serialization.shared.android@default:default"/>
299
-<setEntry value="com.sap.sailing.server.gateway.serialization@default:default"/>
300
-<setEntry value="com.sap.sailing.dashboards.gwt@6:true"/>
301
-<setEntry value="com.sap.sailing.dashboards.gwt@6:true"/>
302
-<setEntry value="com.sap.sailing.datamining@5:true"/>
303
-<setEntry value="com.sap.sailing.datamining.shared@default:default"/>
304
-<setEntry value="com.sap.sailing.polars@5:true"/>
305
-<setEntry value="com.sap.sailing.windestimation@5:true"/>
306
-<setEntry value="com.sap.sailing.polars.datamining@5:true"/>
307
-<setEntry value="com.sap.sailing.domain.shared.android@default:default"/>
308
-<setEntry value="com.sap.sailing.manage2sail@default:default"/>
309
-<setEntry value="com.sap.sailing.polars.datamining.shared@default:default"/>
310
-<setEntry value="com.sap.sailing.xrr.schema@default:default"/>
311
-<setEntry value="com.sap.sailing.server.trackfiles@default:default"/>
312
-<setEntry value="com.sap.sailing.competitorimport@default:default"/>
313
-<setEntry value="com.sap.sailing.datamining.provider@default:default"/>
314
-<setEntry value="com.sap.sailing.grib@default:default"/>
315
-<setEntry value="com.sap.sailing.nmeaconnector@default:default"/>
316
-<setEntry value="com.sap.sailing.domain.expeditionadapter@5:true"/>
317
-<setEntry value="com.sap.sailing.expeditionconnector.persistence@4:true"/>
318
-<setEntry value="com.sap.sailing.expeditionconnector.common@default:default"/>
319
-<setEntry value="com.sap.sailing.domain.bravoadapter@5:true"/>
320
-<setEntry value="net.sf.marineapi@default:default"/>
321
-<setEntry value="com.sap.sailing.routeconverterjava11extension@default:default"/>
322
-<setEntry value="com.sap.sailing.server.interface@default:default"/>
323
-<setEntry value="com.sap.sse.datamining.ui@default:default"/>
324
-<setEntry value="com.sap.sailing.domain.igtimiadapter.gateway@5:true"/>
325
-<setEntry value="com.sap.sailing.shared.server@5:true"/>
326
-<setEntry value="com.sap.sailing.shared.server.gateway@5:true"/>
327
-<setEntry value="com.sap.sailing.shared.persistence@default:default"/>
328
-<setEntry value="com.sap.sailing.landscape@default:default"/>
329
-<setEntry value="com.sap.sailing.landscape.gateway@5:true"/>
330
-<setEntry value="com.sap.sailing.landscape.common@default:default"/>
331
-<setEntry value="com.sap.sailing.landscape.ui@default:default"/>
332
-<setEntry value="com.sap.sailing.hanaexport@5:true"/>
333
-<setEntry value="com.sap.sailing.domain.queclinkadapter@5:true"/>
334
-<setEntry value="com.sap.sailing.aiagent.interfaces@default:default"/>
335
-<setEntry value="com.sap.sailing.aiagent@4:true"/>
336
-<setEntry value="com.sap.sailing.aiagent.persistence@5:true"/>
337
-<setEntry value="com.sap.sailing.aiagent.gateway@6:true"/>
338
-<setEntry value="com.tractrac.clientmodule@default:default"/>
339
-<setEntry value="com.sap.sse.gwt@default:default"/>
340
-<setEntry value="com.google.gwt.servlet@default:default"/>
341
-<setEntry value="com.sap.sse.security@default:default"/>
342
-<setEntry value="com.sap.sse.security.ui@6:true"/>
343
-<setEntry value="com.sap.sse.security.userstore.mongodb@4:true"/>
344
-<setEntry value="com.sap.sse@default:default"/>
345
-<setEntry value="com.sap.sse.common@default:default"/>
346
-<setEntry value="com.sap.sse.datamining@default:default"/>
347
-<setEntry value="com.sap.sse.datamining.annotations@default:default"/>
348
-<setEntry value="com.sap.sse.datamining.shared@default:default"/>
349
-<setEntry value="com.sap.sse.gwt.adminconsole@default:default"/>
350
-<setEntry value="com.sap.sse.mongodb@default:default"/>
351
-<setEntry value="com.sap.sse.operationaltransformation@default:default"/>
352
-<setEntry value="com.sap.sse.replication@6:true"/>
353
-<setEntry value="com.sap.sse.filestorage@4:true"/>
354
-<setEntry value="com.sap.sse.shared.android@default:default"/>
355
-<setEntry value="com.sap.sse.mail@5:true"/>
356
-<setEntry value="com.sap.sse.threadmanager@default:default"/>
357
-<setEntry value="com.sap.sse.security.common@default:default"/>
358
-<setEntry value="org.json.simple@default:default"/>
359
-<setEntry value="org.moxieapps.gwt.highcharts@default:default"/>
360
-<setEntry value="com.googlecode.java-diff-utils@default:default"/>
361
-<setEntry value="org.mp4parser.isoparser@default:default"/>
362
-<setEntry value="com.sap.sse.replication.interfaces@default:default"/>
363
-<setEntry value="com.sap.sse.security.datamining@5:true"/>
364
-<setEntry value="com.sap.sse.security.persistence@default:default"/>
365
-<setEntry value="com.sap.sse.security.interface@default:default"/>
366
-<setEntry value="com.sap.sse.replication.persistence@default:default"/>
367
-<setEntry value="com.sap.sse.landscape.common@default:default"/>
368
-<setEntry value="com.sap.sse.landscape@default:default"/>
369
-<setEntry value="com.sap.sse.landscape.aws@4:true"/>
370
-<setEntry value="com.sap.sse.landscape.aws.common@default:default"/>
371
-<setEntry value="com.sap.sse.landscape.aws.persistence@default:default"/>
372
-<setEntry value="com.sap.sse.branding@default:default"/>
373
-<setEntry value="com.sap.sse.branding.sap@5:true"/>
374
-<setEntry value="com.sap.sse.aicore@default:default"/>
375
-<setEntry value="elemental2@default:default"/>
376
-<setEntry value="com.sap.sailing.server.gateway.test.support@5:true"/>
377
-
378
-</setAttribute>
251
+ <setEntry value="com.google.gwt.servlet@default:default"/>
252
+ <setEntry value="com.googlecode.java-diff-utils@default:default"/>
253
+ <setEntry value="com.sap.sailing.aiagent.gateway@6:true"/>
254
+ <setEntry value="com.sap.sailing.aiagent.interfaces@default:default"/>
255
+ <setEntry value="com.sap.sailing.aiagent.persistence@5:true"/>
256
+ <setEntry value="com.sap.sailing.aiagent@4:true"/>
257
+ <setEntry value="com.sap.sailing.barbados.resultimport@4:true"/>
258
+ <setEntry value="com.sap.sailing.competitorimport@default:default"/>
259
+ <setEntry value="com.sap.sailing.dashboards.gwt@6:true"/>
260
+ <setEntry value="com.sap.sailing.datamining.provider@default:default"/>
261
+ <setEntry value="com.sap.sailing.datamining.shared@default:default"/>
262
+ <setEntry value="com.sap.sailing.datamining@5:true"/>
263
+ <setEntry value="com.sap.sailing.declination@default:default"/>
264
+ <setEntry value="com.sap.sailing.domain.bravoadapter@5:true"/>
265
+ <setEntry value="com.sap.sailing.domain.common@default:default"/>
266
+ <setEntry value="com.sap.sailing.domain.deckmanadapter@5:true"/>
267
+ <setEntry value="com.sap.sailing.domain.expeditionadapter@5:true"/>
268
+ <setEntry value="com.sap.sailing.domain.igtimiadapter.gateway@5:true"/>
269
+ <setEntry value="com.sap.sailing.domain.igtimiadapter.persistence@default:default"/>
270
+ <setEntry value="com.sap.sailing.domain.igtimiadapter.server@4:true"/>
271
+ <setEntry value="com.sap.sailing.domain.igtimiadapter@default:default"/>
272
+ <setEntry value="com.sap.sailing.domain.oceanraceadapter@5:true"/>
273
+ <setEntry value="com.sap.sailing.domain.persistence@default:default"/>
274
+ <setEntry value="com.sap.sailing.domain.queclinkadapter@5:true"/>
275
+ <setEntry value="com.sap.sailing.domain.racelogtrackingadapter@4:true"/>
276
+ <setEntry value="com.sap.sailing.domain.shared.android@default:default"/>
277
+ <setEntry value="com.sap.sailing.domain.swisstimingadapter.persistence@4:true"/>
278
+ <setEntry value="com.sap.sailing.domain.swisstimingadapter@5:true"/>
279
+ <setEntry value="com.sap.sailing.domain.swisstimingreplayadapter@4:true"/>
280
+ <setEntry value="com.sap.sailing.domain.tractracadapter.persistence@4:true"/>
281
+ <setEntry value="com.sap.sailing.domain.tractracadapter@5:true"/>
282
+ <setEntry value="com.sap.sailing.domain.vakarosadapter@4:true"/>
283
+ <setEntry value="com.sap.sailing.domain.windfinderadapter@4:true"/>
284
+ <setEntry value="com.sap.sailing.domain.yellowbrickadapter.persistence@4:true"/>
285
+ <setEntry value="com.sap.sailing.domain.yellowbrickadapter@5:true"/>
286
+ <setEntry value="com.sap.sailing.domain@default:default"/>
287
+ <setEntry value="com.sap.sailing.ess40.resultimport@4:true"/>
288
+ <setEntry value="com.sap.sailing.expeditionconnector.common@default:default"/>
289
+ <setEntry value="com.sap.sailing.expeditionconnector.persistence@4:true"/>
290
+ <setEntry value="com.sap.sailing.expeditionconnector@default:default"/>
291
+ <setEntry value="com.sap.sailing.freg.resultimport@4:true"/>
292
+ <setEntry value="com.sap.sailing.geocoding@default:default"/>
293
+ <setEntry value="com.sap.sailing.grib@default:default"/>
294
+ <setEntry value="com.sap.sailing.gwt.ui@6:true"/>
295
+ <setEntry value="com.sap.sailing.hanaexport@5:true"/>
296
+ <setEntry value="com.sap.sailing.kiworesultimport@4:true"/>
297
+ <setEntry value="com.sap.sailing.landscape.common@default:default"/>
298
+ <setEntry value="com.sap.sailing.landscape.gateway@5:true"/>
299
+ <setEntry value="com.sap.sailing.landscape.ui@default:default"/>
300
+ <setEntry value="com.sap.sailing.landscape@default:default"/>
301
+ <setEntry value="com.sap.sailing.manage2sail.resultimport@4:true"/>
302
+ <setEntry value="com.sap.sailing.manage2sail@default:default"/>
303
+ <setEntry value="com.sap.sailing.monitoring@7:true"/>
304
+ <setEntry value="com.sap.sailing.news@4:true"/>
305
+ <setEntry value="com.sap.sailing.nmeaconnector@default:default"/>
306
+ <setEntry value="com.sap.sailing.polars.datamining.shared@default:default"/>
307
+ <setEntry value="com.sap.sailing.polars.datamining@5:true"/>
308
+ <setEntry value="com.sap.sailing.polars@5:true"/>
309
+ <setEntry value="com.sap.sailing.resultimport@4:true"/>
310
+ <setEntry value="com.sap.sailing.routeconverterjava11extension@default:false"/>
311
+ <setEntry value="com.sap.sailing.sailti.resultimport@4:true"/>
312
+ <setEntry value="com.sap.sailing.sailwave.html.resultimport@4:true"/>
313
+ <setEntry value="com.sap.sailing.sailwave.resultimport@4:true"/>
314
+ <setEntry value="com.sap.sailing.server.gateway.interfaces@default:default"/>
315
+ <setEntry value="com.sap.sailing.server.gateway.serialization.shared.android@default:default"/>
316
+ <setEntry value="com.sap.sailing.server.gateway.serialization@default:default"/>
317
+ <setEntry value="com.sap.sailing.server.gateway.test.support@5:true"/>
318
+ <setEntry value="com.sap.sailing.server.gateway@5:true"/>
319
+ <setEntry value="com.sap.sailing.server.interface@default:default"/>
320
+ <setEntry value="com.sap.sailing.server.trackfiles@default:default"/>
321
+ <setEntry value="com.sap.sailing.server@5:true"/>
322
+ <setEntry value="com.sap.sailing.shared.persistence@default:default"/>
323
+ <setEntry value="com.sap.sailing.shared.server.gateway@5:true"/>
324
+ <setEntry value="com.sap.sailing.shared.server@5:true"/>
325
+ <setEntry value="com.sap.sailing.simulator@default:default"/>
326
+ <setEntry value="com.sap.sailing.udpconnector@default:default"/>
327
+ <setEntry value="com.sap.sailing.velum.resultimport@4:true"/>
328
+ <setEntry value="com.sap.sailing.windestimation@5:true"/>
329
+ <setEntry value="com.sap.sailing.www@5:true"/>
330
+ <setEntry value="com.sap.sailing.xrr.resultimport@4:true"/>
331
+ <setEntry value="com.sap.sailing.xrr.schema@default:default"/>
332
+ <setEntry value="com.sap.sailing.xrr.structureimport@default:default"/>
333
+ <setEntry value="com.sap.sailing.yachtscoring.resultimport@4:true"/>
334
+ <setEntry value="com.sap.sse.aicore@default:default"/>
335
+ <setEntry value="com.sap.sse.branding.sap@5:true"/>
336
+ <setEntry value="com.sap.sse.branding@default:default"/>
337
+ <setEntry value="com.sap.sse.common@default:default"/>
338
+ <setEntry value="com.sap.sse.datamining.annotations@default:default"/>
339
+ <setEntry value="com.sap.sse.datamining.shared@default:default"/>
340
+ <setEntry value="com.sap.sse.datamining.ui@default:default"/>
341
+ <setEntry value="com.sap.sse.datamining@default:default"/>
342
+ <setEntry value="com.sap.sse.filestorage@4:true"/>
343
+ <setEntry value="com.sap.sse.gwt.adminconsole@default:default"/>
344
+ <setEntry value="com.sap.sse.gwt@default:default"/>
345
+ <setEntry value="com.sap.sse.landscape.aws.common@default:default"/>
346
+ <setEntry value="com.sap.sse.landscape.aws.persistence@default:default"/>
347
+ <setEntry value="com.sap.sse.landscape.aws@4:true"/>
348
+ <setEntry value="com.sap.sse.landscape.common@default:default"/>
349
+ <setEntry value="com.sap.sse.landscape@default:default"/>
350
+ <setEntry value="com.sap.sse.mail@5:true"/>
351
+ <setEntry value="com.sap.sse.mongodb@default:default"/>
352
+ <setEntry value="com.sap.sse.operationaltransformation@default:default"/>
353
+ <setEntry value="com.sap.sse.replication.interfaces@default:default"/>
354
+ <setEntry value="com.sap.sse.replication.persistence@default:default"/>
355
+ <setEntry value="com.sap.sse.replication@6:true"/>
356
+ <setEntry value="com.sap.sse.security.common@default:default"/>
357
+ <setEntry value="com.sap.sse.security.datamining@5:true"/>
358
+ <setEntry value="com.sap.sse.security.interface@default:default"/>
359
+ <setEntry value="com.sap.sse.security.persistence@default:default"/>
360
+ <setEntry value="com.sap.sse.security.ui@6:true"/>
361
+ <setEntry value="com.sap.sse.security.userstore.mongodb@4:true"/>
362
+ <setEntry value="com.sap.sse.security@default:default"/>
363
+ <setEntry value="com.sap.sse.shared.android@default:default"/>
364
+ <setEntry value="com.sap.sse.threadmanager@default:default"/>
365
+ <setEntry value="com.sap.sse@default:default"/>
366
+ <setEntry value="com.tractrac.clientmodule@default:default"/>
367
+ <setEntry value="elemental2@default:default"/>
368
+ <setEntry value="net.sf.marineapi@default:default"/>
369
+ <setEntry value="org.json.simple@default:default"/>
370
+ <setEntry value="org.moxieapps.gwt.highcharts@default:default"/>
371
+ <setEntry value="org.mp4parser.isoparser@default:default"/>
372
+ </setAttribute>
379 373
<booleanAttribute key="show_selected_only" value="false"/>
380 374
<booleanAttribute key="tracing" value="false"/>
381 375
<booleanAttribute key="useCustomFeatures" value="false"/>
java/com.sap.sailing.windestimation.lab/python/MST_GRAPH_VISUALIZATION_README.md
... ...
@@ -0,0 +1,171 @@
1
+# MST Maneuver Graph Visualization
2
+
3
+This directory contains tools for visualizing the MST (Minimum Spanning Tree)
4
+maneuver graph used in wind estimation.
5
+
6
+## Overview
7
+
8
+The MST maneuver graph is a tree structure where:
9
+- Each tree node (`MstGraphLevel`) represents a detected maneuver with its timestamp and position
10
+- Each tree node has 4 "compartments" - one for each possible maneuver classification:
11
+ - **TACK** (green): Tacking maneuver
12
+ - **JIBE** (blue): Jibing/gybing maneuver
13
+ - **HEAD_UP** (orange): Heading up toward the wind
14
+ - **BEAR_AWAY** (purple): Bearing away from the wind
15
+- Each compartment shows:
16
+ - Classification confidence (probability this maneuver is of this type)
17
+ - Estimated wind direction range
18
+- Edges connect compartments between adjacent tree levels with transition probabilities
19
+- The "best path" through the inner graph is highlighted - this determines the final maneuver classifications
20
+
21
+## Files
22
+
23
+### Java (Exporter)
24
+
25
+- `MstGraphExporter.java` - Exports the MST graph to JSON format
26
+- `MstGraphExportHelper.java` - Helper class with convenience methods
27
+
28
+### Python (Visualizers)
29
+
30
+- `mst_graph_visualizer.py` - Basic matplotlib visualization
31
+- `mst_graph_visualizer_graphviz.py` - Advanced graphviz visualization (recommended)
32
+
33
+## Usage
34
+
35
+### Step 1: Export the graph from Java
36
+
37
+In your Java code (e.g., in a test or debug session):
38
+
39
+```java
40
+import com.sap.sailing.windestimation.aggregator.msthmm.MstGraphExportHelper;
41
+
42
+// After building your MST graph:
43
+MstManeuverGraphComponents graphComponents = mstManeuverGraphGenerator.parseGraph();
44
+
45
+// Export to JSON file:
46
+MstGraphExportHelper.exportToFile(graphComponents, transitionProbabilitiesCalculator,
47
+ "/tmp/mst_graph.json");
48
+```
49
+
50
+### Step 2: Visualize with Python
51
+
52
+Install dependencies:
53
+```bash
54
+pip install matplotlib graphviz
55
+```
56
+
57
+For graphviz, also install the system package:
58
+```bash
59
+# Ubuntu/Debian
60
+sudo apt-get install graphviz
61
+
62
+# macOS
63
+brew install graphviz
64
+```
65
+
66
+Run the visualizer:
67
+```bash
68
+# Basic tree visualization
69
+python mst_graph_visualizer_graphviz.py /tmp/mst_graph.json output.pdf
70
+
71
+# Detailed compartment-level view (for small graphs)
72
+python mst_graph_visualizer_graphviz.py /tmp/mst_graph.json output.pdf --detailed
73
+
74
+# Interactive matplotlib visualization
75
+python mst_graph_visualizer.py /tmp/mst_graph.json
76
+```
77
+
78
+## Understanding the Visualization
79
+
80
+### Node Structure
81
+
82
+Each node box contains 4 compartments (T, J, H, B):
83
+```
84
++------+------+------+------+
85
+| T | J | H | B |
86
+| 0.68 | 0.05 | 0.13 | 0.13 |
87
+| 224° | 44° | 308°±51° | 89°±51° |
88
++------+------+------+------+
89
+ 15:30:41
90
+```
91
+
92
+- First row: Maneuver type abbreviation
93
+- Second row: Classification confidence
94
+- Third row: Wind direction estimate (or range for HEAD_UP/BEAR_AWAY)
95
+- Below: Timestamp
96
+
97
+### Edge Colors
98
+
99
+- **Red**: Best path edges (selected by Dijkstra)
100
+- **Green**: High transition probability
101
+- **Gray**: Lower transition probability
102
+
103
+### Best Path Highlighting
104
+
105
+The best path represents the most likely sequence of maneuver classifications
106
+considering both:
107
+1. Individual classification confidences
108
+2. Transition probabilities (how likely is it for the wind to change this much given the time/distance between maneuvers)
109
+
110
+## JSON Format
111
+
112
+The exported JSON has this structure:
113
+
114
+```json
115
+{
116
+ "nodes": [
117
+ {
118
+ "id": 0,
119
+ "depth": 0,
120
+ "timestamp": "2011-06-23 15:30:40.500",
121
+ "position": {"lat": 54.493, "lon": 10.197},
122
+ "distanceToParent": 0.0,
123
+ "compartments": [
124
+ {
125
+ "type": "TACK",
126
+ "confidence": 0.17,
127
+ "windRangeFrom": 224.18,
128
+ "windRangeWidth": 0.0,
129
+ "windEstimate": 224.18,
130
+ "tackAfter": "PORT"
131
+ },
132
+ // ... JIBE, HEAD_UP, BEAR_AWAY
133
+ ]
134
+ }
135
+ ],
136
+ "edges": [
137
+ {
138
+ "from": 0,
139
+ "fromType": "TACK",
140
+ "to": 1,
141
+ "toType": "TACK",
142
+ "transitionProbability": 0.023,
143
+ "distance": 12.5,
144
+ "isBestPath": true
145
+ }
146
+ ],
147
+ "bestPaths": {
148
+ "0": "JIBE",
149
+ "1": "TACK"
150
+ }
151
+}
152
+```
153
+
154
+## Troubleshooting
155
+
156
+### Large graphs are slow to render
157
+
158
+For graphs with >100 nodes, the visualization limits the display to the first N nodes.
159
+Use the `--detailed` flag only for small subtrees.
160
+
161
+### Graphviz not found
162
+
163
+Make sure graphviz is installed at the system level, not just the Python package:
164
+```bash
165
+which dot # Should return path to dot executable
166
+```
167
+
168
+### Edges not showing
169
+
170
+Edges with very low transition probabilities are filtered out by default.
171
+Use `show_low_prob_edges=True` in Python to see all edges.
java/com.sap.sailing.windestimation.lab/python/mst_graph_visualizer.py
... ...
@@ -0,0 +1,338 @@
1
+#!/usr/bin/env python3
2
+"""
3
+MST Maneuver Graph Visualization Script
4
+
5
+This script visualizes the Minimum Spanning Tree (MST) maneuver graph structure
6
+exported from the Java MstGraphExporter. It shows:
7
+- Tree structure of MstGraphLevel nodes
8
+- Each node as a box with 4 compartments (TACK, JIBE, HEAD_UP, BEAR_AWAY)
9
+- Classification confidence and wind direction for each compartment
10
+- Edges between compartments with transition probabilities
11
+- Best path highlighting
12
+
13
+Usage:
14
+ python mst_graph_visualizer.py <input_json_file> [output_file]
15
+
16
+Dependencies:
17
+ pip install matplotlib networkx
18
+"""
19
+
20
+import json
21
+import sys
22
+import math
23
+from collections import defaultdict
24
+
25
+import matplotlib.pyplot as plt
26
+import matplotlib.patches as patches
27
+from matplotlib.patches import FancyBboxPatch, FancyArrowPatch
28
+import matplotlib.colors as mcolors
29
+
30
+# Maneuver type colors
31
+TYPE_COLORS = {
32
+ 'TACK': '#4CAF50', # Green
33
+ 'JIBE': '#2196F3', # Blue
34
+ 'HEAD_UP': '#FF9800', # Orange
35
+ 'BEAR_AWAY': '#9C27B0', # Purple
36
+}
37
+
38
+TYPE_ORDER = ['TACK', 'JIBE', 'HEAD_UP', 'BEAR_AWAY']
39
+
40
+
41
+class MstGraphVisualizer:
42
+ def __init__(self, data):
43
+ self.data = data
44
+ self.nodes = {n['id']: n for n in data['nodes']}
45
+ self.edges = data['edges']
46
+ self.best_paths = data.get('bestPaths', {})
47
+
48
+ # Layout parameters
49
+ self.node_width = 4.0
50
+ self.node_height = 1.2
51
+ self.compartment_width = self.node_width / 4
52
+ self.horizontal_spacing = 1.5
53
+ self.vertical_spacing = 2.5
54
+
55
+ # Calculate layout
56
+ self.node_positions = {}
57
+ self.calculate_layout()
58
+
59
+ def calculate_layout(self):
60
+ """Calculate x,y positions for each node based on tree structure."""
61
+ # Group nodes by depth
62
+ depth_groups = defaultdict(list)
63
+ for node in self.data['nodes']:
64
+ depth_groups[node['depth']].append(node['id'])
65
+
66
+ max_depth = max(depth_groups.keys()) if depth_groups else 0
67
+
68
+ # Assign positions - breadth-first layout
69
+ for depth in range(max_depth + 1):
70
+ nodes_at_depth = depth_groups[depth]
71
+ n = len(nodes_at_depth)
72
+ for i, node_id in enumerate(nodes_at_depth):
73
+ # Center nodes horizontally at each level
74
+ x = (i - (n - 1) / 2) * (self.node_width + self.horizontal_spacing)
75
+ y = -depth * (self.node_height + self.vertical_spacing)
76
+ self.node_positions[node_id] = (x, y)
77
+
78
+ def draw_node(self, ax, node_id):
79
+ """Draw a single node as a box with 4 compartments."""
80
+ node = self.nodes[node_id]
81
+ x, y = self.node_positions[node_id]
82
+
83
+ # Draw outer box
84
+ outer_rect = FancyBboxPatch(
85
+ (x - self.node_width/2, y - self.node_height/2),
86
+ self.node_width, self.node_height,
87
+ boxstyle="round,pad=0.02,rounding_size=0.1",
88
+ facecolor='white',
89
+ edgecolor='black',
90
+ linewidth=1.5
91
+ )
92
+ ax.add_patch(outer_rect)
93
+
94
+ # Check if this node has a best classification
95
+ best_type = self.best_paths.get(str(node_id))
96
+
97
+ # Draw compartments
98
+ compartments = {c['type']: c for c in node['compartments']}
99
+ for i, type_name in enumerate(TYPE_ORDER):
100
+ comp = compartments.get(type_name)
101
+ if comp is None:
102
+ continue
103
+
104
+ cx = x - self.node_width/2 + i * self.compartment_width
105
+ cy = y - self.node_height/2
106
+
107
+ # Highlight best type
108
+ is_best = (best_type == type_name)
109
+
110
+ # Draw compartment background
111
+ color = TYPE_COLORS.get(type_name, 'gray')
112
+ alpha = 0.8 if is_best else 0.3
113
+ comp_rect = patches.Rectangle(
114
+ (cx, cy),
115
+ self.compartment_width, self.node_height,
116
+ facecolor=color,
117
+ alpha=alpha,
118
+ edgecolor='black' if is_best else 'gray',
119
+ linewidth=2 if is_best else 0.5
120
+ )
121
+ ax.add_patch(comp_rect)
122
+
123
+ # Draw text - type abbreviation
124
+ type_abbrev = type_name[:1] # T, J, H, B
125
+ ax.text(cx + self.compartment_width/2, cy + self.node_height * 0.75,
126
+ type_abbrev, ha='center', va='center', fontsize=8, fontweight='bold')
127
+
128
+ # Draw confidence
129
+ conf_text = f"{comp['confidence']:.2f}"
130
+ ax.text(cx + self.compartment_width/2, cy + self.node_height * 0.5,
131
+ conf_text, ha='center', va='center', fontsize=6)
132
+
133
+ # Draw wind direction
134
+ wind_est = comp.get('windEstimate', comp.get('windRangeFrom', 0))
135
+ wind_width = comp.get('windRangeWidth', 0)
136
+ if wind_width < 1:
137
+ wind_text = f"{wind_est:.0f}°"
138
+ else:
139
+ wind_text = f"{wind_est:.0f}±{wind_width/2:.0f}°"
140
+ ax.text(cx + self.compartment_width/2, cy + self.node_height * 0.25,
141
+ wind_text, ha='center', va='center', fontsize=5)
142
+
143
+ # Draw timestamp label below node
144
+ timestamp = node.get('timestamp', '')
145
+ # Extract just time portion
146
+ if ' ' in timestamp:
147
+ time_part = timestamp.split(' ')[1][:8] # HH:MM:SS
148
+ else:
149
+ time_part = timestamp
150
+ ax.text(x, y - self.node_height/2 - 0.15, time_part,
151
+ ha='center', va='top', fontsize=5, color='gray')
152
+
153
+ def draw_edge(self, ax, edge):
154
+ """Draw an edge between two compartments."""
155
+ from_id = edge['from']
156
+ to_id = edge['to']
157
+ from_type = edge['fromType']
158
+ to_type = edge['toType']
159
+ trans_prob = edge['transitionProbability']
160
+ is_best = edge.get('isBestPath', False)
161
+
162
+ # Get compartment positions
163
+ from_x, from_y = self.node_positions[from_id]
164
+ to_x, to_y = self.node_positions[to_id]
165
+
166
+ from_type_idx = TYPE_ORDER.index(from_type)
167
+ to_type_idx = TYPE_ORDER.index(to_type)
168
+
169
+ # Calculate start and end points at compartment centers
170
+ start_x = from_x - self.node_width/2 + (from_type_idx + 0.5) * self.compartment_width
171
+ start_y = from_y - self.node_height/2
172
+
173
+ end_x = to_x - self.node_width/2 + (to_type_idx + 0.5) * self.compartment_width
174
+ end_y = to_y + self.node_height/2
175
+
176
+ # Color based on transition probability and best path status
177
+ if is_best:
178
+ color = 'red'
179
+ linewidth = 2.0
180
+ alpha = 0.9
181
+ else:
182
+ # Color from green (high prob) to gray (low prob)
183
+ prob_normalized = min(1.0, trans_prob * 100) # Scale up small probabilities
184
+ color = plt.cm.RdYlGn(prob_normalized)
185
+ linewidth = 0.5 + prob_normalized * 1.5
186
+ alpha = 0.2 + prob_normalized * 0.4
187
+
188
+ # Draw the edge
189
+ ax.annotate('',
190
+ xy=(end_x, end_y), xytext=(start_x, start_y),
191
+ arrowprops=dict(
192
+ arrowstyle='-|>',
193
+ color=color,
194
+ lw=linewidth,
195
+ alpha=alpha,
196
+ shrinkA=2, shrinkB=2,
197
+ connectionstyle="arc3,rad=0.1" if abs(from_type_idx - to_type_idx) > 0 else "arc3,rad=0"
198
+ ))
199
+
200
+ # Optionally add probability label for best path edges
201
+ if is_best:
202
+ mid_x = (start_x + end_x) / 2
203
+ mid_y = (start_y + end_y) / 2
204
+ ax.text(mid_x + 0.3, mid_y, f"{trans_prob:.2e}",
205
+ fontsize=4, color='red', alpha=0.8)
206
+
207
+ def visualize(self, output_file=None, max_nodes=50, show_edges=True,
208
+ edge_filter_threshold=0.0001):
209
+ """
210
+ Create the visualization.
211
+
212
+ Args:
213
+ output_file: Path to save the figure (or None to display)
214
+ max_nodes: Maximum number of nodes to display (for large graphs)
215
+ show_edges: Whether to draw edges
216
+ edge_filter_threshold: Only show edges with probability above this
217
+ """
218
+ # Limit nodes if graph is too large
219
+ nodes_to_draw = list(self.nodes.keys())[:max_nodes]
220
+
221
+ # Calculate figure size based on layout
222
+ x_coords = [self.node_positions[n][0] for n in nodes_to_draw]
223
+ y_coords = [self.node_positions[n][1] for n in nodes_to_draw]
224
+
225
+ width = max(x_coords) - min(x_coords) + self.node_width * 2
226
+ height = max(y_coords) - min(y_coords) + self.node_height * 3
227
+
228
+ # Scale figure
229
+ scale = 0.8
230
+ fig_width = max(12, width * scale)
231
+ fig_height = max(8, height * scale)
232
+
233
+ fig, ax = plt.subplots(figsize=(fig_width, fig_height))
234
+
235
+ # Draw edges first (so they're behind nodes)
236
+ if show_edges:
237
+ # Filter edges to only those involving displayed nodes
238
+ relevant_edges = [e for e in self.edges
239
+ if e['from'] in nodes_to_draw
240
+ and e['to'] in nodes_to_draw
241
+ and (e['transitionProbability'] > edge_filter_threshold
242
+ or e.get('isBestPath', False))]
243
+
244
+ for edge in relevant_edges:
245
+ self.draw_edge(ax, edge)
246
+
247
+ # Draw nodes
248
+ for node_id in nodes_to_draw:
249
+ self.draw_node(ax, node_id)
250
+
251
+ # Add legend
252
+ legend_elements = [
253
+ patches.Patch(facecolor=TYPE_COLORS['TACK'], alpha=0.6, label='TACK'),
254
+ patches.Patch(facecolor=TYPE_COLORS['JIBE'], alpha=0.6, label='JIBE'),
255
+ patches.Patch(facecolor=TYPE_COLORS['HEAD_UP'], alpha=0.6, label='HEAD_UP'),
256
+ patches.Patch(facecolor=TYPE_COLORS['BEAR_AWAY'], alpha=0.6, label='BEAR_AWAY'),
257
+ patches.Patch(facecolor='red', alpha=0.6, label='Best Path'),
258
+ ]
259
+ ax.legend(handles=legend_elements, loc='upper right', fontsize=8)
260
+
261
+ # Set axis limits
262
+ margin = 2
263
+ ax.set_xlim(min(x_coords) - margin, max(x_coords) + margin)
264
+ ax.set_ylim(min(y_coords) - margin, max(y_coords) + margin)
265
+
266
+ ax.set_aspect('equal')
267
+ ax.axis('off')
268
+
269
+ plt.title(f'MST Maneuver Graph ({len(nodes_to_draw)} nodes)', fontsize=14)
270
+ plt.tight_layout()
271
+
272
+ if output_file:
273
+ plt.savefig(output_file, dpi=150, bbox_inches='tight',
274
+ facecolor='white', edgecolor='none')
275
+ print(f"Saved visualization to {output_file}")
276
+ else:
277
+ plt.show()
278
+
279
+ plt.close()
280
+
281
+ def visualize_subtree(self, root_id, depth_limit=5, output_file=None):
282
+ """Visualize a subtree starting from a specific node."""
283
+ # Collect nodes in subtree
284
+ subtree_nodes = set()
285
+ def collect_subtree(node_id, current_depth):
286
+ if current_depth > depth_limit:
287
+ return
288
+ subtree_nodes.add(node_id)
289
+ node = self.nodes[node_id]
290
+ # Find children by looking at edges
291
+ for edge in self.edges:
292
+ if edge['from'] == node_id:
293
+ collect_subtree(edge['to'], current_depth + 1)
294
+
295
+ collect_subtree(root_id, 0)
296
+
297
+ # Create filtered data
298
+ filtered_data = {
299
+ 'nodes': [n for n in self.data['nodes'] if n['id'] in subtree_nodes],
300
+ 'edges': [e for e in self.edges if e['from'] in subtree_nodes and e['to'] in subtree_nodes],
301
+ 'bestPaths': {k: v for k, v in self.best_paths.items() if int(k) in subtree_nodes}
302
+ }
303
+
304
+ # Create new visualizer for subtree
305
+ subtree_viz = MstGraphVisualizer(filtered_data)
306
+ subtree_viz.visualize(output_file=output_file)
307
+
308
+
309
+def main():
310
+ if len(sys.argv) < 2:
311
+ print("Usage: python mst_graph_visualizer.py <input_json_file> [output_file]")
312
+ print("\nOptions:")
313
+ print(" input_json_file - JSON file exported from MstGraphExporter")
314
+ print(" output_file - Optional output image file (PNG, PDF, SVG)")
315
+ sys.exit(1)
316
+
317
+ input_file = sys.argv[1]
318
+ output_file = sys.argv[2] if len(sys.argv) > 2 else None
319
+
320
+ print(f"Loading graph from {input_file}...")
321
+ with open(input_file, 'r') as f:
322
+ data = json.load(f)
323
+
324
+ print(f"Loaded {len(data['nodes'])} nodes, {len(data['edges'])} edges")
325
+
326
+ visualizer = MstGraphVisualizer(data)
327
+
328
+ # For large graphs, show only first N nodes
329
+ num_nodes = len(data['nodes'])
330
+ if num_nodes > 100:
331
+ print(f"Graph has {num_nodes} nodes, showing first 50 for clarity")
332
+ visualizer.visualize(output_file=output_file, max_nodes=50)
333
+ else:
334
+ visualizer.visualize(output_file=output_file, max_nodes=num_nodes)
335
+
336
+
337
+if __name__ == '__main__':
338
+ main()
java/com.sap.sailing.windestimation.lab/python/mst_graph_visualizer_graphviz.py
... ...
@@ -0,0 +1,554 @@
1
+#!/usr/bin/env python3
2
+"""
3
+MST Maneuver Graph Visualization using Graphviz
4
+
5
+This creates a hierarchical visualization of the MST graph that better handles
6
+large trees. It uses DOT language to create the graph and renders it with graphviz.
7
+
8
+Features:
9
+- Proper tree layout (top to bottom)
10
+- Each node shows 4 compartments as an HTML-like table with PORT attributes
11
+- Edges connect to specific compartments
12
+- Best path highlighted in red
13
+- All other edges shown in green (high prob) or gray (lower prob)
14
+- Edge labels with transition probabilities
15
+- Color-coded compartments by maneuver type
16
+- Legend explaining colors
17
+
18
+Usage:
19
+ python mst_graph_visualizer_graphviz.py <input_json_file> [output_file]
20
+
21
+Dependencies:
22
+ pip install graphviz
23
+
24
+Also requires graphviz to be installed on the system:
25
+ sudo apt-get install graphviz # Ubuntu/Debian
26
+ brew install graphviz # macOS
27
+"""
28
+
29
+import json
30
+import sys
31
+from graphviz import Digraph
32
+
33
+# Maneuver type colors (HTML hex format)
34
+TYPE_COLORS = {
35
+ 'TACK': '#4CAF50', # Green
36
+ 'JIBE': '#2196F3', # Blue
37
+ 'HEAD_UP': '#FF9800', # Orange
38
+ 'BEAR_AWAY': '#9C27B0', # Purple
39
+}
40
+
41
+TYPE_ORDER = ['TACK', 'JIBE', 'HEAD_UP', 'BEAR_AWAY']
42
+TYPE_ABBREV = {'TACK': 'T', 'JIBE': 'J', 'HEAD_UP': 'H', 'BEAR_AWAY': 'B'}
43
+
44
+
45
+def blend_color_with_white(hex_color, factor):
46
+ """Blend a hex color with white based on factor (0=white, 1=full color)."""
47
+ r = int(int(hex_color[1:3], 16) * factor + 255 * (1 - factor))
48
+ g = int(int(hex_color[3:5], 16) * factor + 255 * (1 - factor))
49
+ b = int(int(hex_color[5:7], 16) * factor + 255 * (1 - factor))
50
+ return f"#{min(255, r):02x}{min(255, g):02x}{min(255, b):02x}"
51
+
52
+
53
+def format_wind(comp):
54
+ """Format wind direction string."""
55
+ wind_est = comp.get('windEstimate', comp.get('windRangeFrom', 0))
56
+ wind_width = comp.get('windRangeWidth', 0)
57
+ if wind_width < 1:
58
+ return f"{wind_est:.0f}°"
59
+ else:
60
+ return f"{wind_est:.0f}±{wind_width/2:.0f}°"
61
+
62
+
63
+def format_distance(node):
64
+ """
65
+ Format distance/time to parent in human-readable form.
66
+
67
+ The node may contain:
68
+ - spatialDistanceToParentMeters: actual spatial distance in meters
69
+ - timeDiffToParentSeconds: actual time difference in seconds
70
+ - compoundDistanceToParent: sum of predicted std deviations (legacy, for transition probability)
71
+
72
+ We prefer to show actual spatial distance and time if available.
73
+ """
74
+ spatial_dist = node.get('spatialDistanceToParentMeters')
75
+ time_diff = node.get('timeDiffToParentSeconds')
76
+
77
+ if spatial_dist is not None and time_diff is not None:
78
+ # Format spatial distance
79
+ if spatial_dist >= 1000:
80
+ dist_str = f'{spatial_dist/1000:.1f}km'
81
+ elif spatial_dist >= 1:
82
+ dist_str = f'{spatial_dist:.0f}m'
83
+ else:
84
+ dist_str = f'{spatial_dist:.1f}m'
85
+
86
+ # Format time difference
87
+ if time_diff >= 60:
88
+ time_str = f'{time_diff/60:.1f}min'
89
+ else:
90
+ time_str = f'{time_diff:.0f}s'
91
+
92
+ return f'{dist_str}, {time_str}'
93
+
94
+ # Fallback to compound distance (legacy)
95
+ compound_dist = node.get('compoundDistanceToParent') or node.get('distanceToParent')
96
+ if compound_dist is None or compound_dist == 0:
97
+ return None
98
+ return f'σ={compound_dist:.1f}' # Mark as std sum with σ symbol
99
+
100
+
101
+def create_node_label_with_ports(node, best_type=None):
102
+ """
103
+ Create HTML-like label for a node with 4 compartments.
104
+ Each compartment has a PORT attribute for incoming edges (from above).
105
+ The footer row has additional ports for outgoing edges (going down).
106
+ This prevents edges from crossing through the timestamp/distance section.
107
+ """
108
+ compartments = {c['type']: c for c in node['compartments']}
109
+
110
+ # Extract time from timestamp
111
+ timestamp = node.get('timestamp', '')
112
+ if ' ' in timestamp:
113
+ time_part = timestamp.split(' ')[1][:8] # HH:MM:SS
114
+ else:
115
+ time_part = timestamp[:8] if len(timestamp) >= 8 else timestamp
116
+
117
+ # Get distance/time to parent (pass whole node for full info)
118
+ dist_str = format_distance(node)
119
+
120
+ # Get competitor info
121
+ competitor_name = node.get('competitorName', '')
122
+ # Truncate long names for display
123
+ if competitor_name:
124
+ competitor_str = competitor_name[:12] + '...' if len(competitor_name) > 15 else competitor_name
125
+ else:
126
+ competitor_str = ''
127
+
128
+ # Build HTML table with PORT attributes
129
+ html = '<<TABLE BORDER="1" CELLBORDER="1" CELLSPACING="0" CELLPADDING="4">'
130
+
131
+ # Main row with compartments - ports here are for INCOMING edges (from above)
132
+ html += '<TR>'
133
+ for type_name in TYPE_ORDER:
134
+ comp = compartments.get(type_name)
135
+ if comp is None:
136
+ continue
137
+
138
+ confidence = comp['confidence']
139
+ is_best = (best_type == type_name)
140
+
141
+ # Calculate background color - blend with white based on confidence
142
+ base_color = TYPE_COLORS[type_name]
143
+ if is_best:
144
+ # Best type: full saturation with red border
145
+ bg_color = base_color
146
+ border_attr = f' BGCOLOR="{bg_color}" BORDER="3" COLOR="red"'
147
+ else:
148
+ # Other types: fade based on confidence (min 0.2 to keep some color visible)
149
+ blend_factor = max(0.2, confidence)
150
+ bg_color = blend_color_with_white(base_color, blend_factor)
151
+ border_attr = f' BGCOLOR="{bg_color}"'
152
+
153
+ wind_str = format_wind(comp)
154
+ abbrev = TYPE_ABBREV[type_name]
155
+
156
+ # PORT for incoming edges (named after type)
157
+ port_name = type_name
158
+
159
+ # Create cell content with PORT
160
+ cell_content = (
161
+ f'<B>{abbrev}</B><BR/>'
162
+ f'<FONT POINT-SIZE="9">{confidence:.2f}</FONT><BR/>'
163
+ f'<FONT POINT-SIZE="8">{wind_str}</FONT>'
164
+ )
165
+
166
+ html += f'<TD PORT="{port_name}"{border_attr}>{cell_content}</TD>'
167
+ html += '</TR>'
168
+
169
+ # Footer row with competitor info, timestamp and distance - spans all columns
170
+ footer_parts = []
171
+ if competitor_str:
172
+ footer_parts.append(f'<FONT COLOR="purple"><B>{competitor_str}</B></FONT>')
173
+ footer_parts.append(time_part)
174
+ if dist_str:
175
+ footer_parts.append(f'<FONT COLOR="blue">↑{dist_str}</FONT>')
176
+ footer_content = ' '.join(footer_parts)
177
+ html += f'<TR><TD COLSPAN="4" BGCOLOR="white"><FONT POINT-SIZE="9">{footer_content}</FONT></TD></TR>'
178
+
179
+ # Bottom row with path vote diagnostics AND ports for OUTGOING edges
180
+ # Each cell corresponds to one compartment position for proper horizontal alignment
181
+ path_votes = node.get('pathVotes', {})
182
+ html += '<TR>'
183
+ for type_name in TYPE_ORDER:
184
+ # Port for outgoing edges (named type_out)
185
+ out_port_name = f'{type_name}_out'
186
+
187
+ vote_info = path_votes.get(type_name, {})
188
+ path_count = vote_info.get('pathCount', 0)
189
+ quality_sum = vote_info.get('qualitySum', 0)
190
+ if path_count > 0:
191
+ # Format quality sum in scientific notation if very small
192
+ if quality_sum < 0.001:
193
+ qs_str = f'{quality_sum:.1e}'
194
+ else:
195
+ qs_str = f'{quality_sum:.3f}'
196
+ vote_content = f'<FONT POINT-SIZE="6" COLOR="gray40">{path_count}p/{qs_str}</FONT>'
197
+ else:
198
+ vote_content = '<FONT POINT-SIZE="6" COLOR="gray70">-</FONT>'
199
+ html += f'<TD PORT="{out_port_name}" BGCOLOR="white">{vote_content}</TD>'
200
+ html += '</TR>'
201
+
202
+ html += '</TABLE>>'
203
+
204
+ return html
205
+
206
+def create_legend():
207
+ """Create a legend explaining the colors."""
208
+ html = '<<TABLE BORDER="0" CELLBORDER="1" CELLSPACING="0" CELLPADDING="4">'
209
+ html += '<TR><TD COLSPAN="2" BGCOLOR="white"><B>Legend</B></TD></TR>'
210
+
211
+ # Maneuver type colors
212
+ for type_name in TYPE_ORDER:
213
+ color = TYPE_COLORS[type_name]
214
+ abbrev = TYPE_ABBREV[type_name]
215
+ html += f'<TR><TD BGCOLOR="{color}">{abbrev}</TD><TD BGCOLOR="white">{type_name}</TD></TR>'
216
+
217
+ # Edge colors
218
+ html += '<TR><TD COLSPAN="2" BGCOLOR="white"><B>Edges</B></TD></TR>'
219
+ html += '<TR><TD BGCOLOR="white"><FONT COLOR="red">━━</FONT></TD><TD BGCOLOR="white">Best Path</TD></TR>'
220
+ html += '<TR><TD BGCOLOR="white"><FONT COLOR="darkgreen">━━</FONT></TD><TD BGCOLOR="white">High Prob (&gt;1%)</TD></TR>'
221
+ html += '<TR><TD BGCOLOR="white"><FONT COLOR="gray50">━━</FONT></TD><TD BGCOLOR="white">Medium Prob</TD></TR>'
222
+ html += '<TR><TD BGCOLOR="white"><FONT COLOR="gray80">━━</FONT></TD><TD BGCOLOR="white">Low Prob</TD></TR>'
223
+
224
+ html += '</TABLE>>'
225
+ return html
226
+
227
+
228
+def visualize_mst_graph(data, output_file=None, max_nodes=100, min_edge_prob=0.0):
229
+ """
230
+ Create graphviz visualization of MST graph with edges connecting to specific compartments.
231
+
232
+ Args:
233
+ data: Parsed JSON data from MstGraphExporter
234
+ output_file: Output file path (without extension; .pdf/.png will be added)
235
+ max_nodes: Maximum number of nodes to show
236
+ min_edge_prob: Minimum edge probability to display (0 = show all)
237
+ """
238
+ nodes = {n['id']: n for n in data['nodes']}
239
+ edges = data['edges']
240
+ best_paths = data.get('bestPaths', {})
241
+
242
+ # Create directed graph
243
+ dot = Digraph(comment='MST Maneuver Graph')
244
+ dot.attr(rankdir='TB') # Top to bottom
245
+ dot.attr('node', shape='plaintext') # Use HTML labels
246
+ dot.attr(splines='polyline') # Use polyline for clearer edge routing
247
+ dot.attr(nodesep='0.5') # Horizontal spacing between nodes
248
+ dot.attr(ranksep='1.0') # Vertical spacing between ranks
249
+
250
+ # Limit nodes
251
+ nodes_to_draw = list(nodes.keys())[:max_nodes]
252
+ nodes_set = set(nodes_to_draw)
253
+
254
+ # Add legend
255
+ dot.node('legend', create_legend())
256
+ dot.node('legend_spacer', '', shape='none', width='0', height='0')
257
+
258
+ # Add nodes
259
+ for node_id in nodes_to_draw:
260
+ node = nodes[node_id]
261
+ best_type = best_paths.get(str(node_id))
262
+ label = create_node_label_with_ports(node, best_type)
263
+ dot.node(str(node_id), label)
264
+
265
+ # Process ALL edges
266
+ for edge in edges:
267
+ from_id = edge['from']
268
+ to_id = edge['to']
269
+
270
+ if from_id not in nodes_set or to_id not in nodes_set:
271
+ continue
272
+
273
+ is_best = edge.get('isBestPath', False)
274
+ trans_prob = edge['transitionProbability']
275
+ from_type = edge['fromType']
276
+ to_type = edge['toType']
277
+
278
+ # Skip very low probability edges unless they're best path
279
+ if trans_prob < min_edge_prob and not is_best:
280
+ continue
281
+
282
+ # Style based on best path and probability
283
+ if is_best:
284
+ color = 'red'
285
+ penwidth = '2.5'
286
+ style = 'bold'
287
+ fontcolor = 'red'
288
+ elif trans_prob > 0.01:
289
+ color = 'darkgreen'
290
+ penwidth = '1.2'
291
+ style = 'solid'
292
+ fontcolor = 'darkgreen'
293
+ elif trans_prob > 0.001:
294
+ color = 'gray50'
295
+ penwidth = '0.8'
296
+ style = 'solid'
297
+ fontcolor = 'gray50'
298
+ else:
299
+ color = 'gray80'
300
+ penwidth = '0.5'
301
+ style = 'dashed'
302
+ fontcolor = 'gray70'
303
+
304
+ # Format probability label
305
+ if trans_prob >= 0.01:
306
+ prob_str = f'{trans_prob:.2f}'
307
+ elif trans_prob >= 0.001:
308
+ prob_str = f'{trans_prob:.3f}'
309
+ else:
310
+ prob_str = f'{trans_prob:.1e}'
311
+
312
+ # Build edge label with type transition and probability
313
+ from_abbrev = TYPE_ABBREV[from_type]
314
+ to_abbrev = TYPE_ABBREV[to_type]
315
+ edge_label = f'{from_abbrev}→{to_abbrev}\\n{prob_str}'
316
+
317
+ # Use PORT to connect to specific compartments
318
+ # Outgoing edges use the _out ports in the footer row (below timestamp)
319
+ # Incoming edges use the compartment ports (top of compartment cells)
320
+ from_port = f'{from_id}:{from_type}_out:s' # :s = south (bottom) of footer cell
321
+ to_port = f'{to_id}:{to_type}:n' # :n = north (top) of compartment cell
322
+
323
+ dot.edge(from_port, to_port,
324
+ label=edge_label,
325
+ color=color,
326
+ penwidth=penwidth,
327
+ style=style,
328
+ fontsize='7',
329
+ fontcolor=fontcolor,
330
+ arrowsize='0.6')
331
+
332
+ # Render
333
+ if output_file:
334
+ # Determine format from extension
335
+ if '.' in output_file:
336
+ base, fmt = output_file.rsplit('.', 1)
337
+ else:
338
+ base = output_file
339
+ fmt = 'pdf'
340
+
341
+ dot.render(base, format=fmt, cleanup=True)
342
+ print(f"Saved visualization to {base}.{fmt}")
343
+ else:
344
+ # Try to display
345
+ dot.view()
346
+
347
+ return dot
348
+
349
+
350
+def create_detailed_edge_graph(data, output_file=None, max_depth=10, min_edge_prob=0.0):
351
+ """
352
+ Create a more detailed graph showing all compartment-to-compartment edges.
353
+ Each node compartment becomes its own graphviz node, grouped by cluster.
354
+
355
+ This is useful for small subtrees to see the full inner graph structure.
356
+ """
357
+ nodes = {n['id']: n for n in data['nodes']}
358
+ edges = data['edges']
359
+ best_paths = data.get('bestPaths', {})
360
+
361
+ # Filter to first N levels
362
+ nodes_to_draw = [n for n in data['nodes'] if n['depth'] <= max_depth]
363
+ nodes_set = {n['id'] for n in nodes_to_draw}
364
+
365
+ dot = Digraph(comment='MST Detailed Inner Graph')
366
+ dot.attr(rankdir='TB')
367
+ dot.attr(nodesep='0.3')
368
+ dot.attr(ranksep='0.8')
369
+
370
+ # Add legend
371
+ dot.node('legend', create_legend())
372
+
373
+ # Create subgraph for each tree node (to keep compartments together)
374
+ for node in nodes_to_draw:
375
+ node_id = node['id']
376
+ best_type = best_paths.get(str(node_id))
377
+
378
+ # Extract time from timestamp for label
379
+ timestamp = node.get('timestamp', '')
380
+ if ' ' in timestamp:
381
+ time_part = timestamp.split(' ')[1][:8]
382
+ else:
383
+ time_part = str(node_id)
384
+
385
+ # Get distance/time to parent (pass whole node)
386
+ dist_str = format_distance(node)
387
+
388
+ # Build cluster label with time and distance
389
+ if dist_str:
390
+ cluster_label = f'{time_part} ↑{dist_str}'
391
+ else:
392
+ cluster_label = time_part
393
+
394
+ with dot.subgraph(name=f'cluster_{node_id}') as c:
395
+ c.attr(label=cluster_label)
396
+ c.attr(style='rounded,filled')
397
+ c.attr(fillcolor='white')
398
+
399
+ for comp in node['compartments']:
400
+ type_name = comp['type']
401
+ comp_id = f"{node_id}_{type_name}"
402
+
403
+ is_best = (best_type == type_name)
404
+ base_color = TYPE_COLORS[type_name]
405
+ confidence = comp['confidence']
406
+
407
+ if is_best:
408
+ fillcolor = base_color
409
+ fontcolor = 'white'
410
+ penwidth = '3'
411
+ pencolor = 'red'
412
+ else:
413
+ # Blend based on confidence
414
+ blend_factor = max(0.3, confidence)
415
+ fillcolor = blend_color_with_white(base_color, blend_factor)
416
+ fontcolor = 'black'
417
+ penwidth = '1'
418
+ pencolor = 'black'
419
+
420
+ label = f"{TYPE_ABBREV[type_name]}\\n{confidence:.2f}\\n{format_wind(comp)}"
421
+
422
+ c.node(comp_id, label,
423
+ shape='box',
424
+ style='filled',
425
+ fillcolor=fillcolor,
426
+ fontcolor=fontcolor,
427
+ fontsize='9',
428
+ penwidth=penwidth,
429
+ color=pencolor)
430
+
431
+ # Add ALL edges between compartments
432
+ for edge in edges:
433
+ from_id = edge['from']
434
+ to_id = edge['to']
435
+
436
+ if from_id not in nodes_set or to_id not in nodes_set:
437
+ continue
438
+
439
+ from_comp_id = f"{from_id}_{edge['fromType']}"
440
+ to_comp_id = f"{to_id}_{edge['toType']}"
441
+
442
+ is_best = edge.get('isBestPath', False)
443
+ trans_prob = edge['transitionProbability']
444
+
445
+ # Skip very low probability edges unless best path
446
+ if trans_prob < min_edge_prob and not is_best:
447
+ continue
448
+
449
+ if is_best:
450
+ color = 'red'
451
+ penwidth = '2.5'
452
+ fontcolor = 'red'
453
+ elif trans_prob > 0.01:
454
+ color = 'darkgreen'
455
+ penwidth = '1.2'
456
+ fontcolor = 'darkgreen'
457
+ elif trans_prob > 0.001:
458
+ color = 'gray50'
459
+ penwidth = '0.8'
460
+ fontcolor = 'gray50'
461
+ else:
462
+ color = 'gray80'
463
+ penwidth = '0.4'
464
+ fontcolor = 'gray70'
465
+
466
+ # Format probability label
467
+ if trans_prob >= 0.01:
468
+ prob_str = f'{trans_prob:.2f}'
469
+ elif trans_prob >= 0.001:
470
+ prob_str = f'{trans_prob:.3f}'
471
+ else:
472
+ prob_str = f'{trans_prob:.1e}'
473
+
474
+ # Build edge label with type transition and probability
475
+ from_abbrev = TYPE_ABBREV[edge['fromType']]
476
+ to_abbrev = TYPE_ABBREV[edge['toType']]
477
+ edge_label = f'{from_abbrev}→{to_abbrev}\\n{prob_str}'
478
+
479
+ dot.edge(from_comp_id, to_comp_id,
480
+ label=edge_label,
481
+ color=color,
482
+ penwidth=penwidth,
483
+ fontsize='7',
484
+ fontcolor=fontcolor,
485
+ arrowsize='0.5')
486
+
487
+ if output_file:
488
+ if '.' in output_file:
489
+ base, fmt = output_file.rsplit('.', 1)
490
+ else:
491
+ base = output_file
492
+ fmt = 'pdf'
493
+ dot.render(base, format=fmt, cleanup=True)
494
+ print(f"Saved detailed visualization to {base}.{fmt}")
495
+
496
+ return dot
497
+
498
+
499
+def main():
500
+ if len(sys.argv) < 2:
501
+ print("Usage: python mst_graph_visualizer_graphviz.py <input_json_file> [output_file] [options]")
502
+ print("\nOptions:")
503
+ print(" input_json_file - JSON file exported from MstGraphExporter")
504
+ print(" output_file - Output file (extension determines format: .pdf, .png, .svg)")
505
+ print(" --detailed - Create detailed compartment-level graph (for small graphs)")
506
+ print(" --min-prob=<value> - Minimum edge probability to show (default: 0 = show all)")
507
+ print(" --max-nodes=<n> - Maximum number of nodes to display (default: 100)")
508
+ print("\nCompartment Colors:")
509
+ print(" T (TACK) - Green")
510
+ print(" J (JIBE) - Blue")
511
+ print(" H (HEAD_UP) - Orange")
512
+ print(" B (BEAR_AWAY) - Purple")
513
+ print("\nEdge Colors:")
514
+ print(" Red - Best path (selected by algorithm)")
515
+ print(" Dark Green - High probability (>1%)")
516
+ print(" Gray - Medium/low probability")
517
+ sys.exit(1)
518
+
519
+ input_file = sys.argv[1]
520
+ output_file = None
521
+ detailed = False
522
+ min_prob = 0.0
523
+ max_nodes = 100
524
+
525
+ # Parse arguments
526
+ for arg in sys.argv[2:]:
527
+ if arg == '--detailed':
528
+ detailed = True
529
+ elif arg.startswith('--min-prob='):
530
+ min_prob = float(arg.split('=')[1])
531
+ elif arg.startswith('--max-nodes='):
532
+ max_nodes = int(arg.split('=')[1])
533
+ elif not arg.startswith('--'):
534
+ output_file = arg
535
+
536
+ print(f"Loading graph from {input_file}...")
537
+ with open(input_file, 'r') as f:
538
+ data = json.load(f)
539
+
540
+ num_nodes = len(data['nodes'])
541
+ num_edges = len(data['edges'])
542
+ print(f"Loaded {num_nodes} nodes, {num_edges} edges")
543
+ print(f"Minimum edge probability: {min_prob}")
544
+
545
+ if detailed:
546
+ print("Creating detailed compartment-level visualization...")
547
+ create_detailed_edge_graph(data, output_file, max_depth=10, min_edge_prob=min_prob)
548
+ else:
549
+ print(f"Creating tree visualization (max {max_nodes} nodes)...")
550
+ visualize_mst_graph(data, output_file, max_nodes=max_nodes, min_edge_prob=min_prob)
551
+
552
+
553
+if __name__ == '__main__':
554
+ main()
java/com.sap.sailing.windestimation.lab/src/com/sap/sailing/windestimation/data/LabeledManeuverForEstimation.java
... ...
@@ -21,12 +21,12 @@ public class LabeledManeuverForEstimation extends ManeuverForEstimation {
21 21
double lowestSpeedVsExitingSpeedRatio, boolean clean, ManeuverCategory maneuverCategory,
22 22
double scaledSpeedBefore, double scaledSpeedAfter, boolean markPassing, BoatClass boatClass,
23 23
boolean markPassingDataAvailable, ManeuverTypeForClassification maneuverType, Wind wind,
24
- String regattaName) {
24
+ String regattaName, String competitorName) {
25 25
super(maneuverTimePoint, maneuverPosition, middleCourse, speedWithBearingBefore, speedWithBearingAfter,
26 26
courseChangeInDegrees, courseChangeWithinMainCurveInDegrees, maxTurningRateInDegreesPerSecond,
27 27
deviationFromOptimalTackAngleInDegrees, deviationFromOptimalJibeAngleInDegrees, speedLossRatio,
28 28
speedGainRatio, lowestSpeedVsExitingSpeedRatio, clean, maneuverCategory, scaledSpeedBefore,
29
- scaledSpeedAfter, markPassing, boatClass, markPassingDataAvailable);
29
+ scaledSpeedAfter, markPassing, boatClass, markPassingDataAvailable, competitorName);
30 30
this.maneuverType = maneuverType;
31 31
this.wind = wind;
32 32
this.regattaName = regattaName;
java/com.sap.sailing.windestimation.lab/src/com/sap/sailing/windestimation/data/serialization/LabeledManeuverForEstimationJsonDeserializer.java
... ...
@@ -62,6 +62,7 @@ public class LabeledManeuverForEstimationJsonDeserializer implements JsonDeseria
62 62
Double windSpeedInKnots = (Double) object.get(LabeledManeuverForEstimationJsonSerializer.WIND_SPEED);
63 63
Double windCourse = (Double) object.get(LabeledManeuverForEstimationJsonSerializer.WIND_COURSE);
64 64
String regattaName = (String) object.get(LabeledManeuverForEstimationJsonSerializer.REGATTA_NAME);
65
+ String competitorName = (String) object.get(LabeledManeuverForEstimationJsonSerializer.COMPETITOR_NAME);
65 66
MillisecondsTimePoint maneuverTimePoint = new MillisecondsTimePoint(maneuverTimePointMillis);
66 67
DegreePosition maneuverPosition = new DegreePosition(positionLatitude, positionLongitude);
67 68
LabeledManeuverForEstimation maneuver = new LabeledManeuverForEstimation(maneuverTimePoint, maneuverPosition,
... ...
@@ -74,7 +75,7 @@ public class LabeledManeuverForEstimationJsonDeserializer implements JsonDeseria
74 75
markPassingDataAvailable, maneuverType,
75 76
new WindImpl(maneuverPosition, maneuverTimePoint,
76 77
new KnotSpeedWithBearingImpl(windSpeedInKnots, new DegreeBearingImpl(windCourse))),
77
- regattaName);
78
+ regattaName, competitorName);
78 79
return maneuver;
79 80
}
80 81
java/com.sap.sailing.windestimation.lab/src/com/sap/sailing/windestimation/data/serialization/LabeledManeuverForEstimationJsonSerializer.java
... ...
@@ -36,6 +36,7 @@ public class LabeledManeuverForEstimationJsonSerializer implements JsonSerialize
36 36
public static final String BOAT_CLASS = "boatClass";
37 37
public static final String MARK_PASSING_DATA_AVAILABLE = "markPassingDataAvailable";
38 38
public static final String REGATTA_NAME = "regattaName";
39
+ public static final String COMPETITOR_NAME = "competitorName";
39 40
40 41
private final BoatClassJsonSerializer boatClassSerializer = new DetailedBoatClassJsonSerializer();
41 42
... ...
@@ -69,6 +70,7 @@ public class LabeledManeuverForEstimationJsonSerializer implements JsonSerialize
69 70
json.put(WIND_SPEED, maneuver.getWind().getKnots());
70 71
json.put(WIND_COURSE, maneuver.getWind().getBearing().getDegrees());
71 72
json.put(REGATTA_NAME, maneuver.getRegattaName());
73
+ json.put(COMPETITOR_NAME, maneuver.getCompetitorName());
72 74
return json;
73 75
}
74 76
java/com.sap.sailing.windestimation.lab/src/com/sap/sailing/windestimation/data/transformer/CompleteManeuverCurveWithEstimationDataToLabelledManeuverForEstimationTransformer.java
... ...
@@ -18,7 +18,7 @@ public class CompleteManeuverCurveWithEstimationDataToLabelledManeuverForEstimat
18 18
.getConvertableManeuvers(competitorTrackWithElementsToTransform.getElements());
19 19
return internalTransformer.getManeuversForEstimation(convertableManeuvers,
20 20
competitorTrackWithElementsToTransform.getBoatClass(),
21
- competitorTrackWithElementsToTransform.getRegattaName());
21
+ competitorTrackWithElementsToTransform.getRegattaName(), competitorTrackWithElementsToTransform.getCompetitorName());
22 22
}
23 23
24 24
}
java/com.sap.sailing.windestimation.lab/src/com/sap/sailing/windestimation/data/transformer/LabeledManeuverForEstimationTransformer.java
... ...
@@ -18,7 +18,7 @@ public class LabeledManeuverForEstimationTransformer implements
18 18
public LabeledManeuverForEstimation getManeuverForEstimation(ConvertableToLabeledManeuverForEstimation maneuver,
19 19
ConvertableToLabeledManeuverForEstimation previousManeuver,
20 20
ConvertableToLabeledManeuverForEstimation nextManeuver, double speedScalingDivisor, BoatClass boatClass,
21
- String regattaName) {
21
+ String regattaName, String competitorName) {
22 22
ManeuverForEstimation maneuverForEstimation = internalTransformer.getManeuverForEstimation(maneuver,
23 23
speedScalingDivisor, boatClass);
24 24
ManeuverTypeForClassification maneuverType = getManeuverTypeForClassification(maneuver);
... ...
@@ -35,7 +35,7 @@ public class LabeledManeuverForEstimationTransformer implements
35 35
maneuverForEstimation.getManeuverCategory(), maneuverForEstimation.getScaledSpeedBefore(),
36 36
maneuverForEstimation.getScaledSpeedAfter(), maneuverForEstimation.isMarkPassing(),
37 37
maneuverForEstimation.getBoatClass(), maneuverForEstimation.isMarkPassingDataAvailable(), maneuverType,
38
- maneuver.getWind(), regattaName);
38
+ maneuver.getWind(), regattaName, competitorName);
39 39
return labelledManeuverForEstimation;
40 40
}
41 41
... ...
@@ -60,7 +60,7 @@ public class LabeledManeuverForEstimationTransformer implements
60 60
61 61
public List<LabeledManeuverForEstimation> getManeuversForEstimation(
62 62
List<ConvertableToLabeledManeuverForEstimation> convertableManeuvers, BoatClass boatClass,
63
- String regattaName) {
63
+ String regattaName, String competitorName) {
64 64
double speedScalingDivisor = internalTransformer.getSpeedScalingDivisor(convertableManeuvers);
65 65
List<LabeledManeuverForEstimation> maneuversForEstimation = new ArrayList<>();
66 66
ConvertableToLabeledManeuverForEstimation previousManeuver = null;
... ...
@@ -68,7 +68,7 @@ public class LabeledManeuverForEstimationTransformer implements
68 68
for (ConvertableToLabeledManeuverForEstimation nextManeuver : convertableManeuvers) {
69 69
if (maneuver != null) {
70 70
LabeledManeuverForEstimation maneuverForEstimation = getManeuverForEstimation(maneuver,
71
- previousManeuver, nextManeuver, speedScalingDivisor, boatClass, regattaName);
71
+ previousManeuver, nextManeuver, speedScalingDivisor, boatClass, regattaName, competitorName);
72 72
if (maneuverForEstimation != null) {
73 73
maneuversForEstimation.add(maneuverForEstimation);
74 74
}
... ...
@@ -78,7 +78,7 @@ public class LabeledManeuverForEstimationTransformer implements
78 78
}
79 79
if (maneuver != null) {
80 80
LabeledManeuverForEstimation maneuverForEstimation = getManeuverForEstimation(maneuver, previousManeuver,
81
- null, speedScalingDivisor, boatClass, regattaName);
81
+ null, speedScalingDivisor, boatClass, regattaName, competitorName);
82 82
if (maneuverForEstimation != null) {
83 83
maneuversForEstimation.add(maneuverForEstimation);
84 84
}
... ...
@@ -91,7 +91,7 @@ public class LabeledManeuverForEstimationTransformer implements
91 91
CompetitorTrackWithEstimationData<ConvertableToLabeledManeuverForEstimation> competitorTrackWithElementsToTransform) {
92 92
return getManeuversForEstimation(competitorTrackWithElementsToTransform.getElements(),
93 93
competitorTrackWithElementsToTransform.getBoatClass(),
94
- competitorTrackWithElementsToTransform.getRegattaName());
94
+ competitorTrackWithElementsToTransform.getRegattaName(), competitorTrackWithElementsToTransform.getCompetitorName());
95 95
}
96 96
97 97
}
java/com.sap.sailing.windestimation.test/src/com/sap/sailing/windestimation/integration/IncrementalMstHmmWindEstimationForTrackedRaceTest.java
... ...
@@ -7,6 +7,8 @@ import java.io.File;
7 7
import java.io.IOException;
8 8
import java.net.URI;
9 9
import java.net.URL;
10
+import java.nio.charset.StandardCharsets;
11
+import java.security.MessageDigest;
10 12
import java.text.SimpleDateFormat;
11 13
import java.util.ArrayList;
12 14
import java.util.Collections;
... ...
@@ -131,13 +133,37 @@ public class IncrementalMstHmmWindEstimationForTrackedRaceTest extends OnlineTra
131 133
new URL("file:///" + new File("resources/event_20110609_KielerWoch-505_Race_2.txt").getCanonicalPath()),
132 134
/* liveUri */ null, /* storedUri */ storedUri,
133 135
new ReceiverType[] { ReceiverType.MARKPASSINGS, ReceiverType.RACECOURSE, ReceiverType.RAWPOSITIONS, ReceiverType.MARKPOSITIONS });
134
- final Optional<String> polardataBearerToken = Optional.ofNullable(Optional.ofNullable(System.getProperty("polardata.source.bearertoken")).orElse(System.getenv("POLAR_DATA_BEARER_TOKEN")));
135
- if (polardataBearerToken.isPresent()) {
136
+ String polarDataBearerToken = System.getProperty("polardata.source.bearertoken");
137
+ if (polarDataBearerToken == null) {
138
+ logger.info("Couldn't find polardata.source.bearertoken system property, trying environment variable POLAR_DATA_BEARER_TOKEN");
139
+ polarDataBearerToken = System.getenv("POLAR_DATA_BEARER_TOKEN");
140
+ if (polarDataBearerToken == null) {
141
+ logger.warning("Couldn't find POLAR_DATA_BEARER_TOKEN environment variable either, polar data service will not be available");
142
+ } else {
143
+ final byte[] digest = MessageDigest.getInstance("SHA-256").digest(polarDataBearerToken.getBytes(StandardCharsets.UTF_8));
144
+ final StringBuilder hexString = new StringBuilder();
145
+ for (byte b : digest) {
146
+ String hex = Integer.toHexString(0xff & b);
147
+ if (hex.length() == 1) {
148
+ hexString.append('0');
149
+ }
150
+ hexString.append(hex);
151
+ }
152
+ logger.info("Found POLAR_DATA_BEARER_TOKEN environment variable, length "+polarDataBearerToken.length()
153
+ +", SHA256 hash "+hexString.toString()
154
+ +"; polar data service will be available");
155
+ }
156
+ } else {
157
+ logger.info("Found polardata.source.bearertoken system property, polar data service will be available");
158
+ }
159
+ final Optional<String> polardataBearerTokenOptional = Optional.ofNullable(polarDataBearerToken);
160
+ if (polardataBearerTokenOptional.isPresent()) {
136 161
polarDataService = new PolarDataServiceImpl();
137 162
final com.sap.sailing.domain.tractracadapter.DomainFactory domainFactoryImpl = getDomainFactory();
138 163
final DomainFactory baseDomainFactory = domainFactoryImpl.getBaseDomainFactory();
139 164
polarDataService.registerDomainFactory(baseDomainFactory);
140
- new PolarDataClient(Optional.ofNullable(System.getenv("POLAR_DATA_BASE_URL")).orElse("https://sapsailing.com"), polarDataService, polardataBearerToken).updatePolarDataRegressions();
165
+ new PolarDataClient(Optional.ofNullable(System.getenv("POLAR_DATA_BASE_URL")).orElse("https://sapsailing.com"), polarDataService, polardataBearerTokenOptional)
166
+ .updatePolarDataRegressions();
141 167
} else {
142 168
polarDataService = new PolarDataServiceImpl();
143 169
}
java/com.sap.sailing.windestimation.test/src/com/sap/sailing/windestimation/integration/IncrementalMstManeuverGraphGeneratorTest.java
... ...
@@ -8,6 +8,9 @@ import java.net.MalformedURLException;
8 8
import java.net.URI;
9 9
import java.net.URISyntaxException;
10 10
import java.net.URL;
11
+import java.nio.charset.StandardCharsets;
12
+import java.security.MessageDigest;
13
+import java.security.NoSuchAlgorithmException;
11 14
import java.text.SimpleDateFormat;
12 15
import java.util.ArrayList;
13 16
import java.util.GregorianCalendar;
... ...
@@ -16,6 +19,7 @@ import java.util.Optional;
16 19
import java.util.Set;
17 20
import java.util.TimeZone;
18 21
import java.util.TreeSet;
22
+import java.util.logging.Logger;
19 23
20 24
import org.json.simple.parser.ParseException;
21 25
import org.junit.jupiter.api.BeforeEach;
... ...
@@ -54,6 +58,7 @@ import com.sap.sse.common.impl.MillisecondsTimePoint;
54 58
*
55 59
*/
56 60
public class IncrementalMstManeuverGraphGeneratorTest extends OnlineTracTracBasedTest {
61
+ private static final Logger logger = Logger.getLogger(IncrementalMstManeuverGraphGeneratorTest.class.getName());
57 62
58 63
protected final SimpleDateFormat dateFormat;
59 64
private ClassPathReadOnlyModelStoreImpl modelStore;
... ...
@@ -81,7 +86,7 @@ public class IncrementalMstManeuverGraphGeneratorTest extends OnlineTracTracBase
81 86
}
82 87
83 88
@Test
84
- public void testIncrementalMstManeuverGraphGenerator() throws ClassNotFoundException, IOException, ParseException, InterruptedException {
89
+ public void testIncrementalMstManeuverGraphGenerator() throws ClassNotFoundException, IOException, ParseException, InterruptedException, NoSuchAlgorithmException {
85 90
final GaussianBasedTwdTransitionDistributionCache gaussianBasedTwdTransitionDistributionCache = new GaussianBasedTwdTransitionDistributionCache(
86 91
modelStore, /* preload all models */ false, Long.MAX_VALUE);
87 92
final DistanceAndDurationAwareWindTransitionProbabilitiesCalculator transitionProbabilitiesCalculator = new DistanceAndDurationAwareWindTransitionProbabilitiesCalculator(
... ...
@@ -92,13 +97,38 @@ public class IncrementalMstManeuverGraphGeneratorTest extends OnlineTracTracBase
92 97
"Wind estimation models are empty");
93 98
final DynamicTrackedRaceImpl trackedRace = getTrackedRace();
94 99
final ReplicablePolarService polarDataService;
95
- final Optional<String> polardataBearerToken = Optional.ofNullable(Optional.ofNullable(System.getProperty("polardata.source.bearertoken")).orElse(System.getenv("POLAR_DATA_BEARER_TOKEN")));
96
- if (polardataBearerToken.isPresent()) {
100
+ String polarDataBearerToken = System.getProperty("polardata.source.bearertoken");
101
+ if (polarDataBearerToken == null) {
102
+ logger.info("Couldn't find polardata.source.bearertoken system property, trying environment variable POLAR_DATA_BEARER_TOKEN");
103
+ polarDataBearerToken = System.getenv("POLAR_DATA_BEARER_TOKEN");
104
+ if (polarDataBearerToken == null) {
105
+ logger.warning("Couldn't find POLAR_DATA_BEARER_TOKEN environment variable either, polar data service will not be available");
106
+ } else {
107
+ final byte[] digest = MessageDigest.getInstance("SHA-256").digest(polarDataBearerToken.getBytes(StandardCharsets.UTF_8));
108
+ final StringBuilder hexString = new StringBuilder();
109
+ for (byte b : digest) {
110
+ String hex = Integer.toHexString(0xff & b);
111
+ if (hex.length() == 1) {
112
+ hexString.append('0');
113
+ }
114
+ hexString.append(hex);
115
+ }
116
+ logger.info("Found POLAR_DATA_BEARER_TOKEN environment variable, length "+polarDataBearerToken.length()
117
+ +", SHA256 hash "+hexString.toString()
118
+ +"; polar data service will be available");
119
+ }
120
+ } else {
121
+ logger.info("Found polardata.source.bearertoken system property, polar data service will be available");
122
+ }
123
+ final Optional<String> polardataBearerTokenOptional = Optional.ofNullable(polarDataBearerToken);
124
+ if (polardataBearerTokenOptional.isPresent()) {
97 125
polarDataService = new PolarDataServiceImpl();
98 126
final com.sap.sailing.domain.tractracadapter.DomainFactory domainFactoryImpl = getDomainFactory();
99 127
final DomainFactory baseDomainFactory = domainFactoryImpl.getBaseDomainFactory();
100 128
polarDataService.registerDomainFactory(baseDomainFactory);
101
- new PolarDataClient(Optional.ofNullable(System.getenv("POLAR_DATA_BASE_URL")).orElse("https://sapsailing.com"), polarDataService, polardataBearerToken).updatePolarDataRegressions();
129
+ new PolarDataClient(
130
+ Optional.ofNullable(System.getenv("POLAR_DATA_BASE_URL")).orElse("https://sapsailing.com"),
131
+ polarDataService, polardataBearerTokenOptional).updatePolarDataRegressions();
102 132
} else {
103 133
polarDataService = null;
104 134
}
java/com.sap.sailing.windestimation/src/com/sap/sailing/windestimation/aggregator/graph/DijkstraShortestPathFinderImpl.java
... ...
@@ -92,7 +92,6 @@ public class DijkstraShortestPathFinderImpl<T extends ElementWithQuality> implem
92 92
result.append("\n");
93 93
predecessor = nodeOnShortestPath;
94 94
}
95
-
96 95
return result.toString();
97 96
}
98 97
java/com.sap.sailing.windestimation/src/com/sap/sailing/windestimation/aggregator/hmm/WindCourseRange.java
... ...
@@ -147,10 +147,7 @@ public class WindCourseRange {
147 147
}
148 148
double deviationFromPortsideTowardStarboardInDegrees = deviationFromPortsideBoundaryTowardStarboard
149 149
- angleTowardStarboard;
150
- if (deviationFromPortsideTowardStarboardInDegrees <= 0) {
151
- return true;
152
- }
153
- return false;
150
+ return deviationFromPortsideTowardStarboardInDegrees <= 0;
154 151
}
155 152
156 153
@Override
java/com.sap.sailing.windestimation/src/com/sap/sailing/windestimation/aggregator/msthmm/MstGraphExportHelper.java
... ...
@@ -0,0 +1,84 @@
1
+package com.sap.sailing.windestimation.aggregator.msthmm;
2
+
3
+import java.io.FileWriter;
4
+import java.io.IOException;
5
+import java.io.StringWriter;
6
+import java.io.Writer;
7
+import java.util.logging.Logger;
8
+
9
+import com.sap.sailing.windestimation.aggregator.msthmm.MstManeuverGraphGenerator.MstManeuverGraphComponents;
10
+
11
+/**
12
+ * Example usage of MstGraphExporter for debugging and visualization.
13
+ *
14
+ * <p>This class can be used to export the MST graph to JSON format, which can then
15
+ * be visualized using the Python script mst_graph_visualizer.py.</p>
16
+ *
17
+ * <p>Example usage in a test or debug context:</p>
18
+ * <pre>
19
+ * // After building your MST graph:
20
+ * MstManeuverGraphComponents graphComponents = mstManeuverGraphGenerator.parseGraph();
21
+ *
22
+ * // Export to JSON file:
23
+ * MstGraphExportHelper.exportToFile(graphComponents, transitionProbabilitiesCalculator,
24
+ * "/tmp/mst_graph.json");
25
+ *
26
+ * // Then run from command line:
27
+ * // python mst_graph_visualizer.py /tmp/mst_graph.json /tmp/mst_graph.png
28
+ * </pre>
29
+ *
30
+ * @author Generated for visualization purposes using Claude Opus 4.5
31
+ */
32
+public class MstGraphExportHelper {
33
+ private static final Logger logger = Logger.getLogger(MstGraphExportHelper.class.getName());
34
+
35
+ /**
36
+ * Exports the MST graph to a JSON file.
37
+ *
38
+ * @param graphComponents The MST graph components to export
39
+ * @param transitionProbabilitiesCalculator Calculator for edge probabilities
40
+ * @param filePath Path to write the JSON file
41
+ * @throws IOException if writing fails
42
+ */
43
+ public static void exportToFile(MstManeuverGraphComponents graphComponents,
44
+ MstGraphNodeTransitionProbabilitiesCalculator transitionProbabilitiesCalculator,
45
+ String filePath) throws IOException {
46
+ final MstGraphExporter exporter = new MstGraphExporter(transitionProbabilitiesCalculator);
47
+ try (final FileWriter writer = new FileWriter(filePath)) {
48
+ exporter.exportToJson(graphComponents, writer);
49
+ }
50
+ logger.info("Exported MST graph to: " + filePath);
51
+ logger.info("Visualize with: python mst_graph_visualizer.py " + filePath + " output.png");
52
+ }
53
+
54
+ /**
55
+ * Exports the MST graph to a JSON string (useful for testing/debugging).
56
+ *
57
+ * @param graphComponents The MST graph components to export
58
+ * @param transitionProbabilitiesCalculator Calculator for edge probabilities
59
+ * @return JSON string representation of the graph
60
+ * @throws IOException if writing fails
61
+ */
62
+ public static String exportToString(MstManeuverGraphComponents graphComponents,
63
+ MstGraphNodeTransitionProbabilitiesCalculator transitionProbabilitiesCalculator) throws IOException {
64
+ final MstGraphExporter exporter = new MstGraphExporter(transitionProbabilitiesCalculator);
65
+ final StringWriter writer = new StringWriter();
66
+ exporter.exportToJson(graphComponents, writer);
67
+ return writer.toString();
68
+ }
69
+
70
+ /**
71
+ * Exports the MST graph to a provided writer.
72
+ *
73
+ * @param graphComponents The MST graph components to export
74
+ * @param transitionProbabilitiesCalculator Calculator for edge probabilities
75
+ * @param writer Writer to output JSON to
76
+ * @throws IOException if writing fails
77
+ */
78
+ public static void exportToWriter(MstManeuverGraphComponents graphComponents,
79
+ MstGraphNodeTransitionProbabilitiesCalculator transitionProbabilitiesCalculator,
80
+ Writer writer) throws IOException {
81
+ final MstGraphExporter exporter = new MstGraphExporter(transitionProbabilitiesCalculator);
82
+ exporter.exportToJson(graphComponents, writer);
83
+ }
84
+}
java/com.sap.sailing.windestimation/src/com/sap/sailing/windestimation/aggregator/msthmm/MstGraphExporter.java
... ...
@@ -0,0 +1,380 @@
1
+package com.sap.sailing.windestimation.aggregator.msthmm;
2
+
3
+import java.io.IOException;
4
+import java.io.Writer;
5
+import java.text.SimpleDateFormat;
6
+import java.util.ArrayList;
7
+import java.util.HashMap;
8
+import java.util.HashSet;
9
+import java.util.List;
10
+import java.util.Map;
11
+import java.util.Set;
12
+import java.util.function.Supplier;
13
+import java.util.stream.Collectors;
14
+
15
+import com.sap.sailing.windestimation.aggregator.graph.DijkstraShortestPathFinderImpl;
16
+import com.sap.sailing.windestimation.aggregator.graph.DijsktraShortestPathFinder;
17
+import com.sap.sailing.windestimation.aggregator.graph.ElementAdjacencyQualityMetric;
18
+import com.sap.sailing.windestimation.aggregator.graph.InnerGraphSuccessorSupplier;
19
+import com.sap.sailing.windestimation.aggregator.hmm.GraphNode;
20
+import com.sap.sailing.windestimation.aggregator.hmm.WindCourseRange;
21
+import com.sap.sailing.windestimation.aggregator.msthmm.MstManeuverGraphGenerator.MstManeuverGraphComponents;
22
+import com.sap.sailing.windestimation.data.ManeuverForEstimation;
23
+import com.sap.sailing.windestimation.data.ManeuverTypeForClassification;
24
+import com.sap.sse.common.impl.DegreeBearingImpl;
25
+
26
+/**
27
+ * Exports MST graph data to JSON format for visualization.
28
+ * The output can be consumed by a Python visualization script.
29
+ *
30
+ * @author Generated for visualization purposes using Claude Opus 4.5
31
+ */
32
+public class MstGraphExporter {
33
+
34
+ private final MstGraphNodeTransitionProbabilitiesCalculator transitionProbabilitiesCalculator;
35
+ private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");
36
+
37
+ public MstGraphExporter(MstGraphNodeTransitionProbabilitiesCalculator transitionProbabilitiesCalculator) {
38
+ this.transitionProbabilitiesCalculator = transitionProbabilitiesCalculator;
39
+ }
40
+
41
+ /**
42
+ * Exports the MST graph components to JSON format.
43
+ *
44
+ * @param graphComponents The MST graph to export
45
+ * @param writer The writer to output JSON to
46
+ * @throws IOException if writing fails
47
+ */
48
+ public void exportToJson(MstManeuverGraphComponents graphComponents, Writer writer) throws IOException {
49
+ // First, assign node IDs and collect best nodes per level
50
+ final MstGraphLevel root = graphComponents.getRoot();
51
+ final Map<MstGraphLevel, Integer> levelToId = new HashMap<>();
52
+ final int[] nodeIdCounter = {0};
53
+ assignNodeIds(root, levelToId, nodeIdCounter);
54
+
55
+ // Collect path vote diagnostics for debugging
56
+ final Map<MstGraphLevel, Map<ManeuverTypeForClassification, List<Double>>> pathVotes =
57
+ collectPathVoteDiagnostics(graphComponents);
58
+
59
+ // Collect best (disambiguated) classification per node
60
+ final Map<String, String> bestNodePerLevel = collectBestNodePerLevel(graphComponents, levelToId, pathVotes);
61
+
62
+ // Derive best path edges from the best node selections
63
+ // This ensures edges connect nodes with red frames (best classifications)
64
+ final Set<String> bestPathEdges = collectBestPathEdges(graphComponents, bestNodePerLevel, levelToId);
65
+
66
+ writer.write("{\n");
67
+ writer.write(" \"nodes\": [\n");
68
+ // Export all nodes starting from root
69
+ final boolean[] firstNode = {true};
70
+ final int[] exportNodeIdCounter = {0};
71
+ final Map<MstGraphLevel, Integer> exportLevelToId = new HashMap<>();
72
+ exportNode(root, writer, firstNode, exportLevelToId, exportNodeIdCounter, 0, pathVotes);
73
+ writer.write("\n ],\n");
74
+ writer.write(" \"edges\": [\n");
75
+ // Export edges between nodes
76
+ final boolean[] firstEdge = {true};
77
+ exportEdges(root, writer, firstEdge, exportLevelToId, bestPathEdges);
78
+ writer.write("\n ],\n");
79
+ // Export best path information
80
+ writer.write(" \"bestPaths\": {\n");
81
+ boolean firstBest = true;
82
+ for (Map.Entry<String, String> entry : bestNodePerLevel.entrySet()) {
83
+ if (!firstBest) {
84
+ writer.write(",\n");
85
+ }
86
+ firstBest = false;
87
+ writer.write(" \"" + entry.getKey() + "\": \"" + entry.getValue() + "\"");
88
+ }
89
+ writer.write("\n }\n");
90
+ writer.write("}\n");
91
+ }
92
+
93
+ private void exportNode(MstGraphLevel level, Writer writer, boolean[] firstNode,
94
+ Map<MstGraphLevel, Integer> levelToId, int[] nodeIdCounter, int depth,
95
+ Map<MstGraphLevel, Map<ManeuverTypeForClassification, List<Double>>> pathVotes) throws IOException {
96
+ final int nodeId = nodeIdCounter[0]++;
97
+ levelToId.put(level, nodeId);
98
+ if (!firstNode[0]) {
99
+ writer.write(",\n");
100
+ }
101
+ firstNode[0] = false;
102
+ final ManeuverForEstimation maneuver = level.getManeuver();
103
+ writer.write(" {\n");
104
+ writer.write(" \"id\": " + nodeId + ",\n");
105
+ writer.write(" \"depth\": " + depth + ",\n");
106
+ writer.write(" \"timestamp\": \"" + DATE_FORMAT.format(maneuver.getManeuverTimePoint().asDate()) + "\",\n");
107
+ // Competitor info
108
+ if (maneuver.getCompetitorName() != null) {
109
+ writer.write(" \"competitorName\": \"" + escapeJson(maneuver.getCompetitorName()) + "\",\n");
110
+ }
111
+ writer.write(" \"position\": {\"lat\": " + maneuver.getManeuverPosition().getLatDeg() +
112
+ ", \"lon\": " + maneuver.getManeuverPosition().getLngDeg() + "},\n");
113
+ // distanceToParent is actually the "compound distance" = sum of two predicted standard deviations
114
+ // It's used for the Gaussian-based transition probability calculation
115
+ writer.write(" \"compoundDistanceToParent\": " + level.getDistanceToParent() + ",\n");
116
+ // Also export actual spatial distance and time difference to parent
117
+ final MstGraphLevel parent = level.getParent();
118
+ if (parent != null) {
119
+ final ManeuverForEstimation parentManeuver = parent.getManeuver();
120
+ final double spatialDistanceMeters = maneuver.getManeuverPosition()
121
+ .getDistance(parentManeuver.getManeuverPosition()).getMeters();
122
+ final long timeDiffMillis = Math.abs(maneuver.getManeuverTimePoint().asMillis()
123
+ - parentManeuver.getManeuverTimePoint().asMillis());
124
+ writer.write(" \"spatialDistanceToParentMeters\": " + spatialDistanceMeters + ",\n");
125
+ writer.write(" \"timeDiffToParentSeconds\": " + (timeDiffMillis / 1000.0) + ",\n");
126
+ }
127
+ writer.write(" \"compartments\": [\n");
128
+ // Export each maneuver type compartment
129
+ boolean firstCompartment = true;
130
+ for (GraphNode<MstGraphLevel> node : level.getLevelNodes()) {
131
+ if (!firstCompartment) {
132
+ writer.write(",\n");
133
+ }
134
+ firstCompartment = false;
135
+ final ManeuverTypeForClassification type = node.getManeuverType();
136
+ final WindCourseRange windRange = node.getValidWindRange();
137
+ final double confidence = node.getConfidence();
138
+ writer.write(" {\n");
139
+ writer.write(" \"type\": \"" + type.name() + "\",\n");
140
+ writer.write(" \"confidence\": " + confidence + ",\n");
141
+ writer.write(" \"windRangeFrom\": " +
142
+ new DegreeBearingImpl(windRange.getFromPortside()).reverse().getDegrees() + ",\n");
143
+ writer.write(" \"windRangeWidth\": " + windRange.getAngleTowardStarboard() + ",\n");
144
+
145
+ // Calculate single wind direction estimate for TACK/JIBE (width ~0)
146
+ double windEstimate;
147
+ if (windRange.getAngleTowardStarboard() < 1.0) {
148
+ windEstimate = new DegreeBearingImpl(windRange.getFromPortside()).reverse().getDegrees();
149
+ } else {
150
+ // For HEAD_UP/BEAR_AWAY, use middle of range
151
+ windEstimate = new DegreeBearingImpl(windRange.getFromPortside()).reverse().getDegrees()
152
+ + windRange.getAngleTowardStarboard() / 2.0;
153
+ if (windEstimate >= 360) {
154
+ windEstimate -= 360;
155
+ }
156
+ }
157
+ writer.write(" \"windEstimate\": " + windEstimate + ",\n");
158
+ writer.write(" \"tackAfter\": \"" + node.getTackAfter() + "\"\n");
159
+ writer.write(" }");
160
+ }
161
+ writer.write("\n ],\n");
162
+ // Add diagnostic info about path votes for this node
163
+ writer.write(" \"pathVotes\": " + formatPathVoteDiagnostics(level, pathVotes) + "\n");
164
+ writer.write(" }");
165
+ // Recursively export children
166
+ for (MstGraphLevel child : level.getChildren()) {
167
+ exportNode(child, writer, firstNode, levelToId, nodeIdCounter, depth + 1, pathVotes);
168
+ }
169
+ }
170
+
171
+ private void exportEdges(MstGraphLevel level, Writer writer, boolean[] firstEdge,
172
+ Map<MstGraphLevel, Integer> levelToId, Set<String> bestPathEdges) throws IOException {
173
+ final int parentId = levelToId.get(level);
174
+ for (final MstGraphLevel child : level.getChildren()) {
175
+ final int childId = levelToId.get(child);
176
+ // Export edges between all compartment pairs
177
+ for (final GraphNode<MstGraphLevel> parentNode : level.getLevelNodes()) {
178
+ for (final GraphNode<MstGraphLevel> childNode : child.getLevelNodes()) {
179
+ // Calculate transition probability
180
+ final double transitionProb = transitionProbabilitiesCalculator.getTransitionProbability(
181
+ childNode, parentNode, child.getDistanceToParent());
182
+ final String edgeKey = parentId + "_" + parentNode.getManeuverType().ordinal() +
183
+ "_" + childId + "_" + childNode.getManeuverType().ordinal();
184
+ final boolean isBestPath = bestPathEdges.contains(edgeKey);
185
+ if (!firstEdge[0]) {
186
+ writer.write(",\n");
187
+ }
188
+ firstEdge[0] = false;
189
+ writer.write(" {\n");
190
+ writer.write(" \"from\": " + parentId + ",\n");
191
+ writer.write(" \"fromType\": \"" + parentNode.getManeuverType().name() + "\",\n");
192
+ writer.write(" \"to\": " + childId + ",\n");
193
+ writer.write(" \"toType\": \"" + childNode.getManeuverType().name() + "\",\n");
194
+ writer.write(" \"transitionProbability\": " + transitionProb + ",\n");
195
+ writer.write(" \"distance\": " + child.getDistanceToParent() + ",\n");
196
+ writer.write(" \"isBestPath\": " + isBestPath + "\n");
197
+ writer.write(" }");
198
+ }
199
+ }
200
+ // Recursively export edges for children
201
+ exportEdges(child, writer, firstEdge, levelToId, bestPathEdges);
202
+ }
203
+ }
204
+
205
+ /**
206
+ * Collects the best path edges based on the disambiguated best node selections.
207
+ * An edge is marked as "best" if it connects two nodes where both endpoints
208
+ * have their best (disambiguated) classification matching the edge's from/to types.
209
+ */
210
+ private Set<String> collectBestPathEdges(MstManeuverGraphComponents graphComponents,
211
+ Map<String, String> bestNodePerLevel, Map<MstGraphLevel, Integer> levelToId) {
212
+ Set<String> bestPathEdges = new HashSet<>();
213
+ collectBestPathEdgesRecursive(graphComponents.getRoot(), bestPathEdges, bestNodePerLevel, levelToId);
214
+ return bestPathEdges;
215
+ }
216
+
217
+ private void collectBestPathEdgesRecursive(MstGraphLevel parent, Set<String> bestPathEdges,
218
+ Map<String, String> bestNodePerLevel, Map<MstGraphLevel, Integer> levelToId) {
219
+ Integer parentId = levelToId.get(parent);
220
+ String parentBestType = bestNodePerLevel.get(String.valueOf(parentId));
221
+
222
+ for (MstGraphLevel child : parent.getChildren()) {
223
+ Integer childId = levelToId.get(child);
224
+ String childBestType = bestNodePerLevel.get(String.valueOf(childId));
225
+
226
+ // Mark the edge between the best classifications as the best path edge
227
+ if (parentBestType != null && childBestType != null) {
228
+ int parentTypeOrdinal = ManeuverTypeForClassification.valueOf(parentBestType).ordinal();
229
+ int childTypeOrdinal = ManeuverTypeForClassification.valueOf(childBestType).ordinal();
230
+ String edgeKey = parentId + "_" + parentTypeOrdinal + "_" + childId + "_" + childTypeOrdinal;
231
+ bestPathEdges.add(edgeKey);
232
+ }
233
+
234
+ // Recurse to children
235
+ collectBestPathEdgesRecursive(child, bestPathEdges, bestNodePerLevel, levelToId);
236
+ }
237
+ }
238
+
239
+ /**
240
+ * Collects diagnostic information about which paths voted for which classification at each node.
241
+ * This runs Dijkstra from each leaf and records which classification was selected and with what path quality.
242
+ */
243
+ private Map<MstGraphLevel, Map<ManeuverTypeForClassification, List<Double>>> collectPathVoteDiagnostics(
244
+ MstManeuverGraphComponents graphComponents) {
245
+ final Map<MstGraphLevel, Map<ManeuverTypeForClassification, List<Double>>> result = new HashMap<>();
246
+
247
+ final ElementAdjacencyQualityMetric<GraphNode<MstGraphLevel>> edgeQualityMetric = (previousNode, currentNode) -> {
248
+ return transitionProbabilitiesCalculator.getTransitionProbability(currentNode, previousNode,
249
+ previousNode.getGraphLevel() == null ? 0.0 : previousNode.getGraphLevel().getDistanceToParent());
250
+ };
251
+
252
+ for (MstGraphLevel leaf : graphComponents.getLeaves()) {
253
+ final InnerGraphSuccessorSupplier<GraphNode<MstGraphLevel>, MstGraphLevel> innerGraphSuccessorSupplier =
254
+ new InnerGraphSuccessorSupplier<>(graphComponents,
255
+ (final Supplier<String> nameSupplier) -> new GraphNode<MstGraphLevel>(
256
+ null, null, new WindCourseRange(0, 360), 1.0, 0, null) {
257
+ @Override
258
+ public String toString() {
259
+ return nameSupplier.get();
260
+ }
261
+ });
262
+
263
+ final DijsktraShortestPathFinder<GraphNode<MstGraphLevel>> dijkstra =
264
+ new DijkstraShortestPathFinderImpl<>(
265
+ innerGraphSuccessorSupplier.getArtificialLeaf(leaf),
266
+ innerGraphSuccessorSupplier.getArtificialRoot(),
267
+ innerGraphSuccessorSupplier, edgeQualityMetric);
268
+
269
+ for (GraphNode<MstGraphLevel> node : dijkstra.getShortestPath()) {
270
+ if (node.getGraphLevel() != null) {
271
+ Map<ManeuverTypeForClassification, List<Double>> votesForNode =
272
+ result.computeIfAbsent(node.getGraphLevel(), k -> new HashMap<>());
273
+ votesForNode.computeIfAbsent(node.getManeuverType(), k -> new ArrayList<>())
274
+ .add(dijkstra.getPathQuality());
275
+ }
276
+ }
277
+ }
278
+
279
+ return result;
280
+ }
281
+
282
+ private Map<String, String> collectBestNodePerLevel(MstManeuverGraphComponents graphComponents,
283
+ Map<MstGraphLevel, Integer> levelToId,
284
+ Map<MstGraphLevel, Map<ManeuverTypeForClassification, List<Double>>> pathVotes) {
285
+ final Map<String, String> bestNodePerLevel = new HashMap<>();
286
+
287
+ // Determine best classification per node based on sum of path qualities
288
+ for (Map.Entry<MstGraphLevel, Map<ManeuverTypeForClassification, List<Double>>> entry : pathVotes.entrySet()) {
289
+ MstGraphLevel level = entry.getKey();
290
+ Map<ManeuverTypeForClassification, List<Double>> votes = entry.getValue();
291
+
292
+ // Find classification with highest sum of path qualities
293
+ double maxSum = -1;
294
+ ManeuverTypeForClassification bestType = null;
295
+
296
+ for (Map.Entry<ManeuverTypeForClassification, List<Double>> typeVotes : votes.entrySet()) {
297
+ double sum = typeVotes.getValue().stream().mapToDouble(Double::doubleValue).sum();
298
+ if (sum > maxSum) {
299
+ maxSum = sum;
300
+ bestType = typeVotes.getKey();
301
+ }
302
+ }
303
+
304
+ if (bestType != null) {
305
+ Integer nodeId = levelToId.get(level);
306
+ if (nodeId != null) {
307
+ bestNodePerLevel.put(String.valueOf(nodeId), bestType.name());
308
+ }
309
+ }
310
+ }
311
+
312
+ return bestNodePerLevel;
313
+ }
314
+
315
+ /**
316
+ * Formats path vote diagnostics for a node as a JSON string for inclusion in the export.
317
+ */
318
+ private String formatPathVoteDiagnostics(MstGraphLevel level,
319
+ Map<MstGraphLevel, Map<ManeuverTypeForClassification, List<Double>>> pathVotes) {
320
+ Map<ManeuverTypeForClassification, List<Double>> votes = pathVotes.get(level);
321
+ if (votes == null || votes.isEmpty()) {
322
+ return "{}";
323
+ }
324
+
325
+ StringBuilder sb = new StringBuilder();
326
+ sb.append("{");
327
+ boolean first = true;
328
+ for (ManeuverTypeForClassification type : ManeuverTypeForClassification.values()) {
329
+ List<Double> typeVotes = votes.get(type);
330
+ if (typeVotes != null && !typeVotes.isEmpty()) {
331
+ if (!first) sb.append(", ");
332
+ first = false;
333
+ double sum = typeVotes.stream().mapToDouble(Double::doubleValue).sum();
334
+ sb.append("\"").append(type.name()).append("\": {");
335
+ sb.append("\"pathCount\": ").append(typeVotes.size());
336
+ sb.append(", \"qualitySum\": ").append(sum);
337
+ sb.append(", \"qualities\": [");
338
+ sb.append(typeVotes.stream().map(d -> String.format("%.6e", d)).collect(Collectors.joining(", ")));
339
+ sb.append("]}");
340
+ }
341
+ }
342
+ sb.append("}");
343
+ return sb.toString();
344
+ }
345
+
346
+ private void assignNodeIds(MstGraphLevel level, Map<MstGraphLevel, Integer> levelToId, int[] nodeIdCounter) {
347
+ levelToId.put(level, nodeIdCounter[0]++);
348
+ for (final MstGraphLevel child : level.getChildren()) {
349
+ assignNodeIds(child, levelToId, nodeIdCounter);
350
+ }
351
+ }
352
+
353
+ /**
354
+ * Escapes special characters in a string for safe JSON encoding.
355
+ */
356
+ private static String escapeJson(String s) {
357
+ if (s == null) {
358
+ return null;
359
+ }
360
+ StringBuilder sb = new StringBuilder();
361
+ for (char c : s.toCharArray()) {
362
+ switch (c) {
363
+ case '"': sb.append("\\\""); break;
364
+ case '\\': sb.append("\\\\"); break;
365
+ case '\b': sb.append("\\b"); break;
366
+ case '\f': sb.append("\\f"); break;
367
+ case '\n': sb.append("\\n"); break;
368
+ case '\r': sb.append("\\r"); break;
369
+ case '\t': sb.append("\\t"); break;
370
+ default:
371
+ if (c < ' ') {
372
+ sb.append(String.format("\\u%04x", (int) c));
373
+ } else {
374
+ sb.append(c);
375
+ }
376
+ }
377
+ }
378
+ return sb.toString();
379
+ }
380
+}
java/com.sap.sailing.windestimation/src/com/sap/sailing/windestimation/aggregator/msthmm/MstManeuverGraphGenerator.java
... ...
@@ -28,6 +28,10 @@ public class MstManeuverGraphGenerator extends AbstractMstGraphGenerator<Maneuve
28 28
this.transitionProbabilitiesCalculator = transitionProbabilitiesCalculator;
29 29
}
30 30
31
+ public MstGraphNodeTransitionProbabilitiesCalculator getTransitionProbabilitiesCalculator() {
32
+ return transitionProbabilitiesCalculator;
33
+ }
34
+
31 35
@Override
32 36
protected double getDistanceBetweenObservations(ManeuverWithProbabilisticTypeClassification o1,
33 37
ManeuverWithProbabilisticTypeClassification o2) {
... ...
@@ -92,5 +96,24 @@ public class MstManeuverGraphGenerator extends AbstractMstGraphGenerator<Maneuve
92 96
public List<MstGraphLevel> getLeaves() {
93 97
return leaves;
94 98
}
99
+
100
+ @Override
101
+ public String toString() {
102
+ StringBuilder result = new StringBuilder();
103
+ result.append("MST maneuver graph:\n");
104
+ appendGraphLevelToStringBuilder(root, result, 0);
105
+ return result.toString();
106
+ }
107
+
108
+ private void appendGraphLevelToStringBuilder(MstGraphLevel graphLevel, StringBuilder result, int indent) {
109
+ for (int i = 0; i < indent; i++) {
110
+ result.append(" ");
111
+ }
112
+ result.append(graphLevel);
113
+ result.append("\n");
114
+ for (MstGraphLevel child : graphLevel.getChildren()) {
115
+ appendGraphLevelToStringBuilder(child, result, indent + 1);
116
+ }
117
+ }
95 118
}
96 119
}
java/com.sap.sailing.windestimation/src/com/sap/sailing/windestimation/data/ManeuverForEstimation.java
... ...
@@ -36,6 +36,7 @@ public class ManeuverForEstimation implements Comparable<ManeuverForEstimation>
36 36
private final boolean markPassing;
37 37
private final BoatClass boatClass;
38 38
private final boolean markPassingDataAvailable;
39
+ private final String competitorName;
39 40
40 41
public ManeuverForEstimation(TimePoint maneuverTimePoint, Position maneuverPosition, Bearing middleCourse,
41 42
SpeedWithBearing speedWithBearingBefore, SpeedWithBearing speedWithBearingAfter,
... ...
@@ -44,7 +45,7 @@ public class ManeuverForEstimation implements Comparable<ManeuverForEstimation>
44 45
Double deviationFromOptimalJibeAngleInDegrees, double speedLossRatio, double speedGainRatio,
45 46
double lowestSpeedVsExitingSpeedRatio, boolean clean, ManeuverCategory maneuverCategory,
46 47
double scaledSpeedBefore, double scaledSpeedAfter, boolean markPassing, BoatClass boatClass,
47
- boolean markPassingDataAvailable) {
48
+ boolean markPassingDataAvailable, String competitorName) {
48 49
this.maneuverTimePoint = maneuverTimePoint;
49 50
this.maneuverPosition = maneuverPosition;
50 51
this.middleCourse = middleCourse;
... ...
@@ -65,6 +66,7 @@ public class ManeuverForEstimation implements Comparable<ManeuverForEstimation>
65 66
this.markPassing = markPassing;
66 67
this.boatClass = boatClass;
67 68
this.markPassingDataAvailable = markPassingDataAvailable;
69
+ this.competitorName = competitorName;
68 70
}
69 71
70 72
public TimePoint getManeuverTimePoint() {
... ...
@@ -147,6 +149,10 @@ public class ManeuverForEstimation implements Comparable<ManeuverForEstimation>
147 149
return markPassingDataAvailable;
148 150
}
149 151
152
+ public String getCompetitorName() {
153
+ return competitorName;
154
+ }
155
+
150 156
@Override
151 157
public int compareTo(ManeuverForEstimation o) {
152 158
return maneuverTimePoint.compareTo(o.maneuverTimePoint);
java/com.sap.sailing.windestimation/src/com/sap/sailing/windestimation/data/transformer/ManeuverForEstimationTransformer.java
... ...
@@ -4,6 +4,7 @@ import java.util.ArrayList;
4 4
import java.util.List;
5 5
6 6
import com.sap.sailing.domain.base.BoatClass;
7
+import com.sap.sailing.domain.base.Competitor;
7 8
import com.sap.sailing.windestimation.data.CompetitorTrackWithEstimationData;
8 9
import com.sap.sailing.windestimation.data.ManeuverCategory;
9 10
import com.sap.sailing.windestimation.data.ManeuverForEstimation;
... ...
@@ -150,6 +151,11 @@ public class ManeuverForEstimationTransformer
150 151
151 152
public ManeuverForEstimation getManeuverForEstimation(ConvertableToManeuverForEstimation maneuver,
152 153
double speedScalingDivisor, BoatClass boatClass) {
154
+ return getManeuverForEstimation(maneuver, speedScalingDivisor, boatClass, null);
155
+ }
156
+
157
+ public ManeuverForEstimation getManeuverForEstimation(ConvertableToManeuverForEstimation maneuver,
158
+ double speedScalingDivisor, BoatClass boatClass, Competitor competitor) {
153 159
ManeuverCategory maneuverCategory = getManeuverCategory(maneuver);
154 160
double speedLossRatio = maneuver.getSpeedWithBearingBefore().getKnots() > 0
155 161
? maneuver.getLowestSpeed().getKnots() / maneuver.getSpeedWithBearingBefore().getKnots()
... ...
@@ -175,7 +181,8 @@ public class ManeuverForEstimationTransformer
175 181
maneuver.getCourseChangeInDegreesWithinTurningSection(), maneuver.getMaxTurningRateInDegreesPerSecond(),
176 182
deviationFromOptimalTackAngleInDegrees, deviationFromOptimalJibeAngleInDegrees, speedLossRatio,
177 183
speedGainRatio, lowestSpeedVsExitingSpeedRatio, clean, maneuverCategory, scaledSpeedBeforeInKnots,
178
- scaledSpeedAfterInKnots, maneuver.isMarkPassing(), boatClass, markPassingDataAvailable);
184
+ scaledSpeedAfterInKnots, maneuver.isMarkPassing(), boatClass, markPassingDataAvailable,
185
+ competitor==null?null:competitor.getName());
179 186
return maneuverForEstimation;
180 187
}
181 188
java/com.sap.sailing.windestimation/src/com/sap/sailing/windestimation/integration/CompleteManeuverCurveToManeuverForEstimationConverter.java
... ...
@@ -111,7 +111,7 @@ public class CompleteManeuverCurveToManeuverForEstimationConverter {
111 111
// TODO compute scaledSpeedDivisor, recompute maneuverForEstimation, reclassify all maneuver instances if
112 112
// scaledSpeedDivisor has significantly changed?
113 113
ManeuverForEstimation maneuverForEstimation = maneuverForEstimationTransformer
114
- .getManeuverForEstimation(convertableManeuver, 1.0, boatClass);
114
+ .getManeuverForEstimation(convertableManeuver, 1.0, boatClass, competitor);
115 115
return maneuverForEstimation;
116 116
}
117 117
java/com.sap.sailing.windestimation/src/com/sap/sailing/windestimation/integration/IncrementalMstHmmWindEstimationForTrackedRace.java
... ...
@@ -1,5 +1,7 @@
1 1
package com.sap.sailing.windestimation.integration;
2 2
3
+import java.io.File;
4
+import java.io.IOException;
3 5
import java.util.ArrayList;
4 6
import java.util.Collections;
5 7
import java.util.HashMap;
... ...
@@ -9,6 +11,7 @@ import java.util.Map;
9 11
import java.util.TreeMap;
10 12
import java.util.concurrent.ConcurrentLinkedDeque;
11 13
import java.util.concurrent.Executor;
14
+import java.util.logging.Level;
12 15
import java.util.logging.Logger;
13 16
14 17
import com.sap.sailing.domain.base.Competitor;
... ...
@@ -29,6 +32,7 @@ import com.sap.sailing.windestimation.aggregator.hmm.GraphLevelInference;
29 32
import com.sap.sailing.windestimation.aggregator.msthmm.DistanceAndDurationAwareWindTransitionProbabilitiesCalculator;
30 33
import com.sap.sailing.windestimation.aggregator.msthmm.MstBestPathsCalculator;
31 34
import com.sap.sailing.windestimation.aggregator.msthmm.MstBestPathsCalculatorImpl;
35
+import com.sap.sailing.windestimation.aggregator.msthmm.MstGraphExportHelper;
32 36
import com.sap.sailing.windestimation.aggregator.msthmm.MstGraphLevel;
33 37
import com.sap.sailing.windestimation.aggregator.msthmm.MstManeuverGraphGenerator.MstManeuverGraphComponents;
34 38
import com.sap.sailing.windestimation.data.ManeuverWithEstimatedType;
... ...
@@ -181,6 +185,14 @@ public class IncrementalMstHmmWindEstimationForTrackedRace implements Incrementa
181 185
mstManeuverGraphGenerator.add(competitor, newManeuverSpot, trackTimeInfo);
182 186
}
183 187
graphComponents = mstManeuverGraphGenerator.parseGraph();
188
+ if (logger.isLoggable(Level.FINE)) {
189
+ logger.fine("Exporting the maneuver graph to file after updating it with new maneuver spots for competitor "+competitor);
190
+ try {
191
+ MstGraphExportHelper.exportToFile(graphComponents, mstManeuverGraphGenerator.getTransitionProbabilitiesCalculator(), File.createTempFile("maneuverExport_", ".json").getCanonicalPath());
192
+ } catch (IOException e) {
193
+ logger.log(Level.WARNING, "Exporting the maneuver graph to file failed", e);
194
+ }
195
+ }
184 196
if (graphComponents != null) {
185 197
Iterable<GraphLevelInference<MstGraphLevel>> bestPath = bestPathsCalculator.getBestNodes(graphComponents);
186 198
for (GraphLevelInference<MstGraphLevel> inference : bestPath) {
java/com.sap.sse.security/src/com/sap/sse/security/impl/SecurityServiceImpl.java
... ...
@@ -3552,14 +3552,14 @@ implements ReplicableSecurityService, ClearStateTestSupport {
3552 3552
3553 3553
@Override
3554 3554
public void internalReleaseUserCreationLockOnIp(String ip) {
3555
- if(clientIPBasedTimedLocksForUserCreation.containsKey(ip)) {
3555
+ if (clientIPBasedTimedLocksForUserCreation.containsKey(ip)) {
3556 3556
clientIPBasedTimedLocksForUserCreation.remove(ip);
3557 3557
}
3558 3558
}
3559 3559
3560 3560
@Override
3561 3561
public void internalReleaseBearerTokenLockOnIp(String ip) {
3562
- if(clientIPBasedTimedLocksForBearerTokenAuthentication.containsKey(ip)) {
3562
+ if (clientIPBasedTimedLocksForBearerTokenAuthentication.containsKey(ip)) {
3563 3563
clientIPBasedTimedLocksForBearerTokenAuthentication.remove(ip);
3564 3564
}
3565 3565
}
java/pom.xml
... ...
@@ -48,7 +48,7 @@
48 48
Can we move the system property -Dmongo.dbName to "parameters.mongodb"? If so the IP of the host would be the same for all
49 49
profiles. -->
50 50
<parameters.common>
51
- -Dfile.encoding=UTF-8 -Dmongo.dbName=winddbTest -DspatialWind=true -Xss100m -Dgwt.rpc.version=9 -Dgoogle.maps.authenticationparams=key=AIzaSyD1Se4tIkt-wglccbco3S7twaHiG20hR9E
51
+ -Dfile.encoding=UTF-8 -Dmongo.dbName=winddbTest -DspatialWind=true -Xss100m -Dgwt.rpc.version=9 -Dgoogle.maps.authenticationparams=key=AIzaSyD1Se4tIkt-wglccbco3S7twaHiG20hR9E -Digtimi.base.url=http://127.0.0.1:8888
52 52
</parameters.common>
53 53
<parameters.vm-settings>
54 54
-ea -Xmx8192m -XX:+UseG1GC
java/target/configuration/logging_debug.properties
... ...
@@ -66,4 +66,5 @@ com.sap.sailing.aiagent.impl.RaceListener.level = FINE
66 66
# Show GithubReleasesRepository log output
67 67
com.sap.sse.landscape.impl.GithubReleasesRepository.level = FINE
68 68
69
-com.sap.sailing.windestimation.integration.IncrementalMstHmmWindEstimationForTrackedRaceTest.level = FINE
... ...
\ No newline at end of file
0
+com.sap.sailing.windestimation.integration.IncrementalMstHmmWindEstimationForTrackedRaceTest.level = FINE
1
+com.sap.sailing.windestimation.integration.IncrementalMstHmmWindEstimationForTrackedRace.level = FINE
... ...
\ No newline at end of file