1b406d97ecc13fc2ed9a756b90d9a4ff98357a31
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 (>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 |