feat: add STEP-113 Android PBF fast gate diagnostics
Some checks failed
validate-scripts / validate-scripts (push) Has been cancelled
Some checks failed
validate-scripts / validate-scripts (push) Has been cancelled
This commit is contained in:
@@ -63,6 +63,10 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() {
|
||||
private var step097Topology: Step097StencilFixture.Topology? = null
|
||||
private var step099Scenario: Step099ReadinessFixture.Scenario? = null
|
||||
private var step101Scenario: String? = null
|
||||
private var step113DiagnosticsHeight: Int? = null
|
||||
private var step113DiagnosticsMaxLines = 6
|
||||
private var step113Scene: Step113PbfGateScene? = null
|
||||
private var step113Scenario: Step113PbfGateScene.Scenario? = null
|
||||
private var step102Scenario: Step102PlacementFixture.Scenario? = null
|
||||
private var step102Tick = 0L
|
||||
private val step102Handler = Handler(Looper.getMainLooper())
|
||||
@@ -347,6 +351,11 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() {
|
||||
step093Variant = savedInstanceState?.getString(STATE_STEP093_VARIANT)
|
||||
?.let { value -> Step093Variant.values().firstOrNull { it.name == value } }
|
||||
?: Step093Variant.DEFAULT
|
||||
val savedStep113 = if (savedInstanceState?.containsKey("step113_scenario") == true) {
|
||||
savedInstanceState.getString("step113_scenario")
|
||||
} else intent.getStringExtra("step113_scenario")
|
||||
step113Scenario = savedStep113
|
||||
?.let { value -> Step113PbfGateScene.Scenario.values().firstOrNull { it.name == value } }
|
||||
super.onCreate(savedInstanceState)
|
||||
Log.i(
|
||||
TAG,
|
||||
@@ -359,6 +368,7 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() {
|
||||
status = findViewById(R.id.validationStatus)
|
||||
diagnostics = findViewById(R.id.validationDiagnostics)
|
||||
mapView.addOnDidFailLoadingMapListener { errorMessage ->
|
||||
step113Scene?.onStyleLoadFailed(errorMessage)
|
||||
if (navSeaPbfMode || purePbfMode) {
|
||||
Log.e(TAG, "NAVSEA_PBF_STYLE_ERROR reason=$errorMessage")
|
||||
} else if (realWeatherMode) {
|
||||
@@ -379,7 +389,8 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() {
|
||||
updateNavSeaPbfButtonState()
|
||||
updatePbfModeButtonState()
|
||||
updateRetainedCacheButtonState()
|
||||
if (step102Scenario != null) loadStep102Style()
|
||||
if (step113Scenario != null) startStep113Scene(step113Scenario!!)
|
||||
else if (step102Scenario != null) loadStep102Style()
|
||||
else if (step101Scenario != null) loadStep101Style()
|
||||
else if (step097Topology != null) loadStep097StencilStyle()
|
||||
else if (step093Mode) loadStep093Style(configureBeforeStyle = false)
|
||||
@@ -688,6 +699,7 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() {
|
||||
menu.add(0, 17, 17, "STEP-099 READINESS")
|
||||
menu.add(0, 18, 18, "STEP-101 TEARDOWN")
|
||||
menu.add(0, 19, 19, "STEP-102 PLACEMENT LOCALITY")
|
||||
menu.add(0, 20, 20, "STEP-113 FZG1-PBF GATE")
|
||||
setOnMenuItemClickListener { item ->
|
||||
when (item.itemId) {
|
||||
in Step093Topology.values().indices -> applyStep093Scene(Step093Topology.values()[item.itemId])
|
||||
@@ -701,6 +713,7 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() {
|
||||
17 -> showStep099Menu(anchor)
|
||||
18 -> showStep101Menu(anchor)
|
||||
19 -> showStep102Menu(anchor)
|
||||
20 -> showStep113Menu(anchor)
|
||||
}
|
||||
true
|
||||
}
|
||||
@@ -761,6 +774,68 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
private fun showStep113Menu(anchor: android.view.View) {
|
||||
PopupMenu(this, anchor).apply {
|
||||
Step113PbfGateScene.Scenario.values().forEachIndexed { index, scenario ->
|
||||
menu.add(0, index, index, "STEP-113 ${scenario.name}")
|
||||
}
|
||||
menu.add(0, 10, 10, "EXPORT LAST SNAPSHOT")
|
||||
menu.add(0, 11, 11, "MUTATE PROBE SOURCE ONCE")
|
||||
menu.add(0, 12, 12, "EXIT STEP-113")
|
||||
setOnMenuItemClickListener { item ->
|
||||
when (item.itemId) {
|
||||
in Step113PbfGateScene.Scenario.values().indices ->
|
||||
startStep113Scene(Step113PbfGateScene.Scenario.values()[item.itemId])
|
||||
10 -> step113Scene?.export()
|
||||
11 -> step113Scene?.mutateSource()
|
||||
12 -> { stopStep113Scene(); loadValidationStyle(active, enabled, "STEP113_EXIT") }
|
||||
}
|
||||
true
|
||||
}
|
||||
show()
|
||||
}
|
||||
}
|
||||
|
||||
private fun startStep113Scene(scenario: Step113PbfGateScene.Scenario) {
|
||||
stopStep113Scene()
|
||||
stopStep102Scenario()
|
||||
stopStep099Scenario()
|
||||
stopStep093DiagnosticsPolling()
|
||||
step101Handler.removeCallbacksAndMessages(null)
|
||||
step101Scenario = null
|
||||
step097Topology = null
|
||||
step093Mode = false
|
||||
pureParticleMode = false
|
||||
purePbfMode = false
|
||||
navSeaPbfMode = false
|
||||
realWeatherMode = false
|
||||
weatherRuntime?.dispose()
|
||||
weatherRuntime = null
|
||||
styleReady = false
|
||||
++styleLoadEpoch // Cancel previous asynchronous style callbacks.
|
||||
step113Scenario = scenario
|
||||
retainedCacheEnabled = true
|
||||
updateRetainedCacheButtonState()
|
||||
step113DiagnosticsHeight = diagnostics.layoutParams.height
|
||||
step113DiagnosticsMaxLines = diagnostics.maxLines
|
||||
diagnostics.maxLines = 10
|
||||
diagnostics.layoutParams = diagnostics.layoutParams.apply { height = (156 * resources.displayMetrics.density).toInt() }
|
||||
status.text = "STEP-113 FZG1-PBF Gate · ${scenario.name} · PARTIAL"
|
||||
step113Scene = Step113PbfGateScene(this, maplibreMap) { diagnostics.text = it }
|
||||
.also { it.start(scenario) }
|
||||
}
|
||||
|
||||
private fun stopStep113Scene() {
|
||||
step113Scene?.stop()
|
||||
step113DiagnosticsHeight?.let { originalHeight ->
|
||||
diagnostics.maxLines = step113DiagnosticsMaxLines
|
||||
diagnostics.layoutParams = diagnostics.layoutParams.apply { height = originalHeight }
|
||||
}
|
||||
step113DiagnosticsHeight = null
|
||||
step113Scene = null
|
||||
step113Scenario = null
|
||||
}
|
||||
|
||||
private fun showStep102Menu(anchor: android.view.View) {
|
||||
PopupMenu(this, anchor).apply {
|
||||
Step102PlacementFixture.Scenario.values().forEachIndexed { index, scenario ->
|
||||
@@ -778,6 +853,7 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() {
|
||||
}
|
||||
|
||||
private fun loadStep102Style() {
|
||||
stopStep113Scene()
|
||||
val scenario = step102Scenario ?: return
|
||||
stopStep093DiagnosticsPolling()
|
||||
step093Mode = false
|
||||
@@ -810,6 +886,7 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() {
|
||||
}
|
||||
|
||||
private fun loadStep101Style() {
|
||||
stopStep113Scene()
|
||||
val scenario = step101Scenario ?: return
|
||||
stopStep102Scenario()
|
||||
stopStep099Scenario()
|
||||
@@ -836,6 +913,7 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() {
|
||||
}
|
||||
|
||||
private fun applyStep099Scenario(scenario: Step099ReadinessFixture.Scenario) {
|
||||
stopStep113Scene()
|
||||
stopStep102Scenario()
|
||||
stopStep093DiagnosticsPolling()
|
||||
step093Mode = false
|
||||
@@ -971,6 +1049,7 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() {
|
||||
}
|
||||
|
||||
private fun loadStep097StencilStyle() {
|
||||
stopStep113Scene()
|
||||
val topology = step097Topology ?: return
|
||||
styleReady = false
|
||||
val epoch = ++styleLoadEpoch
|
||||
@@ -1124,6 +1203,7 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() {
|
||||
}
|
||||
|
||||
private fun loadStep093Style(configureBeforeStyle: Boolean) {
|
||||
stopStep113Scene()
|
||||
weatherRuntime?.dispose()
|
||||
weatherRuntime = null
|
||||
styleReady = false
|
||||
@@ -1621,6 +1701,7 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() {
|
||||
}
|
||||
|
||||
private fun loadValidationStyle(active: Boolean, enabled: Boolean, reason: String = "STYLE_CHANGE") {
|
||||
stopStep113Scene()
|
||||
stopStep102Scenario()
|
||||
if (step099Scenario != null) stopStep099Scenario()
|
||||
step097V4GoldenReady = false
|
||||
@@ -1692,6 +1773,7 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() {
|
||||
)
|
||||
|
||||
private fun loadPureParticleStyle(active: Boolean, enabled: Boolean, reason: String) {
|
||||
stopStep113Scene()
|
||||
weatherRuntime?.dispose()
|
||||
weatherRuntime = null
|
||||
this.active = active
|
||||
@@ -1721,6 +1803,7 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() {
|
||||
}
|
||||
|
||||
private fun loadPurePbfStyle(reason: String) {
|
||||
stopStep113Scene()
|
||||
weatherRuntime?.dispose()
|
||||
weatherRuntime = null
|
||||
styleReady = false
|
||||
@@ -1752,6 +1835,7 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() {
|
||||
}
|
||||
|
||||
private fun loadNavSeaPbfStyle(active: Boolean, enabled: Boolean, reason: String) {
|
||||
stopStep113Scene()
|
||||
weatherRuntime?.dispose()
|
||||
weatherRuntime = null
|
||||
this.active = active
|
||||
@@ -2397,8 +2481,8 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() {
|
||||
}
|
||||
} else if (step093Mode || step097Topology != null) startStep093DiagnosticsPolling()
|
||||
}
|
||||
override fun onResume() { super.onResume(); mapView.onResume() }
|
||||
override fun onPause() { mapView.onPause(); super.onPause() }
|
||||
override fun onResume() { super.onResume(); mapView.onResume(); step113Scene?.resume() }
|
||||
override fun onPause() { step113Scene?.pause(); mapView.onPause(); super.onPause() }
|
||||
override fun onStop() {
|
||||
step101Handler.removeCallbacksAndMessages(null)
|
||||
step093Handler.removeCallbacks(step093DiagnosticsPoll)
|
||||
@@ -2408,6 +2492,7 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() {
|
||||
super.onStop()
|
||||
}
|
||||
override fun onDestroy() {
|
||||
stopStep113Scene()
|
||||
step102Handler.removeCallbacksAndMessages(null)
|
||||
step101Handler.removeCallbacksAndMessages(null)
|
||||
if (step101Scenario != null) Log.i(TAG, "STEP101_MAPVIEW_DESTROY_ENTER scenario=$step101Scenario completed=$step101Completed")
|
||||
@@ -2422,6 +2507,7 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() {
|
||||
override fun onLowMemory() { super.onLowMemory(); mapView.onLowMemory() }
|
||||
|
||||
override fun onSaveInstanceState(outState: Bundle) {
|
||||
outState.putString("step113_scenario", step113Scenario?.name)
|
||||
outState.putBoolean(STATE_ACTIVE, active)
|
||||
outState.putBoolean(STATE_ENABLED, enabled)
|
||||
outState.putInt(STATE_MASK_MODE, maskMode)
|
||||
|
||||
@@ -0,0 +1,394 @@
|
||||
package org.maplibre.android.testapp.activity.particle
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.os.SystemClock
|
||||
import android.util.Base64
|
||||
import android.util.Log
|
||||
import org.json.JSONArray
|
||||
import org.json.JSONObject
|
||||
import org.maplibre.android.camera.CameraPosition
|
||||
import org.maplibre.android.geometry.LatLng
|
||||
import org.maplibre.android.maps.MapLibreMap
|
||||
import org.maplibre.android.maps.Style
|
||||
import org.maplibre.android.maps.WeatherRuntime
|
||||
import org.maplibre.android.style.sources.GeoJsonSource
|
||||
import org.maplibre.android.testapp.BuildConfig
|
||||
import org.maplibre.geojson.Feature
|
||||
import org.maplibre.geojson.Point
|
||||
import java.io.File
|
||||
import java.security.MessageDigest
|
||||
|
||||
/** STEP-113: real remote chart, explicit partial parity, fixed forecast and low-frequency pull. */
|
||||
internal class Step113PbfGateScene(
|
||||
private val context: Context,
|
||||
private val map: MapLibreMap,
|
||||
private val display: (String) -> Unit
|
||||
) {
|
||||
enum class Scenario { PBF63, STATIC_RC, PARTICLE_OFF, PARTIAL_RCRR, SOURCE_CHANGE }
|
||||
|
||||
private val handler = Handler(Looper.getMainLooper())
|
||||
private var generation = 0
|
||||
private var running = false
|
||||
private var paused = false
|
||||
private var ready = false
|
||||
private var failureReason: String? = null
|
||||
private var runtime: WeatherRuntime? = null
|
||||
private var scenario = Scenario.PBF63
|
||||
private var parity = JSONObject()
|
||||
private var latest: JSONObject? = null
|
||||
private var lastLogMs = 0L
|
||||
private var mutation = 0
|
||||
private var exportSequence = 0L
|
||||
private var commandResults = JSONObject()
|
||||
private var savedGestures: List<Boolean>? = null
|
||||
private val poll = object : Runnable {
|
||||
override fun run() {
|
||||
if (!running || paused) return
|
||||
if (running) {
|
||||
try {
|
||||
// One shared snapshot query per refresh; all UI/log/export uses this copy.
|
||||
val native = JSONObject(map.getRenderSegmentDiagnostics())
|
||||
latest = envelope(native)
|
||||
val trace = native.optJSONObject("fastGateTrace")
|
||||
val frames = trace?.optJSONArray("frames")
|
||||
val lastFrame = if (frames != null && frames.length() > 0) frames.optJSONObject(frames.length() - 1) else null
|
||||
val firstFailure = lastFrame?.optString("firstFailure") ?: native.optString("fastPathFallbackReason")
|
||||
val gates = trace?.optJSONArray("gates") ?: JSONArray()
|
||||
val rank = (0 until gates.length()).map { gates.getJSONObject(it) }
|
||||
.sortedByDescending { it.optLong("failures") }.take(4)
|
||||
val segments = native.optJSONArray("segments") ?: JSONArray()
|
||||
display(buildString {
|
||||
append("STEP-113 $scenario · ${if (ready) "READY/PARTIAL" else failureReason ?: "LOADING"} · actual=${native.optInt("styleLayerCount")}\n")
|
||||
append("first=$firstFailure seq=${lastFrame?.optLong("sequence")} full=${trace?.optLong("fullRendered")} ")
|
||||
append("window=${trace?.optInt("windowCount")} mismatch=${trace?.optLong("decisionMismatch")}\n")
|
||||
append(rank.joinToString(" / ") { "${it.optString("reason")}=${it.optLong("failures")}" })
|
||||
for (i in 0 until segments.length()) {
|
||||
val s = segments.getJSONObject(i)
|
||||
val c = s.optJSONObject("cache")
|
||||
append("\n${s.optString("strategy")}[${s.optInt("beginIndex")},${s.optInt("endExclusive")}) ")
|
||||
append("${c?.optString("cacheState") ?: "live"} B=${c?.optLong("build") ?: 0} ")
|
||||
append("R=${c?.optLong("reuse") ?: 0} I=${c?.optLong("invalidation") ?: 0}")
|
||||
}
|
||||
})
|
||||
val now = SystemClock.elapsedRealtime()
|
||||
if (now - lastLogMs >= 30000L) {
|
||||
lastLogMs = now
|
||||
logRecord("summary", summary(native))
|
||||
}
|
||||
} catch (error: Exception) {
|
||||
display("STEP-113 诊断读取失败: ${error.message}")
|
||||
Log.e(TAG, "diagnostic-read-failed", error)
|
||||
}
|
||||
}
|
||||
handler.postDelayed(this, 3000L)
|
||||
}
|
||||
}
|
||||
|
||||
fun start(next: Scenario) {
|
||||
stop()
|
||||
running = true
|
||||
scenario = next
|
||||
ready = false
|
||||
failureReason = null
|
||||
latest = null
|
||||
parity = JSONObject()
|
||||
lastLogMs = 0L
|
||||
mutation = 0
|
||||
commandResults = JSONObject()
|
||||
val epoch = ++generation
|
||||
display("STEP-113 $scenario · 加载正式 PBF · 113层完整等价未验证")
|
||||
val ui = map.uiSettings
|
||||
savedGestures = listOf(ui.isScrollGesturesEnabled(), ui.isRotateGesturesEnabled(),
|
||||
ui.isTiltGesturesEnabled(), ui.isZoomGesturesEnabled(),
|
||||
ui.isDoubleTapGesturesEnabled(), ui.isQuickZoomGesturesEnabled())
|
||||
ui.setAllGesturesEnabled(false)
|
||||
map.cancelTransitions()
|
||||
map.setRetainedUnderlayEnabled(true)
|
||||
if (!paused) handler.post(poll)
|
||||
map.setStyle(Style.Builder().fromUri(STYLE_URL)) production@ { loaded ->
|
||||
if (!running || epoch != generation) return@production
|
||||
try {
|
||||
val root = JSONObject(loaded.json)
|
||||
val baseLayers = root.getJSONArray("layers")
|
||||
val base = JSONArray(baseLayers.toString())
|
||||
check(base.length() == 63) { "正式基础层数已变化: ${base.length()},保留现场" }
|
||||
val ids = (0 until base.length()).map { base.getJSONObject(it).getString("id") }
|
||||
check(ids.distinct().size == ids.size && "clip-outline" in ids && "sea-area-fill" in ids &&
|
||||
"special-pattern-line-754" in ids) { "正式 Style 边界 ID 缺失或重复" }
|
||||
val source = root.getJSONObject("sources").getJSONObject("navsea_delivery")
|
||||
check(root.optString("sprite") == SPRITE && root.optString("glyphs") == GLYPHS &&
|
||||
source.optString("type") == "vector" && source.optJSONArray("tiles")?.optString(0) == TILES) {
|
||||
"正式 Sprite/Glyph/VectorTile URI 与 NavSea URL 事实源不一致"
|
||||
}
|
||||
parity = JSONObject().put("evidence", "TEST-APP-STRUCTURAL")
|
||||
.put("styleUrl", STYLE_URL).put("styleVersion", root.optInt("version"))
|
||||
.put("styleVersionBasis", "Style specification version; 包版本未知")
|
||||
.put("stylePackageVersionObserved", JSONObject.NULL)
|
||||
.put("styleMetadata", root.opt("metadata") ?: JSONObject.NULL)
|
||||
.put("styleName", root.optString("name"))
|
||||
.put("fingerprintBasis", "SDK-returned-production-json-utf8")
|
||||
.put("productionFingerprintSha256", sha256(loaded.json))
|
||||
.put("baseLayerCount", base.length()).put("baseLayersPreserved", true)
|
||||
.put("full113Equivalent", false).put("missingProductionDynamicLayerDefinitions", true)
|
||||
.put("dynamicParity", "测试场结构模式;缺少同版本113层完整Source/Layer/visibility/数据快照")
|
||||
.put("navSeaSourceHead", "ad9aa043fde37f87462f31de3fbd8ce008f8424b")
|
||||
.put("navSeaSourceSelector", "RenderSegmentHost.tsx:RC@rn-weather-color")
|
||||
.put("deviceReferenceSelector", "用户STEP-112:RCRR_PREFIX[0,13)/[13,33)/[33,107)/[107,113)")
|
||||
.put("baseLayerManifest", layerManifest(base)).put("sprite", root.optString("sprite"))
|
||||
.put("glyphs", root.optString("glyphs")).put("vectorSource", JSONObject(source.toString()))
|
||||
val hasField = scenario != Scenario.PBF63
|
||||
if (hasField) {
|
||||
val sources = root.getJSONObject("sources")
|
||||
check(!sources.has(SOURCE)) { "天气 Source ID 冲突" }
|
||||
sources.put(SOURCE, JSONObject().put("type", "raster-array").put("host-field-submission", true))
|
||||
val layers = JSONArray()
|
||||
val prefix = scenario == Scenario.PARTIAL_RCRR
|
||||
for (i in 0 until base.length()) {
|
||||
layers.put(base.getJSONObject(i))
|
||||
if (prefix && base.getJSONObject(i).getString("id") == "clip-outline") addFields(layers)
|
||||
}
|
||||
if (!prefix) addFields(layers) // NavSea current source fragment appends to top.
|
||||
if (scenario == Scenario.SOURCE_CHANGE) {
|
||||
sources.put(PROBE_SOURCE, JSONObject().put("type", "geojson")
|
||||
.put("data", JSONObject().put("type", "FeatureCollection").put("features", JSONArray())))
|
||||
layers.put(JSONObject().put("id", PROBE_LAYER).put("type", "circle")
|
||||
.put("source", PROBE_SOURCE).put("paint", JSONObject()
|
||||
.put("circle-radius", 6).put("circle-color", "#ff8800")))
|
||||
}
|
||||
root.put("layers", layers)
|
||||
}
|
||||
// Prove every original definition, including visibility, survives unchanged and in order.
|
||||
val finalLayers = root.getJSONArray("layers")
|
||||
val retainedBase = (0 until finalLayers.length()).map { finalLayers.getJSONObject(it) }
|
||||
.filter { it.getString("id") in ids }
|
||||
check(retainedBase.size == base.length() && retainedBase.map { it.getString("id") } == ids &&
|
||||
retainedBase.indices.all { retainedBase[it].toString() == base.getJSONObject(it).toString() }) {
|
||||
"基础图层等价检查失败"
|
||||
}
|
||||
parity.put("testStyleFingerprintSha256", sha256(root.toString()))
|
||||
.put("testJsonLayerCount", finalLayers.length())
|
||||
.put("baseParityBasis", "同次SDK生产Style回读;完整定义逐层比较,不是设备画面证明")
|
||||
val plan = planJson(scenario, ids.last())
|
||||
parity.put("testPlanSelector", if (scenario == Scenario.PARTIAL_RCRR)
|
||||
"结构RCRR:sea-area-fill/special-pattern-line-754/最后生产层ID;非现场113边界" else
|
||||
if (hasField) "当前NavSea源码RC:rn-weather-color" else "基础63层全RETAINED")
|
||||
.put("requestedPlan", JSONObject(plan))
|
||||
commandResults.put("planAccepted", map.setRenderSegmentPlan(plan))
|
||||
map.setStyle(Style.Builder().fromJson(root.toString())) testStyle@ {
|
||||
if (!running || epoch != generation) return@testStyle
|
||||
try {
|
||||
map.cameraPosition = CameraPosition.Builder().target(LatLng(33.45, 129.97))
|
||||
.zoom(10.5).bearing(0.0).tilt(0.0).build()
|
||||
if (hasField) initializeFields()
|
||||
ready = true
|
||||
lastLogMs = 0L
|
||||
handler.removeCallbacksAndMessages(null)
|
||||
if (!paused) handler.post(poll)
|
||||
} catch (error: Exception) { fail(error) }
|
||||
}
|
||||
} catch (error: Exception) { fail(error) }
|
||||
}
|
||||
}
|
||||
|
||||
private fun addFields(layers: JSONArray) {
|
||||
layers.put(JSONObject().put("id", COLOR).put("type", "weather-color").put("source", SOURCE)
|
||||
.put("paint", JSONObject().put("weather-color-opacity", 0.62)))
|
||||
layers.put(JSONObject().put("id", PARTICLE).put("type", "raster-particle").put("source", SOURCE)
|
||||
.put("layout", JSONObject().put("visibility", if (scenario == Scenario.PARTICLE_OFF) "none" else "visible"))
|
||||
.put("paint", JSONObject().put("raster-particle-active", true).put("raster-particle-particle-count", 1024)))
|
||||
}
|
||||
|
||||
private fun initializeFields() {
|
||||
val host = WeatherRuntime(map, WeatherRuntime.Config(SOURCE, COLOR, PARTICLE))
|
||||
runtime = host
|
||||
// Existing real-weather canonical assets. Explicitly not the current NavSea forecast/QBIN feed.
|
||||
val results = JSONArray()
|
||||
for (cycle in listOf("2026080700", "2026080706")) {
|
||||
val path = "integration/particle-weatherv2/ecmwf-hres-wind-20260807/$cycle"
|
||||
val meta = context.assets.open("$path/metadata.json").bufferedReader().use { it.readText() }
|
||||
val data = context.assets.open("$path/field.f32").use { it.readBytes() }
|
||||
val result = host.submitWindField(meta, data)
|
||||
results.put(result.name)
|
||||
check(result.accepted()) { "Field提交拒绝: $cycle $result" }
|
||||
}
|
||||
commandResults.put("fieldSubmit", results)
|
||||
.put("forecastSeek", host.setForecastTime(FORECAST_MS).name)
|
||||
.put("forecastPause", host.setPlaybackState(WeatherRuntime.PlaybackState.PAUSE).name)
|
||||
.put("particleVisibility", host.setParticleVisible(scenario != Scenario.PARTICLE_OFF).name)
|
||||
.put("targetFps", host.setTargetFps(WeatherRuntime.TargetFps.FPS_30).name)
|
||||
check(host.state == WeatherRuntime.State.READY &&
|
||||
commandResults.getString("forecastSeek") == "ACCEPTED" &&
|
||||
commandResults.getString("forecastPause") == "ACCEPTED" &&
|
||||
commandResults.getString("particleVisibility") == "ACCEPTED" &&
|
||||
commandResults.getString("targetFps") == "ACCEPTED") { "固定预报/30fps命令未全部接受: $commandResults" }
|
||||
}
|
||||
|
||||
fun mutateSource() {
|
||||
if (!running || !ready || scenario != Scenario.SOURCE_CHANGE) return
|
||||
val source = map.style?.getSourceAs<GeoJsonSource>(PROBE_SOURCE) ?: return
|
||||
source.setGeoJson(Feature.fromGeometry(Point.fromLngLat(129.97 + (++mutation % 2) * 0.01, 33.45)))
|
||||
// Single explicit mutation; polling never changes Source, Symbol, forecast or camera.
|
||||
commandResults.put("sourceMutationCount", mutation)
|
||||
logRecord("source-command", JSONObject().put("schemaVersion", 1).put("scenario", scenario.name)
|
||||
.put("mutation", mutation).put("source", PROBE_SOURCE).put("evidence", "TEST-APP-STRUCTURAL"))
|
||||
}
|
||||
|
||||
private fun envelope(native: JSONObject): JSONObject = JSONObject().put("schema", "step113-pbf-fast-gate")
|
||||
.put("schemaVersion", 1).put("appVersion", BuildConfig.VERSION_NAME)
|
||||
.put("scenario", scenario.name).put("sceneGeneration", generation)
|
||||
.put("sceneReady", ready).put("blockedReason", failureReason ?: JSONObject.NULL)
|
||||
.put("sampleElapsedMs", SystemClock.elapsedRealtime()).put("styleParity", parity)
|
||||
.put("cameraRequested", JSONObject().put("latitude", 33.45).put("longitude", 129.97)
|
||||
.put("zoom", 10.5).put("bearing", 0.0).put("pitch", 0.0).put("gesturesEnabled", false))
|
||||
.put("commands", JSONObject(commandResults.toString()))
|
||||
.put("forecastRequestedMs", if (runtime != null) FORECAST_MS else JSONObject.NULL)
|
||||
.put("actualForecastTimeObservedMs", JSONObject.NULL)
|
||||
.put("forecastStatus", runtime?.playbackState?.name ?: "NO_FIELD")
|
||||
.put("fieldParity", "既有ECMWF canonical双帧测试数据;非NavSea当前业务预报")
|
||||
.put("particleTick", JSONObject().put("targetFps", runtime?.targetFps?.name ?: "NONE")
|
||||
.put("state", runtime?.state?.name ?: "NO_FIELD").put("particleVisible", scenario != Scenario.PARTICLE_OFF && runtime != null)
|
||||
.put("simulationCounterScope", "process-debug-counter; 非本Map独占计数")
|
||||
.put("processSimulationAdvance", lastPost(native)?.opt("processSimulationAdvance") ?: JSONObject.NULL))
|
||||
.put("native", native)
|
||||
|
||||
private fun lastPost(native: JSONObject): JSONObject? {
|
||||
val frames = native.optJSONObject("fastGateTrace")?.optJSONArray("frames") ?: return null
|
||||
return if (frames.length() > 0) frames.optJSONObject(frames.length() - 1)?.optJSONObject("post") else null
|
||||
}
|
||||
|
||||
private fun summary(native: JSONObject): JSONObject {
|
||||
val t = native.optJSONObject("fastGateTrace") ?: JSONObject()
|
||||
val gates = t.optJSONArray("gates") ?: JSONArray()
|
||||
val counts = JSONArray()
|
||||
for (i in 0 until gates.length()) {
|
||||
val g = gates.getJSONObject(i)
|
||||
counts.put(JSONArray().put(g.optString("reason")).put(g.optLong("failures")).put(g.optLong("firstFailures")))
|
||||
}
|
||||
val rs = JSONArray()
|
||||
val segments = native.optJSONArray("segments") ?: JSONArray()
|
||||
for (i in 0 until segments.length()) {
|
||||
val s = segments.getJSONObject(i)
|
||||
val c = s.optJSONObject("cache") ?: continue
|
||||
rs.put(JSONObject().put("segment", s.optInt("segmentIndex")).put("begin", s.optInt("beginIndex"))
|
||||
.put("end", s.optInt("endExclusive")).put("state", c.optString("cacheState"))
|
||||
.put("build", c.optLong("build")).put("reuse", c.optLong("reuse"))
|
||||
.put("invalidation", c.optLong("invalidation")).put("fallback", c.optLong("fallbackCount"))
|
||||
.put("reason", c.optString("lastFallbackReason")))
|
||||
}
|
||||
return JSONObject().put("schema", "step113-summary").put("schemaVersion", 1).put("scenario", scenario.name)
|
||||
.put("sceneReady", ready).put("blockedReason", failureReason ?: JSONObject.NULL)
|
||||
.put("sceneGeneration", generation).put("full113Equivalent", false).put("basePreserved", parity.optBoolean("baseLayersPreserved"))
|
||||
.put("fingerprint", parity.optString("productionFingerprintSha256")).put("styleUrl", STYLE_URL)
|
||||
.put("actualLayers", native.optInt("styleLayerCount")).put("planValid", native.optBoolean("valid"))
|
||||
.put("planReason", native.optString("lastReason")).put("lifecycle", t.optLong("lifecycle"))
|
||||
.put("attempts", t.optLong("attempts")).put("windowCount", t.optInt("windowCount"))
|
||||
.put("windowBegin", t.optLong("windowBeginSequence")).put("windowEnd", t.optLong("windowEndSequence"))
|
||||
.put("windowFullRendered", t.optLong("windowFullRendered"))
|
||||
.put("windowFastRendered", t.optLong("windowFastRendered"))
|
||||
.put("processSimulationAdvance", lastPost(native)?.opt("processSimulationAdvance") ?: JSONObject.NULL)
|
||||
.put("overwritten", t.optLong("overwritten")).put("decisionMismatch", t.optLong("decisionMismatch"))
|
||||
.put("fullRendered", t.optLong("fullRendered")).put("fastRendered", t.optLong("fastRendered"))
|
||||
.put("gateFailureCounts", counts).put("retained", rs)
|
||||
}
|
||||
|
||||
fun export() {
|
||||
// Export exactly the last low-frequency shared snapshot; do not request an extra render/query.
|
||||
val record = latest ?: return
|
||||
val file = File(context.filesDir, "step113-${scenario.name}-${SystemClock.elapsedRealtime()}.json")
|
||||
try {
|
||||
file.writeText(record.toString(), Charsets.UTF_8)
|
||||
logRecord("snapshot", record)
|
||||
display("STEP-113 已导出 ${file.name};Logcat标签 $TAG")
|
||||
} catch (error: Exception) { Log.e(TAG, "export-failed", error) }
|
||||
}
|
||||
|
||||
private fun logRecord(kind: String, record: JSONObject) {
|
||||
// Logcat has a per-entry limit. UTF-8 -> Base64 ensures chunks never split Unicode.
|
||||
val data = Base64.encodeToString(record.toString().toByteArray(Charsets.UTF_8), Base64.NO_WRAP)
|
||||
val chunks = data.chunked(2400)
|
||||
val id = "${SystemClock.elapsedRealtime()}-${generation}-${++exportSequence}"
|
||||
chunks.forEachIndexed { index, chunk ->
|
||||
Log.i(TAG, JSONObject().put("schemaVersion", 1).put("kind", kind).put("exportId", id)
|
||||
.put("chunkIndex", index).put("chunkCount", chunks.size).put("encoding", "base64-utf8")
|
||||
.put("payload", chunk).toString())
|
||||
}
|
||||
}
|
||||
|
||||
fun onStyleLoadFailed(message: String) {
|
||||
if (running) fail(IllegalStateException(message))
|
||||
}
|
||||
|
||||
private fun fail(error: Exception) {
|
||||
ready = false
|
||||
failureReason = "BLOCKED: ${error.message}"
|
||||
display("STEP-113 BLOCKED: ${error.message}")
|
||||
Log.e(TAG, "scene-blocked", error)
|
||||
logRecord("blocked", JSONObject().put("schemaVersion", 1).put("scenario", scenario.name)
|
||||
.put("reason", error.message).put("styleUrl", STYLE_URL).put("evidence", "NOT-VERIFIED"))
|
||||
}
|
||||
|
||||
fun pause() { paused = true; handler.removeCallbacksAndMessages(null) }
|
||||
fun resume() {
|
||||
paused = false
|
||||
handler.removeCallbacksAndMessages(null)
|
||||
if (running) handler.post(poll)
|
||||
}
|
||||
fun stop() {
|
||||
running = false
|
||||
++generation
|
||||
handler.removeCallbacksAndMessages(null)
|
||||
runtime?.dispose()
|
||||
runtime = null
|
||||
savedGestures?.let { flags ->
|
||||
val ui = map.uiSettings
|
||||
ui.setScrollGesturesEnabled(flags[0])
|
||||
ui.setRotateGesturesEnabled(flags[1])
|
||||
ui.setTiltGesturesEnabled(flags[2])
|
||||
ui.setZoomGesturesEnabled(flags[3])
|
||||
ui.setDoubleTapGesturesEnabled(flags[4])
|
||||
ui.setQuickZoomGesturesEnabled(flags[5])
|
||||
}
|
||||
savedGestures = null
|
||||
ready = false
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val TAG = "STEP113Gate"
|
||||
const val STYLE_URL = "https://pbf.jpcn.ccwu.cc/style/style.navsea-delivery-full-semantic-extentfix.json"
|
||||
private const val SPRITE = "https://pbf.jpcn.ccwu.cc/sprite/sprite"
|
||||
private const val GLYPHS = "https://pbf.jpcn.ccwu.cc/fonts/{fontstack}/{range}.pbf"
|
||||
private const val TILES = "https://pbf.jpcn.ccwu.cc/pbf/pbf-delivery-full-20260418-rebuild/{z}/{x}/{y}.pbf"
|
||||
private const val SOURCE = "rn-weather-fields"
|
||||
private const val COLOR = "rn-weather-color"
|
||||
private const val PARTICLE = "rn-weather-particles"
|
||||
private const val PROBE_SOURCE = "step113-source-probe"
|
||||
private const val PROBE_LAYER = "step113-source-probe-circle"
|
||||
private const val FORECAST_MS = 1786060800000L
|
||||
|
||||
private fun planJson(s: Scenario, finalBaseId: String): String {
|
||||
val segments = JSONArray().put(JSONObject().put("strategy", "RETAINED"))
|
||||
if (s != Scenario.PBF63) {
|
||||
segments.put(JSONObject().put("strategy", "CONTINUOUS")
|
||||
.put("startLayerId", if (s == Scenario.PARTIAL_RCRR) "sea-area-fill" else COLOR))
|
||||
if (s == Scenario.PARTIAL_RCRR) {
|
||||
segments.put(JSONObject().put("strategy", "RETAINED").put("startLayerId", "special-pattern-line-754"))
|
||||
segments.put(JSONObject().put("strategy", "RETAINED").put("startLayerId", finalBaseId))
|
||||
}
|
||||
}
|
||||
return JSONObject().put("schemaVersion", 1).put("segments", segments).toString()
|
||||
}
|
||||
|
||||
private fun layerManifest(layers: JSONArray): JSONArray = JSONArray().apply {
|
||||
for (i in 0 until layers.length()) {
|
||||
val l = layers.getJSONObject(i)
|
||||
put(JSONObject().put("index", i).put("id", l.getString("id")).put("type", l.getString("type"))
|
||||
.put("source", l.opt("source") ?: JSONObject.NULL)
|
||||
.put("visibility", l.optJSONObject("layout")?.optString("visibility", "visible") ?: "visible"))
|
||||
}
|
||||
}
|
||||
private fun sha256(text: String): String = MessageDigest.getInstance("SHA-256")
|
||||
.digest(text.toByteArray(Charsets.UTF_8)).joinToString("") { "%02x".format(it.toInt() and 0xff) }
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user