feat: add retained underlay runtime controls

This commit is contained in:
tei
2026-09-22 16:18:58 +09:00
parent 9ad2fe5d58
commit 8384c8bd24
31 changed files with 1181 additions and 59 deletions

View File

@@ -716,6 +716,27 @@ jni::jboolean NativeMapView::getDebug(JNIEnv&) {
return map->getDebug() != DebugOptions::NoDebug;
}
void NativeMapView::setRetainedUnderlayEnabled(JNIEnv&, jni::jboolean enabled) {
map->setRetainedUnderlayEnabled(enabled);
}
jni::Local<jni::Array<jni::String>> NativeMapView::getRetainedUnderlayDiagnostics(JNIEnv& env) {
const auto snapshot = map->getRetainedUnderlayDiagnostics();
const std::array<std::string, 6> values = {
snapshot.enabled ? "true" : "false",
std::to_string(snapshot.build),
std::to_string(snapshot.reuse),
std::to_string(snapshot.fallback),
std::to_string(snapshot.invalidation),
snapshot.lastReason,
};
auto result = jni::Array<jni::String>::New(env, values.size());
for (std::size_t index = 0; index < values.size(); ++index) {
result.Set(env, index, jni::Make<jni::String>(env, values[index]));
}
return result;
}
jni::Local<jni::Array<jni::String>> NativeMapView::getActionJournalLogFiles(JNIEnv& env) {
const auto& actionJournal = map->getActionJournal();
if (!actionJournal) {
@@ -1795,6 +1816,8 @@ void NativeMapView::registerNative(jni::JNIEnv& env) {
METHOD(&NativeMapView::addMarkers, "nativeAddMarkers"),
METHOD(&NativeMapView::setDebug, "nativeSetDebug"),
METHOD(&NativeMapView::getDebug, "nativeGetDebug"),
METHOD(&NativeMapView::setRetainedUnderlayEnabled, "nativeSetRetainedUnderlayEnabled"),
METHOD(&NativeMapView::getRetainedUnderlayDiagnostics, "nativeGetRetainedUnderlayDiagnostics"),
METHOD(&NativeMapView::getActionJournalLogFiles, "nativeGetActionJournalLogFiles"),
METHOD(&NativeMapView::getActionJournalLog, "nativeGetActionJournalLog"),
METHOD(&NativeMapView::clearActionJournalLog, "nativeClearActionJournalLog"),

View File

@@ -201,6 +201,10 @@ public:
jni::jboolean getDebug(JNIEnv&);
void setRetainedUnderlayEnabled(JNIEnv&, jni::jboolean);
jni::Local<jni::Array<jni::String>> getRetainedUnderlayDiagnostics(JNIEnv&);
jni::Local<jni::Array<jni::String>> getActionJournalLogFiles(JNIEnv&);
jni::Local<jni::Array<jni::String>> getActionJournalLog(JNIEnv&);

View File

@@ -111,6 +111,22 @@ public final class MapLibreMap {
nativeMapView.triggerRepaint();
}
/** Enables or disables the renderer's map-scoped retained static underlay optimization. */
public void setRetainedUnderlayEnabled(boolean enabled) {
nativeMapView.setRetainedUnderlayEnabled(enabled);
}
/** Returns whether the retained static underlay optimization is enabled for this map. */
public boolean isRetainedUnderlayEnabled() {
return nativeMapView.getRetainedUnderlayDiagnostics().enabled;
}
/** Returns a thread-safe, pull-based retained-underlay diagnostics snapshot. */
@NonNull
public RetainedUnderlayDiagnostics getRetainedUnderlayDiagnostics() {
return nativeMapView.getRetainedUnderlayDiagnostics();
}
/**
* Query rendering statistics overlay status.
*/

View File

@@ -286,6 +286,10 @@ interface NativeMap {
boolean getDebug();
void setRetainedUnderlayEnabled(boolean enabled);
RetainedUnderlayDiagnostics getRetainedUnderlayDiagnostics();
String[] getActionJournalLogFiles();
String[] getActionJournalLog();

View File

@@ -653,6 +653,22 @@ final class NativeMapView implements NativeMap {
return nativeGetDebug();
}
@Override
public void setRetainedUnderlayEnabled(boolean enabled) {
if (checkState("setRetainedUnderlayEnabled")) {
return;
}
nativeSetRetainedUnderlayEnabled(enabled);
}
@Override
public RetainedUnderlayDiagnostics getRetainedUnderlayDiagnostics() {
if (checkState("getRetainedUnderlayDiagnostics")) {
return new RetainedUnderlayDiagnostics(new String[] {"true", "0", "0", "0", "0", "unavailable"});
}
return new RetainedUnderlayDiagnostics(nativeGetRetainedUnderlayDiagnostics());
}
@Override
public String[] getActionJournalLogFiles() {
if (checkState("getActionJournalLogFiles")) {
@@ -1702,6 +1718,12 @@ final class NativeMapView implements NativeMap {
@Keep
private native boolean nativeGetDebug();
@Keep
private native void nativeSetRetainedUnderlayEnabled(boolean enabled);
@Keep
private native String[] nativeGetRetainedUnderlayDiagnostics();
@Keep
private native String[] nativeGetActionJournalLogFiles();

View File

@@ -0,0 +1,27 @@
package org.maplibre.android.maps;
import androidx.annotation.Keep;
import androidx.annotation.NonNull;
/** A pull-based snapshot of this map's retained-underlay renderer state. */
@Keep
public final class RetainedUnderlayDiagnostics {
public final boolean enabled;
public final long build;
public final long reuse;
public final long fallback;
public final long invalidation;
@NonNull public final String lastReason;
RetainedUnderlayDiagnostics(@NonNull String[] values) {
if (values.length != 6) {
throw new IllegalArgumentException("Retained-underlay diagnostics require six values");
}
enabled = Boolean.parseBoolean(values[0]);
build = Long.parseLong(values[1]);
reuse = Long.parseLong(values[2]);
fallback = Long.parseLong(values[3]);
invalidation = Long.parseLong(values[4]);
lastReason = values[5];
}
}

View File

@@ -1,12 +1,17 @@
package org.maplibre.android.testapp.activity.particle
import android.os.Bundle
import android.graphics.Color
import android.os.Bundle
import android.text.InputType
import android.util.Log
import android.widget.Button
import android.widget.EditText
import android.widget.PopupMenu
import android.widget.TextView
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import org.json.JSONArray
import org.json.JSONObject
import org.maplibre.android.camera.CameraPosition
import org.maplibre.android.camera.CameraUpdateFactory
import org.maplibre.android.geometry.LatLng
@@ -39,6 +44,13 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() {
private var productionFeedMode = false
private var hostSubmissionMode = false
private var realWeatherMode = false
private var pureParticleMode = false
private var purePbfMode = false
private var navSeaPbfMode = false
private var navSeaPbfPosition = NavSeaPbfPosition.AFTER_CLIP
private var navSeaPbfRenderEnabled = true
private var navSeaPbfLayerVisibility = emptyList<NavSeaPbfLayerVisibility>()
private var retainedCacheEnabled = true
private var realWeatherTuningMenu: PopupMenu? = null
private var realWeatherMenuAnchor: android.view.View? = null
private var openRealWeatherTuningAfterStyle = false
@@ -57,7 +69,8 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() {
maskMode = savedInstanceState?.getInt(STATE_MASK_MODE, MASK_GLOBAL) ?: MASK_GLOBAL
fixtureMode = savedInstanceState?.getInt(STATE_FIXTURE_MODE, FIXTURE_UNIFORM_EAST) ?: FIXTURE_UNIFORM_EAST
timeMix = savedInstanceState?.getFloat(STATE_TIME_MIX, 0.0f) ?: 0.0f
particleCount = savedInstanceState?.getInt(STATE_PARTICLE_COUNT, PARTICLE_COUNT_256) ?: PARTICLE_COUNT_256
particleCount = (savedInstanceState?.getInt(STATE_PARTICLE_COUNT, PARTICLE_COUNT_256) ?: PARTICLE_COUNT_256)
.coerceIn(MIN_PARTICLE_COUNT, MAX_PARTICLE_COUNT)
lineWidth = savedInstanceState?.getFloat(STATE_LINE_WIDTH, DEFAULT_LINE_WIDTH) ?: DEFAULT_LINE_WIDTH
particleLength = savedInstanceState?.getFloat(STATE_PARTICLE_LENGTH, DEFAULT_PARTICLE_LENGTH) ?: DEFAULT_PARTICLE_LENGTH
particleOpacity = savedInstanceState?.getFloat(STATE_PARTICLE_OPACITY, DEFAULT_PARTICLE_OPACITY) ?: DEFAULT_PARTICLE_OPACITY
@@ -67,6 +80,14 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() {
timeWindowMode = savedInstanceState?.getBoolean(STATE_TIME_WINDOW, false) ?: false
hostSubmissionMode = savedInstanceState?.getBoolean(STATE_HOST_SUBMISSION, false) ?: false
realWeatherMode = savedInstanceState?.getBoolean(STATE_REAL_WEATHER, false) ?: false
pureParticleMode = savedInstanceState?.getBoolean(STATE_PURE_PARTICLE, false) ?: false
purePbfMode = savedInstanceState?.getBoolean(STATE_PURE_PBF, false) ?: false
navSeaPbfMode = savedInstanceState?.getBoolean(STATE_NAVSEA_PBF, false) ?: false
navSeaPbfPosition = savedInstanceState?.getString(STATE_NAVSEA_PBF_POSITION)
?.let { value -> NavSeaPbfPosition.values().firstOrNull { it.name == value } }
?: NavSeaPbfPosition.AFTER_CLIP
navSeaPbfRenderEnabled = savedInstanceState?.getBoolean(STATE_NAVSEA_PBF_RENDER_ENABLED, true) ?: true
retainedCacheEnabled = savedInstanceState?.getBoolean(STATE_RETAINED_CACHE_ENABLED, true) ?: true
super.onCreate(savedInstanceState)
Log.i(
TAG,
@@ -78,13 +99,16 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() {
mapView = findViewById(R.id.mapView)
status = findViewById(R.id.validationStatus)
mapView.addOnDidFailLoadingMapListener { errorMessage ->
if (realWeatherMode) {
if (navSeaPbfMode || purePbfMode) {
Log.e(TAG, "NAVSEA_PBF_STYLE_ERROR reason=$errorMessage")
} else if (realWeatherMode) {
Log.w(TAG, "ParticleField: event=BASEMAP_TILE_ERROR provider=GSI reason=$errorMessage failSoft=true")
}
}
mapView.onCreate(savedInstanceState)
mapView.getMapAsync { map ->
maplibreMap = map
maplibreMap.setRetainedUnderlayEnabled(retainedCacheEnabled)
if (savedInstanceState == null) {
map.cameraPosition = CameraPosition.Builder()
.target(LatLng(0.0, 0.0))
@@ -92,6 +116,9 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() {
.build()
}
updateRealWeatherButtonState()
updateNavSeaPbfButtonState()
updatePbfModeButtonState()
updateRetainedCacheButtonState()
loadValidationStyle(active = active, enabled = enabled, reason = "INITIAL")
}
@@ -160,10 +187,7 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() {
findViewById<Button>(R.id.validationParticle256).setOnClickListener { setParticleCount(PARTICLE_COUNT_256) }
findViewById<Button>(R.id.validationParticle1024).setOnClickListener { setParticleCount(PARTICLE_COUNT_1024) }
findViewById<Button>(R.id.validationParticle4096).setOnClickListener { setParticleCount(PARTICLE_COUNT_4096) }
findViewById<Button>(R.id.validationParticle16384).setOnClickListener { setParticleCount(PARTICLE_COUNT_16384) }
findViewById<Button>(R.id.validationParticle65536).setOnClickListener { setParticleCount(PARTICLE_COUNT_65536) }
findViewById<Button>(R.id.validationParticle262144).setOnClickListener { setParticleCount(PARTICLE_COUNT_262144) }
findViewById<Button>(R.id.validationParticle1048576).setOnClickListener { setParticleCount(PARTICLE_COUNT_1048576) }
findViewById<Button>(R.id.validationParticle10000).setOnClickListener { setParticleCount(MAX_PARTICLE_COUNT) }
findViewById<Button>(R.id.validationAdvanceWindow).setOnClickListener {
if (this::maplibreMap.isInitialized) advanceRuntimeFieldWindow()
}
@@ -221,15 +245,27 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() {
}
findViewById<Button>(R.id.validationMenuTime).setOnClickListener { dismissRealWeatherTuning(); showTimeMenu(it) }
findViewById<Button>(R.id.validationMenuVisual).setOnClickListener { dismissRealWeatherTuning(); showMenu(it, "VIS", intArrayOf(R.id.validationVisualDefault, R.id.validationVisualColor, R.id.validationVisualSizeDown, R.id.validationVisualSizeUp, R.id.validationVisualLengthDown, R.id.validationVisualLengthUp, R.id.validationVisualOpacityDown, R.id.validationVisualOpacityUp, R.id.validationVisualTrailDown, R.id.validationVisualTrailUp, R.id.validationVisualFadeDown, R.id.validationVisualFadeUp)) }
findViewById<Button>(R.id.validationMenuCount).setOnClickListener { dismissRealWeatherTuning(); showMenu(it, "COUNT", intArrayOf(R.id.validationParticle256, R.id.validationParticle1024, R.id.validationParticle4096, R.id.validationParticle16384, R.id.validationParticle65536, R.id.validationParticle262144, R.id.validationParticle1048576)) }
findViewById<Button>(R.id.validationMenuCount).setOnClickListener { dismissRealWeatherTuning(); showCountMenu(it) }
findViewById<Button>(R.id.validationMenuMap).setOnClickListener { dismissRealWeatherTuning(); showMenu(it, "MAP", intArrayOf(R.id.validationGlobal, R.id.validationOceanA, R.id.validationOceanB, R.id.validationDateline, R.id.validationWorldWrap)) }
findViewById<Button>(R.id.validationMenuRealWeather).setOnClickListener { activateRealWeather(it) }
findViewById<Button>(R.id.validationMenuNavSeaPbf).setOnClickListener { showNavSeaPbfMenu(it) }
findViewById<Button>(R.id.validationMenuPbfMode).setOnClickListener { showPbfModeMenu(it) }
findViewById<Button>(R.id.validationMenuRetainedCache).setOnClickListener { showRetainedCacheMenu(it) }
findViewById<Button>(R.id.validationMenuMore).setOnClickListener { dismissRealWeatherTuning(); showMenu(it, "MORE", intArrayOf(R.id.validationActive, R.id.validationStatic, R.id.validationDisable, R.id.validationEnable, R.id.validationUniformEast, R.id.validationUniformNorth, R.id.validationVortex, R.id.validationTimeWindow, R.id.validationHostFieldSubmit, R.id.validationProductionInvalid, R.id.validationHostFieldDuplicate, R.id.validationHostFieldInvalid, R.id.validationAdvanceWindow, R.id.validationDuplicateWindow)) }
}
private data class ParticleVisuals(val count: Int, val width: Float, val length: Float, val opacity: Float, val trailOpacity: Float, val trailFade: Float)
private enum class NavSeaPbfPosition(val label: String) {
BOTTOM("BOTTOM"),
AFTER_SEA("AFTER_SEA"),
AFTER_CLIP("AFTER_CLIP"),
BEFORE_SYMBOL_CLUSTER("BEFORE_SYMBOL_CLUSTER"),
TOP("TOP")
}
private fun activateRealWeather(anchor: android.view.View?) {
if (isPbfMatrixMode()) leaveRealWeatherMode()
realWeatherMenuAnchor = anchor ?: findViewById(R.id.validationMenuRealWeather)
if (realWeatherMode) {
if (realWeatherTuningMenu == null) showRealWeatherTuning(realWeatherMenuAnchor!!) else dismissRealWeatherTuning()
@@ -246,11 +282,16 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() {
productionFeedMode = false
hostSubmissionMode = true
realWeatherMode = true
pureParticleMode = false
purePbfMode = false
navSeaPbfMode = false
fixtureMode = FIXTURE_UNIFORM_EAST
timeMix = 0.0f
timelineState = "NOT READY"
openRealWeatherTuningAfterStyle = true
updateRealWeatherButtonState()
updateNavSeaPbfButtonState()
updatePbfModeButtonState()
if (this::maplibreMap.isInitialized) loadValidationStyle(active, enabled, "REAL_WEATHER")
}
@@ -258,7 +299,7 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() {
dismissRealWeatherTuning()
weatherRuntime?.dispose()
weatherRuntime = null
if (realWeatherMode) {
if (realWeatherMode || isPbfMatrixMode()) {
preRealWeatherVisuals?.let { visuals ->
particleCount = visuals.count; lineWidth = visuals.width; particleLength = visuals.length
particleOpacity = visuals.opacity; trailOpacity = visuals.trailOpacity; trailFade = visuals.trailFade
@@ -266,8 +307,14 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() {
}
preRealWeatherVisuals = null
realWeatherMode = false
pureParticleMode = false
purePbfMode = false
navSeaPbfMode = false
navSeaPbfLayerVisibility = emptyList()
openRealWeatherTuningAfterStyle = false
updateRealWeatherButtonState()
updateNavSeaPbfButtonState()
updatePbfModeButtonState()
}
private fun updateRealWeatherButtonState() {
@@ -277,10 +324,67 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() {
button.setTextColor(if (realWeatherMode) Color.YELLOW else Color.WHITE)
}
private fun updateNavSeaPbfButtonState() {
val button = findViewById<Button>(R.id.validationMenuNavSeaPbf)
button.isSelected = navSeaPbfMode
button.isActivated = navSeaPbfMode
button.setTextColor(if (navSeaPbfMode) Color.YELLOW else Color.WHITE)
}
private fun updatePbfModeButtonState() {
val button = findViewById<Button>(R.id.validationMenuPbfMode)
button.isSelected = isPbfMatrixMode()
button.isActivated = isPbfMatrixMode()
button.setTextColor(if (isPbfMatrixMode()) Color.YELLOW else Color.WHITE)
}
private fun updateRetainedCacheButtonState() {
val button = findViewById<Button>(R.id.validationMenuRetainedCache)
button.text = if (retainedCacheEnabled) "CACHE ON" else "CACHE OFF"
button.setTextColor(if (retainedCacheEnabled) Color.GREEN else Color.YELLOW)
}
private fun showRetainedCacheMenu(anchor: android.view.View) {
PopupMenu(this, anchor).apply {
menu.add(0, 0, 0, "CACHE ON")
menu.add(0, 1, 1, "CACHE OFF")
menu.add(0, 2, 2, "READ SNAPSHOT")
setOnMenuItemClickListener { item ->
when (item.itemId) {
0 -> setRetainedCacheEnabled(true)
1 -> setRetainedCacheEnabled(false)
2 -> logRetainedCacheSnapshot()
}
true
}
show()
}
}
private fun setRetainedCacheEnabled(enabled: Boolean) {
retainedCacheEnabled = enabled
maplibreMap.setRetainedUnderlayEnabled(enabled)
updateRetainedCacheButtonState()
status.text = statusText()
Log.i(TAG, "RETAINED_CACHE_CONTROL enabled=$enabled")
logRetainedCacheSnapshot()
}
private fun logRetainedCacheSnapshot() {
val snapshot = maplibreMap.retainedUnderlayDiagnostics
retainedCacheEnabled = snapshot.enabled
updateRetainedCacheButtonState()
Log.i(
TAG,
"RETAINED_CACHE_SNAPSHOT enabled=${snapshot.enabled} build=${snapshot.build} reuse=${snapshot.reuse} " +
"fallback=${snapshot.fallback} invalidation=${snapshot.invalidation} lastReason=${snapshot.lastReason}"
)
}
private fun dismissRealWeatherTuning() { realWeatherTuningMenu?.dismiss(); realWeatherTuningMenu = null }
private fun showRealWeatherTuning(anchor: android.view.View) {
if (!realWeatherMode) return
if (!usesRealWeatherAssets()) return
PopupMenu(this, anchor).also { popup ->
realWeatherTuningMenu = popup
popup.menu.add(0, 100, 0, "PARTICLE TUNING").isEnabled = false
@@ -338,6 +442,199 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() {
setVisualFloat("raster-particle-trail-fade", REAL_WEATHER_TRAIL_FADE) { trailFade = it }
}
private fun showNavSeaPbfMenu(anchor: android.view.View) {
dismissRealWeatherTuning()
val positions = NavSeaPbfPosition.values()
PopupMenu(this, anchor).apply {
positions.forEachIndexed { index, position ->
menu.add(0, index, index, position.label).isChecked = navSeaPbfMode && navSeaPbfPosition == position
}
menu.add(0, 101, 101, "PBF ON").apply {
isEnabled = hasProductionPbfStyle() && styleReady
isChecked = hasProductionPbfStyle() && navSeaPbfRenderEnabled
}
menu.add(0, 102, 102, "PBF OFF").apply {
isEnabled = hasProductionPbfStyle() && styleReady
isChecked = hasProductionPbfStyle() && !navSeaPbfRenderEnabled
}
menu.add(0, 100, 100, "PARTICLE TUNING")
setOnMenuItemClickListener { item ->
if (item.itemId in positions.indices) {
activateNavSeaPbf(positions[item.itemId], anchor)
} else if (item.itemId == 101) {
setNavSeaPbfRenderEnabled(true)
} else if (item.itemId == 102) {
setNavSeaPbfRenderEnabled(false)
} else if (item.itemId == 100) {
realWeatherMenuAnchor = anchor
showRealWeatherTuning(anchor)
}
true
}
show()
}
}
private fun activateNavSeaPbf(position: NavSeaPbfPosition, anchor: android.view.View) {
enterPbfMatrix()
realWeatherMenuAnchor = anchor
timeWindowMode = true
productionFeedMode = false
hostSubmissionMode = true
realWeatherMode = false
pureParticleMode = false
purePbfMode = false
navSeaPbfMode = true
navSeaPbfPosition = position
fixtureMode = FIXTURE_UNIFORM_EAST
timeMix = 0.0f
timelineState = "NOT READY"
openRealWeatherTuningAfterStyle = false
updateRealWeatherButtonState()
updateNavSeaPbfButtonState()
updatePbfModeButtonState()
if (this::maplibreMap.isInitialized) loadValidationStyle(active, enabled, "NAVSEA_PBF_${position.name}")
}
private fun showPbfModeMenu(anchor: android.view.View) {
PopupMenu(this, anchor).apply {
menu.add(0, 0, 0, "PURE PARTICLE").isChecked = pureParticleMode
menu.add(0, 1, 1, "PURE PBF").isChecked = purePbfMode
menu.add(0, 2, 2, "NAVSEA PBF").isChecked = navSeaPbfMode
setOnMenuItemClickListener { item ->
when (item.itemId) {
0 -> activatePureParticle(anchor)
1 -> activatePurePbf(anchor)
2 -> activateNavSeaPbf(navSeaPbfPosition, anchor)
}
true
}
show()
}
}
private fun showCountMenu(anchor: android.view.View) {
PopupMenu(this, anchor).apply {
menu.add(0, 0, 0, "256")
menu.add(0, 1, 1, "1K")
menu.add(0, 2, 2, "4K")
menu.add(0, 3, 3, "10K")
menu.add(0, 4, 4, "CUSTOM COUNT")
setOnMenuItemClickListener { item ->
when (item.itemId) {
0 -> setParticleCount(PARTICLE_COUNT_256)
1 -> setParticleCount(PARTICLE_COUNT_1024)
2 -> setParticleCount(PARTICLE_COUNT_4096)
3 -> setParticleCount(MAX_PARTICLE_COUNT)
4 -> showCustomParticleCountDialog()
}
true
}
show()
}
}
private fun showCustomParticleCountDialog() {
val input = EditText(this).apply {
inputType = InputType.TYPE_CLASS_NUMBER
setText(particleCount.toString())
selectAll()
hint = "$MIN_PARTICLE_COUNT..$MAX_PARTICLE_COUNT"
contentDescription = "CUSTOM PARTICLE COUNT"
}
val dialog = AlertDialog.Builder(this)
.setTitle("CUSTOM PARTICLE COUNT")
.setView(input)
.setNegativeButton("CANCEL", null)
.setPositiveButton("APPLY", null)
.create()
dialog.setOnShowListener {
dialog.getButton(AlertDialog.BUTTON_POSITIVE).setOnClickListener {
val requested = input.text.toString().toIntOrNull()
if (requested == null || requested !in MIN_PARTICLE_COUNT..MAX_PARTICLE_COUNT) {
input.error = "Enter an integer from $MIN_PARTICLE_COUNT to $MAX_PARTICLE_COUNT"
} else if (setParticleCount(requested)) {
dialog.dismiss()
} else {
input.error = "Particle layer is not ready"
}
}
}
dialog.show()
}
private fun enterPbfMatrix() {
if (realWeatherMode) leaveRealWeatherMode()
if (!isPbfMatrixMode()) {
preRealWeatherVisuals = ParticleVisuals(particleCount, lineWidth, particleLength, particleOpacity, trailOpacity, trailFade)
particleCount = REAL_WEATHER_PARTICLE_COUNT
lineWidth = REAL_WEATHER_LINE_WIDTH
particleLength = REAL_WEATHER_STREAK_LENGTH
particleOpacity = REAL_WEATHER_PARTICLE_OPACITY
trailOpacity = REAL_WEATHER_TRAIL_OPACITY
trailFade = REAL_WEATHER_TRAIL_FADE
realWeatherTargetFps = WeatherRuntime.TargetFps.FPS_30
}
}
private fun activatePureParticle(anchor: android.view.View) {
enterPbfMatrix()
realWeatherMenuAnchor = anchor
timeWindowMode = true
productionFeedMode = false
hostSubmissionMode = true
realWeatherMode = false
pureParticleMode = true
purePbfMode = false
navSeaPbfMode = false
fixtureMode = FIXTURE_UNIFORM_EAST
timeMix = 0.0f
timelineState = "NOT READY"
updateRealWeatherButtonState()
updateNavSeaPbfButtonState()
updatePbfModeButtonState()
if (this::maplibreMap.isInitialized) loadValidationStyle(active, enabled, "PURE_PARTICLE")
}
private fun activatePurePbf(anchor: android.view.View) {
enterPbfMatrix()
realWeatherMenuAnchor = anchor
timeWindowMode = false
productionFeedMode = false
hostSubmissionMode = false
realWeatherMode = false
pureParticleMode = false
purePbfMode = true
navSeaPbfMode = false
timelineState = "NOT READY"
updateRealWeatherButtonState()
updateNavSeaPbfButtonState()
updatePbfModeButtonState()
if (this::maplibreMap.isInitialized) loadValidationStyle(active, enabled, "PURE_PBF")
}
private fun isPbfMatrixMode(): Boolean = pureParticleMode || purePbfMode || navSeaPbfMode
private fun hasProductionPbfStyle(): Boolean = purePbfMode || navSeaPbfMode
private fun usesRealWeatherAssets(): Boolean = realWeatherMode || pureParticleMode || navSeaPbfMode
private fun setNavSeaPbfRenderEnabled(enabled: Boolean) {
if (!hasProductionPbfStyle() || !styleReady) return
if (navSeaPbfRenderEnabled == enabled) return
navSeaPbfRenderEnabled = enabled
applyNavSeaPbfRenderState()
status.text = statusText()
Log.i(TAG, "NAVSEA_PBF_RENDER enabled=$enabled affectedLayers=${navSeaPbfLayerVisibility.size}")
}
private fun applyNavSeaPbfRenderState() {
navSeaPbfLayerVisibility.forEach { layer ->
maplibreMap.setStyleLayerVisibility(
layer.id,
navSeaPbfRenderEnabled && layer.originallyVisible
)
}
}
private fun showMenu(anchor: android.view.View, title: String, actions: IntArray) {
PopupMenu(this, anchor).apply {
actions.forEachIndexed { index, id -> menu.add(0, index, index, actionLabel(id)) }
@@ -383,7 +680,7 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() {
}
val commandAccepted = when {
accepted == WeatherRuntime.CommandResult.ACCEPTED -> true
realWeatherMode -> false
usesRealWeatherAssets() -> false
else -> maplibreMap.controlRasterArraySourceTimeline(PARTICLE_SOURCE_ID, command, value)
}
if (!commandAccepted) {
@@ -403,10 +700,7 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() {
R.id.validationProductionInvalid -> "INVALID PROD"
R.id.validationHostFieldDuplicate -> "HOST DUPLICATE"
R.id.validationHostFieldInvalid -> "HOST INVALID"
R.id.validationParticle1048576 -> "1M"
R.id.validationParticle262144 -> "256K"
R.id.validationParticle65536 -> "64K"
R.id.validationParticle16384 -> "16K"
R.id.validationParticle10000 -> "10K"
R.id.validationParticle4096 -> "4K"
R.id.validationParticle1024 -> "1K"
R.id.validationParticle256 -> "256"
@@ -440,6 +734,18 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() {
}
private fun loadValidationStyle(active: Boolean, enabled: Boolean, reason: String = "STYLE_CHANGE") {
if (pureParticleMode) {
loadPureParticleStyle(active, enabled, reason)
return
}
if (purePbfMode) {
loadPurePbfStyle(reason)
return
}
if (navSeaPbfMode) {
loadNavSeaPbfStyle(active, enabled, reason)
return
}
weatherRuntime?.dispose()
weatherRuntime = null
this.active = active
@@ -463,6 +769,7 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() {
}
status.text = statusText()
updateRealWeatherButtonState()
updateNavSeaPbfButtonState()
if (openRealWeatherTuningAfterStyle && realWeatherMode) {
openRealWeatherTuningAfterStyle = false
realWeatherMenuAnchor?.let { showRealWeatherTuning(it) }
@@ -470,6 +777,276 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() {
}
}
private data class NavSeaPbfStyle(
val json: String,
val productionLayerCount: Int,
val seaAreaFill: Boolean,
val clipOutline: Boolean,
val belowCount: Int,
val aboveCount: Int,
val anchorLayer: String,
val pbfLayerVisibility: List<NavSeaPbfLayerVisibility>
)
private data class NavSeaPbfLayerVisibility(val id: String, val originallyVisible: Boolean)
private data class NavSeaProductionStyle(
val root: JSONObject,
val layers: JSONArray,
val layerIds: List<String>,
val seaIndex: Int,
val clipIndex: Int,
val specialPatternIndex: Int,
val hazardPointsIndex: Int,
val pbfLayerVisibility: List<NavSeaPbfLayerVisibility>
)
private fun loadPureParticleStyle(active: Boolean, enabled: Boolean, reason: String) {
weatherRuntime?.dispose()
weatherRuntime = null
this.active = active
this.enabled = enabled
styleReady = false
navSeaPbfLayerVisibility = emptyList()
val epoch = ++styleLoadEpoch
Log.i(TAG, "ParticleGPU: event=STYLE_LOAD epoch=$epoch reason=$reason")
maplibreMap.setStyle(Style.Builder().fromJson(pureParticleStyle(active, enabled))) {
if (pureParticleMode && epoch == styleLoadEpoch) {
styleReady = true
weatherRuntime = WeatherRuntime(
maplibreMap,
WeatherRuntime.Config(PARTICLE_SOURCE_ID, WEATHER_COLOR_LAYER_ID, PARTICLE_LAYER_ID)
)
weatherRuntime?.setTargetFps(realWeatherTargetFps)
setNavSeaPbfCamera(logNavSeaReady = false)
if (hostSubmissionMode) submitRealWeatherDataset()
status.text = statusText()
Log.i(
TAG,
"PURE_PARTICLE_READY camera=$NAVSEA_CAMERA_LATITUDE,$NAVSEA_CAMERA_LONGITUDE " +
"particleCount=$particleCount targetFps=${realWeatherTargetFps.name.removePrefix("FPS_")}"
)
}
}
}
private fun loadPurePbfStyle(reason: String) {
weatherRuntime?.dispose()
weatherRuntime = null
styleReady = false
navSeaPbfLayerVisibility = emptyList()
val epoch = ++styleLoadEpoch
Log.i(TAG, "ParticleGPU: event=STYLE_LOAD epoch=$epoch reason=$reason")
maplibreMap.setStyle(Style.Builder().fromUri(NAVSEA_PBF_STYLE_URL)) { productionStyle ->
if (purePbfMode && epoch == styleLoadEpoch) {
val production = try {
inspectNavSeaProductionStyle(productionStyle.json)
} catch (error: Exception) {
Log.e(TAG, "NAVSEA_PBF_STYLE_ERROR reason=PURE_PBF_CONTRACT", error)
null
}
if (production != null) {
styleReady = true
navSeaPbfLayerVisibility = production.pbfLayerVisibility
applyNavSeaPbfRenderState()
setNavSeaPbfCamera(logNavSeaReady = false)
status.text = statusText()
Log.i(
TAG,
"PURE_PBF_READY layerCount=${production.layers.length()} seaAreaFill=${production.seaIndex >= 0} " +
"clipOutline=${production.clipIndex >= 0} camera=$NAVSEA_CAMERA_LATITUDE,$NAVSEA_CAMERA_LONGITUDE"
)
}
}
}
}
private fun loadNavSeaPbfStyle(active: Boolean, enabled: Boolean, reason: String) {
weatherRuntime?.dispose()
weatherRuntime = null
this.active = active
this.enabled = enabled
styleReady = false
navSeaPbfLayerVisibility = emptyList()
val epoch = ++styleLoadEpoch
status.text = statusText()
Log.i(TAG, "ParticleGPU: event=STYLE_LOAD epoch=$epoch reason=$reason")
Log.i(TAG, "NAVSEA_PBF_STYLE_LOAD url=$NAVSEA_PBF_STYLE_URL position=${navSeaPbfPosition.name}")
maplibreMap.setStyle(Style.Builder().fromUri(NAVSEA_PBF_STYLE_URL)) { productionStyle ->
if (navSeaPbfMode && epoch == styleLoadEpoch) {
val augmented = try {
buildNavSeaPbfStyle(productionStyle.json, active, enabled, navSeaPbfPosition)
} catch (error: Exception) {
Log.e(TAG, "NAVSEA_PBF_STYLE_ERROR reason=AUGMENT_FAILED position=${navSeaPbfPosition.name}", error)
null
}
if (augmented != null) {
maplibreMap.setStyle(Style.Builder().fromJson(augmented.json)) {
if (navSeaPbfMode && epoch == styleLoadEpoch) {
styleReady = true
navSeaPbfLayerVisibility = augmented.pbfLayerVisibility
applyNavSeaPbfRenderState()
weatherRuntime = WeatherRuntime(
maplibreMap,
WeatherRuntime.Config(PARTICLE_SOURCE_ID, WEATHER_COLOR_LAYER_ID, PARTICLE_LAYER_ID)
)
weatherRuntime?.setTargetFps(realWeatherTargetFps)
updateRealWeatherButtonState()
updateNavSeaPbfButtonState()
Log.i(
TAG,
"NAVSEA_PBF_STYLE_READY layerCount=${augmented.productionLayerCount} " +
"seaAreaFill=${augmented.seaAreaFill} clipOutline=${augmented.clipOutline}"
)
Log.i(
TAG,
"NAVSEA_PBF_POSITION position=${navSeaPbfPosition.name} belowCount=${augmented.belowCount} " +
"aboveCount=${augmented.aboveCount} anchorLayer=${augmented.anchorLayer}"
)
setNavSeaPbfCamera()
if (hostSubmissionMode) submitRealWeatherDataset()
status.text = statusText()
}
}
}
}
}
}
private fun buildNavSeaPbfStyle(
productionJson: String,
active: Boolean,
enabled: Boolean,
position: NavSeaPbfPosition
): NavSeaPbfStyle {
val production = inspectNavSeaProductionStyle(productionJson)
val root = production.root
val sources = root.getJSONObject("sources")
check(!sources.has(PARTICLE_SOURCE_ID)) { "production style already contains $PARTICLE_SOURCE_ID" }
sources.put(PARTICLE_SOURCE_ID, JSONObject().put("type", "raster-array").put("host-field-submission", true))
val productionLayers = production.layers
val layerIds = production.layerIds
val seaIndex = production.seaIndex
val clipIndex = production.clipIndex
val specialPatternIndex = production.specialPatternIndex
val hazardPointsIndex = production.hazardPointsIndex
val insertionIndex = when (position) {
NavSeaPbfPosition.BOTTOM -> 0
NavSeaPbfPosition.AFTER_SEA -> seaIndex + 1
NavSeaPbfPosition.AFTER_CLIP -> clipIndex + 1
NavSeaPbfPosition.BEFORE_SYMBOL_CLUSTER -> hazardPointsIndex
NavSeaPbfPosition.TOP -> productionLayers.length()
}
val anchorLayer = when (position) {
NavSeaPbfPosition.BOTTOM -> layerIds.first()
NavSeaPbfPosition.AFTER_SEA -> NAVSEA_SEA_AREA_LAYER_ID
NavSeaPbfPosition.AFTER_CLIP -> NAVSEA_CLIP_OUTLINE_LAYER_ID
NavSeaPbfPosition.BEFORE_SYMBOL_CLUSTER -> NAVSEA_SPECIAL_PATTERN_LAYER_ID
NavSeaPbfPosition.TOP -> layerIds.last()
}
val augmentedLayers = JSONArray()
for (index in 0..productionLayers.length()) {
if (enabled && index == insertionIndex) augmentedLayers.put(navSeaParticleLayer(active))
if (index < productionLayers.length()) augmentedLayers.put(productionLayers.get(index))
}
root.put("layers", augmentedLayers)
return NavSeaPbfStyle(
json = root.toString(),
productionLayerCount = productionLayers.length(),
seaAreaFill = seaIndex >= 0,
clipOutline = clipIndex >= 0,
belowCount = insertionIndex,
aboveCount = productionLayers.length() - insertionIndex,
anchorLayer = anchorLayer,
pbfLayerVisibility = production.pbfLayerVisibility
)
}
private fun inspectNavSeaProductionStyle(productionJson: String): NavSeaProductionStyle {
val root = JSONObject(productionJson)
val layers = root.getJSONArray("layers")
val layerIds = (0 until layers.length()).map { layers.getJSONObject(it).getString("id") }
val pbfLayerVisibility = (0 until layers.length()).mapNotNull { index ->
val layer = layers.getJSONObject(index)
if (layer.optString("source") != NAVSEA_PBF_SOURCE_ID) null else NavSeaPbfLayerVisibility(
id = layer.getString("id"),
originallyVisible = layer.optJSONObject("layout")?.optString("visibility", "visible") != "none"
)
}
val result = NavSeaProductionStyle(
root = root,
layers = layers,
layerIds = layerIds,
seaIndex = layerIds.indexOf(NAVSEA_SEA_AREA_LAYER_ID),
clipIndex = layerIds.indexOf(NAVSEA_CLIP_OUTLINE_LAYER_ID),
specialPatternIndex = layerIds.indexOf(NAVSEA_SPECIAL_PATTERN_LAYER_ID),
hazardPointsIndex = layerIds.indexOf(NAVSEA_HAZARD_POINTS_LAYER_ID),
pbfLayerVisibility = pbfLayerVisibility
)
check(
layers.length() == NAVSEA_PRODUCTION_LAYER_COUNT && result.seaIndex == 3 && result.clipIndex == 31 &&
result.specialPatternIndex == 37 && result.hazardPointsIndex == 38 && pbfLayerVisibility.isNotEmpty()
) {
"production style contract changed: count=${layers.length()} sea=${result.seaIndex} clip=${result.clipIndex} " +
"specialPattern=${result.specialPatternIndex} hazardPoints=${result.hazardPointsIndex} " +
"pbfLayers=${pbfLayerVisibility.size}"
}
return result
}
private fun pureParticleStyle(active: Boolean, enabled: Boolean): String {
val particleLayer = if (enabled) navSeaParticleLayer(active) else null
return JSONObject()
.put("version", 8)
.put("name", "Pure Particle Validation")
.put(
"sources",
JSONObject().put(
PARTICLE_SOURCE_ID,
JSONObject().put("type", "raster-array").put("host-field-submission", true)
)
)
.put("layers", JSONArray().apply { if (particleLayer != null) put(particleLayer) })
.toString()
}
private fun navSeaParticleLayer(active: Boolean): JSONObject = JSONObject()
.put("id", PARTICLE_LAYER_ID)
.put("type", "raster-particle")
.put("source", PARTICLE_SOURCE_ID)
.put(
"paint",
JSONObject()
.put("raster-particle-active", active)
.put("raster-particle-mask-mode", maskMode)
.put("raster-particle-time-mix", timeMix)
.put("raster-particle-particle-count", particleCount)
.put("raster-particle-line-width", lineWidth)
.put("raster-particle-particle-length", particleLength)
.put("raster-particle-particle-opacity", particleOpacity)
.put("raster-particle-trail-opacity", trailOpacity)
.put("raster-particle-trail-fade", trailFade)
.put("raster-particle-speed-color-enabled", speedColorEnabled)
)
private fun setNavSeaPbfCamera(logNavSeaReady: Boolean = true) {
maplibreMap.cameraPosition = CameraPosition.Builder()
.target(LatLng(NAVSEA_CAMERA_LATITUDE, NAVSEA_CAMERA_LONGITUDE))
.zoom(NAVSEA_CAMERA_ZOOM)
.bearing(NAVSEA_CAMERA_BEARING)
.tilt(NAVSEA_CAMERA_PITCH)
.build()
if (logNavSeaReady) {
Log.i(
TAG,
"NAVSEA_PBF_CAMERA_READY center=$NAVSEA_CAMERA_LATITUDE,$NAVSEA_CAMERA_LONGITUDE " +
"zoom=$NAVSEA_CAMERA_ZOOM bearing=$NAVSEA_CAMERA_BEARING pitch=$NAVSEA_CAMERA_PITCH"
)
}
}
private fun setValidationProperties(active: Boolean, maskMode: Int, fixtureMode: Int = this.fixtureMode) {
this.active = active
this.maskMode = maskMode
@@ -509,23 +1086,13 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() {
Log.i(TAG, "ParticleGPU: event=MIX_PROPERTY_ONLY value=$timeMix styleEpoch=$styleLoadEpoch")
}
private fun setParticleCount(requestedParticleCount: Int) {
if (!styleReady) {
Log.i(TAG, "ParticleGPU: event=PARTICLE_COUNT_REJECTED reason=STYLE_LOADING requestedParticleCount=$requestedParticleCount styleEpoch=$styleLoadEpoch")
return
}
if (!enabled) {
Log.i(TAG, "ParticleGPU: event=PARTICLE_COUNT_REJECTED reason=DISABLED requestedParticleCount=$requestedParticleCount styleEpoch=$styleLoadEpoch")
return
}
Log.i(TAG, "ParticleGPU: event=PARTICLE_COUNT_REQUEST requestedParticleCount=$requestedParticleCount styleEpoch=$styleLoadEpoch")
if (!setParticleFloat("raster-particle-particle-count", requestedParticleCount.toFloat())) {
Log.i(TAG, "ParticleGPU: event=PARTICLE_COUNT_REJECTED reason=CORE_PROPERTY_REJECTED requestedParticleCount=$requestedParticleCount styleEpoch=$styleLoadEpoch")
return
}
private fun setParticleCount(requestedParticleCount: Int): Boolean {
if (requestedParticleCount !in MIN_PARTICLE_COUNT..MAX_PARTICLE_COUNT || !styleReady || !enabled) return false
if (!setParticleFloat("raster-particle-particle-count", requestedParticleCount.toFloat())) return false
particleCount = requestedParticleCount
status.text = statusText()
Log.i(TAG, "ParticleGPU: event=PARTICLE_COUNT_APPLIED requestedParticleCount=$requestedParticleCount effectiveParticleCount=$particleCount styleEpoch=$styleLoadEpoch")
Log.i(TAG, "PARTICLE_COUNT_CHANGED count=$particleCount")
return true
}
private fun setVisualFloat(property: String, requested: Float, apply: (Float) -> Unit) {
@@ -572,7 +1139,7 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() {
}
}
return runtimeResult == WeatherRuntime.CommandResult.ACCEPTED ||
(!realWeatherMode && runtimeResult == null && maplibreMap.setStyleLayerFloatProperty(PARTICLE_LAYER_ID, property, value))
(!usesRealWeatherAssets() && runtimeResult == null && maplibreMap.setStyleLayerFloatProperty(PARTICLE_LAYER_ID, property, value))
}
private fun resetVisualDefaults() {
@@ -647,11 +1214,15 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() {
}
}
timelineState = if (results.size == 4 && results.all { it.accepted() }) "READY" else "NOT READY"
fitRealWeatherBounds()
if (!navSeaPbfMode && !pureParticleMode) fitRealWeatherBounds()
status.text = statusText()
Log.i(TAG, "ParticleField: event=REAL_WEATHER_INITIAL_WINDOW_READY results=${results.joinToString(",") { it.name }} timeline=$timelineState")
Log.i(TAG, "WeatherColorLayer: event=WEATHER_COLOR_FIELD_READY source=$PARTICLE_SOURCE_ID scale=0..30mps")
Log.i(TAG, "ParticleField: event=REAL_WEATHER_COMPOSITE_READY source=$PARTICLE_SOURCE_ID basemap=GSI failSoft=true color=weather-color particle=raster-particle")
if (navSeaPbfMode) {
Log.i(TAG, "NAVSEA_PBF_PARTICLE_READY source=$PARTICLE_SOURCE_ID weatherColor=false")
} else if (!pureParticleMode) {
Log.i(TAG, "WeatherColorLayer: event=WEATHER_COLOR_FIELD_READY source=$PARTICLE_SOURCE_ID scale=0..30mps")
Log.i(TAG, "ParticleField: event=REAL_WEATHER_COMPOSITE_READY source=$PARTICLE_SOURCE_ID basemap=GSI failSoft=true color=weather-color particle=raster-particle")
}
}
private fun fitRealWeatherBounds() {
@@ -721,9 +1292,28 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() {
} else {
"EAST"
}
val playback = if (timeWindowMode) " · ${if (realWeatherMode) "REAL WEATHER" else if (hostSubmissionMode) "HOST" else if (productionFeedMode) "PRODUCTION" else "FIXTURE"} · TL $timelineState · $timelineSpeed" else ""
val summary = "$mask · $runtime · $fields$playback · MIX ${(timeMix * 100).toInt()}%"
return if (realWeatherMode) "$summary\n${realWeatherHudValues()}" else "$summary · $particleCount · W ${"%.1f".format(lineWidth)} L ${"%.3f".format(particleLength)} O ${(particleOpacity * 100).toInt()} T ${(trailOpacity * 100).toInt()} F ${(trailFade * 100).toInt()} C ${if (speedColorEnabled) "ON" else "OFF"}"
val playbackMode = when {
pureParticleMode -> "PURE PARTICLE"
purePbfMode -> "PURE PBF"
navSeaPbfMode -> "NAVSEA PBF"
realWeatherMode -> "REAL WEATHER"
hostSubmissionMode -> "HOST"
productionFeedMode -> "PRODUCTION"
else -> "FIXTURE"
}
val playback = if (timeWindowMode) " · $playbackMode · TL $timelineState · $timelineSpeed" else ""
val scene = when {
navSeaPbfMode -> "NAVSEA PBF · ${navSeaPbfPosition.label} · PBF ${if (navSeaPbfRenderEnabled) "ON" else "OFF"} · CACHE ${if (retainedCacheEnabled) "ON" else "OFF"} · "
pureParticleMode -> "PURE PARTICLE · CACHE ${if (retainedCacheEnabled) "ON" else "OFF"} · "
purePbfMode -> "PURE PBF · PBF ${if (navSeaPbfRenderEnabled) "ON" else "OFF"} · CACHE ${if (retainedCacheEnabled) "ON" else "OFF"} · "
else -> ""
}
val summary = "$scene$mask · $runtime · $fields$playback · MIX ${(timeMix * 100).toInt()}%"
return when {
purePbfMode -> summary
usesRealWeatherAssets() -> "$summary\n${realWeatherHudValues()}"
else -> "$summary · $particleCount · W ${"%.1f".format(lineWidth)} L ${"%.3f".format(particleLength)} O ${(particleOpacity * 100).toInt()} T ${(trailOpacity * 100).toInt()} F ${(trailFade * 100).toInt()} C ${if (speedColorEnabled) "ON" else "OFF"}"
}
}
private fun realWeatherHudValues(): String = "FPS ${realWeatherTargetFps.name.removePrefix("FPS_")} · P $particleCount · W ${"%.2f".format(lineWidth)} · L ${"%.4f".format(particleLength)} · O ${"%.2f".format(particleOpacity)} · T ${"%.2f".format(trailOpacity)} · F ${"%.2f".format(trailFade)}"
@@ -829,6 +1419,12 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() {
outState.putBoolean(STATE_TIME_WINDOW, timeWindowMode)
outState.putBoolean(STATE_HOST_SUBMISSION, hostSubmissionMode)
outState.putBoolean(STATE_REAL_WEATHER, realWeatherMode)
outState.putBoolean(STATE_PURE_PARTICLE, pureParticleMode)
outState.putBoolean(STATE_PURE_PBF, purePbfMode)
outState.putBoolean(STATE_NAVSEA_PBF, navSeaPbfMode)
outState.putString(STATE_NAVSEA_PBF_POSITION, navSeaPbfPosition.name)
outState.putBoolean(STATE_NAVSEA_PBF_RENDER_ENABLED, navSeaPbfRenderEnabled)
outState.putBoolean(STATE_RETAINED_CACHE_ENABLED, retainedCacheEnabled)
super.onSaveInstanceState(outState)
mapView.onSaveInstanceState(outState)
}
@@ -856,6 +1452,12 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() {
private const val STATE_TIME_WINDOW = "particle_validation.time_window"
private const val STATE_HOST_SUBMISSION = "particle_validation.host_submission"
private const val STATE_REAL_WEATHER = "particle_validation.real_weather"
private const val STATE_PURE_PARTICLE = "particle_validation.pure_particle"
private const val STATE_PURE_PBF = "particle_validation.pure_pbf"
private const val STATE_NAVSEA_PBF = "particle_validation.navsea_pbf"
private const val STATE_NAVSEA_PBF_POSITION = "particle_validation.navsea_pbf_position"
private const val STATE_NAVSEA_PBF_RENDER_ENABLED = "particle_validation.navsea_pbf_render_enabled"
private const val STATE_RETAINED_CACHE_ENABLED = "particle_validation.retained_cache_enabled"
private const val PARTICLE_LAYER_ID = "particle-raster-core"
private const val WEATHER_COLOR_LAYER_ID = "weather-color-real"
private const val PARTICLE_SOURCE_ID = "particle-fixture"
@@ -871,10 +1473,8 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() {
private const val PARTICLE_COUNT_2048 = 2048
private const val PARTICLE_COUNT_4096 = 4096
private const val PARTICLE_COUNT_8192 = 8192
private const val PARTICLE_COUNT_16384 = 16384
private const val PARTICLE_COUNT_65536 = 65536
private const val PARTICLE_COUNT_262144 = 262144
private const val PARTICLE_COUNT_1048576 = 1048576
private const val MIN_PARTICLE_COUNT = 1
private const val MAX_PARTICLE_COUNT = 10000
private const val DEFAULT_LINE_WIDTH = 2.0f
private const val DEFAULT_PARTICLE_LENGTH = 0.018f
private const val DEFAULT_PARTICLE_OPACITY = 1.0f
@@ -886,6 +1486,18 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() {
private const val REAL_WEATHER_PARTICLE_OPACITY = 0.75f
private const val REAL_WEATHER_TRAIL_OPACITY = 0.55f
private const val REAL_WEATHER_TRAIL_FADE = 0.88f
private val REAL_WEATHER_PARTICLE_COUNTS = intArrayOf(PARTICLE_COUNT_256, PARTICLE_COUNT_512, PARTICLE_COUNT_1024, PARTICLE_COUNT_2048, PARTICLE_COUNT_4096, PARTICLE_COUNT_8192, PARTICLE_COUNT_16384)
private const val NAVSEA_PBF_STYLE_URL = "https://pbf.jpcn.ccwu.cc/style/style.navsea-delivery-full-semantic-extentfix.json"
private const val NAVSEA_PBF_SOURCE_ID = "navsea_delivery"
private const val NAVSEA_PRODUCTION_LAYER_COUNT = 63
private const val NAVSEA_SEA_AREA_LAYER_ID = "sea-area-fill"
private const val NAVSEA_CLIP_OUTLINE_LAYER_ID = "clip-outline"
private const val NAVSEA_SPECIAL_PATTERN_LAYER_ID = "special-pattern-line-754"
private const val NAVSEA_HAZARD_POINTS_LAYER_ID = "hazard-points"
private const val NAVSEA_CAMERA_LATITUDE = 33.45
private const val NAVSEA_CAMERA_LONGITUDE = 129.97
private const val NAVSEA_CAMERA_ZOOM = 10.5
private const val NAVSEA_CAMERA_BEARING = 0.0
private const val NAVSEA_CAMERA_PITCH = 0.0
private val REAL_WEATHER_PARTICLE_COUNTS = intArrayOf(PARTICLE_COUNT_256, PARTICLE_COUNT_512, PARTICLE_COUNT_1024, PARTICLE_COUNT_2048, PARTICLE_COUNT_4096, PARTICLE_COUNT_8192, MAX_PARTICLE_COUNT)
}
}

View File

@@ -9,13 +9,16 @@
<Button android:id="@+id/validationMenuCount" style="@style/ParticleValidationButton" android:layout_width="0dp" android:layout_weight="1" android:text="COUNT" />
<Button android:id="@+id/validationMenuMap" style="@style/ParticleValidationButton" android:layout_width="0dp" android:layout_weight="1" android:text="MAP" />
<Button android:id="@+id/validationMenuRealWeather" style="@style/ParticleValidationButton" android:layout_width="0dp" android:layout_weight="1" android:text="REAL WEATHER" android:textSize="10sp" />
<Button android:id="@+id/validationMenuPbfMode" style="@style/ParticleValidationButton" android:layout_width="0dp" android:layout_weight="1" android:text="PBF MODE" android:textSize="9sp" />
<Button android:id="@+id/validationMenuNavSeaPbf" style="@style/ParticleValidationButton" android:layout_width="0dp" android:layout_weight="1" android:text="NAVSEA PBF" android:textSize="9sp" />
<Button android:id="@+id/validationMenuRetainedCache" style="@style/ParticleValidationButton" android:layout_width="0dp" android:layout_weight="1" android:text="CACHE ON" android:textSize="9sp" />
<Button android:id="@+id/validationMenuMore" style="@style/ParticleValidationButton" android:layout_width="0dp" android:layout_weight="1" android:text="MORE" />
</LinearLayout>
<!-- Existing action IDs are invisible menu targets; no permanent button grid. -->
<LinearLayout android:visibility="gone" android:layout_width="1dp" android:layout_height="1dp" android:orientation="vertical">
<Button style="@style/ParticleValidationButton" android:id="@+id/validationActive"/><Button style="@style/ParticleValidationButton" android:id="@+id/validationStatic"/><Button style="@style/ParticleValidationButton" android:id="@+id/validationDisable"/><Button style="@style/ParticleValidationButton" android:id="@+id/validationEnable"/><Button style="@style/ParticleValidationButton" android:id="@+id/validationGlobal"/><Button style="@style/ParticleValidationButton" android:id="@+id/validationOceanA"/><Button style="@style/ParticleValidationButton" android:id="@+id/validationOceanB"/>
<Button style="@style/ParticleValidationButton" android:id="@+id/validationUniformEast"/><Button style="@style/ParticleValidationButton" android:id="@+id/validationUniformNorth"/><Button style="@style/ParticleValidationButton" android:id="@+id/validationVortex"/><Button style="@style/ParticleValidationButton" android:id="@+id/validationTimeWindow"/><Button style="@style/ParticleValidationButton" android:id="@+id/validationMix0"/><Button style="@style/ParticleValidationButton" android:id="@+id/validationMix50"/><Button style="@style/ParticleValidationButton" android:id="@+id/validationMix100"/>
<Button style="@style/ParticleValidationButton" android:id="@+id/validationParticle256"/><Button style="@style/ParticleValidationButton" android:id="@+id/validationParticle1024"/><Button style="@style/ParticleValidationButton" android:id="@+id/validationParticle4096"/><Button style="@style/ParticleValidationButton" android:id="@+id/validationParticle16384"/><Button style="@style/ParticleValidationButton" android:id="@+id/validationParticle65536"/><Button style="@style/ParticleValidationButton" android:id="@+id/validationParticle262144"/><Button style="@style/ParticleValidationButton" android:id="@+id/validationParticle1048576"/>
<Button style="@style/ParticleValidationButton" android:id="@+id/validationParticle256"/><Button style="@style/ParticleValidationButton" android:id="@+id/validationParticle1024"/><Button style="@style/ParticleValidationButton" android:id="@+id/validationParticle4096"/><Button style="@style/ParticleValidationButton" android:id="@+id/validationParticle10000"/>
<Button style="@style/ParticleValidationButton" android:id="@+id/validationAdvanceWindow"/><Button style="@style/ParticleValidationButton" android:id="@+id/validationDuplicateWindow"/><Button style="@style/ParticleValidationButton" android:id="@+id/validationProductionInvalid"/><Button style="@style/ParticleValidationButton" android:id="@+id/validationHostFieldSubmit"/><Button style="@style/ParticleValidationButton" android:id="@+id/validationRealWeather"/><Button style="@style/ParticleValidationButton" android:id="@+id/validationHostFieldDuplicate"/><Button style="@style/ParticleValidationButton" android:id="@+id/validationHostFieldInvalid"/><Button style="@style/ParticleValidationButton" android:id="@+id/validationVisualDefault"/><Button style="@style/ParticleValidationButton" android:id="@+id/validationVisualSizeDown"/><Button style="@style/ParticleValidationButton" android:id="@+id/validationVisualSizeUp"/><Button style="@style/ParticleValidationButton" android:id="@+id/validationVisualLengthDown"/><Button style="@style/ParticleValidationButton" android:id="@+id/validationVisualLengthUp"/><Button style="@style/ParticleValidationButton" android:id="@+id/validationVisualOpacityDown"/><Button style="@style/ParticleValidationButton" android:id="@+id/validationVisualOpacityUp"/><Button style="@style/ParticleValidationButton" android:id="@+id/validationVisualTrailDown"/><Button style="@style/ParticleValidationButton" android:id="@+id/validationVisualTrailUp"/><Button style="@style/ParticleValidationButton" android:id="@+id/validationVisualFadeDown"/><Button style="@style/ParticleValidationButton" android:id="@+id/validationVisualFadeUp"/><Button style="@style/ParticleValidationButton" android:id="@+id/validationVisualColor"/><Button style="@style/ParticleValidationButton" android:id="@+id/validationDateline"/><Button style="@style/ParticleValidationButton" android:id="@+id/validationWorldWrap"/>
</LinearLayout>
</LinearLayout>

View File

@@ -28,6 +28,21 @@ NS_ASSUME_NONNULL_BEGIN
@protocol MLNFeature;
@protocol MLNLocationManager;
/** A pull-based snapshot of one map view's retained-underlay renderer state. */
MLN_EXPORT
@interface MLNRetainedUnderlayDiagnostics : NSObject
@property (nonatomic, readonly, getter=isEnabled) BOOL enabled;
@property (nonatomic, readonly) uint64_t build;
@property (nonatomic, readonly) uint64_t reuse;
@property (nonatomic, readonly) uint64_t fallback;
@property (nonatomic, readonly) uint64_t invalidation;
@property (nonatomic, copy, readonly) NSString *lastReason;
- (instancetype)init NS_UNAVAILABLE;
@end
/** Options for ``MLNMapView/decelerationRate``. */
typedef CGFloat MLNMapViewDecelerationRate NS_TYPED_EXTENSIBLE_ENUM;
@@ -2266,6 +2281,12 @@ of north, the map will automatically snap to exact north.
*/
@property (nonatomic) MLNMapDebugMaskOptions debugMask;
/** Enables or disables the map-scoped retained static underlay optimization. */
@property (nonatomic, getter=isRetainedUnderlayEnabled) BOOL retainedUnderlayEnabled;
/** Returns a thread-safe retained-underlay diagnostics snapshot without enabling periodic events. */
@property (nonatomic, readonly) MLNRetainedUnderlayDiagnostics *retainedUnderlayDiagnostics;
/**
Returns the status of the rendering statistics overlay.
*/

View File

@@ -239,6 +239,34 @@ int processIsTranslated() {
#endif
@interface MLNRetainedUnderlayDiagnostics ()
- (instancetype)initWithEnabled:(BOOL)enabled
build:(uint64_t)build
reuse:(uint64_t)reuse
fallback:(uint64_t)fallback
invalidation:(uint64_t)invalidation
lastReason:(NSString *)lastReason;
@end
@implementation MLNRetainedUnderlayDiagnostics
- (instancetype)initWithEnabled:(BOOL)enabled
build:(uint64_t)build
reuse:(uint64_t)reuse
fallback:(uint64_t)fallback
invalidation:(uint64_t)invalidation
lastReason:(NSString *)lastReason {
if ((self = [super init])) {
_enabled = enabled;
_build = build;
_reuse = reuse;
_fallback = fallback;
_invalidation = invalidation;
_lastReason = [lastReason copy];
}
return self;
}
@end
class MLNAnnotationContext;
const MLNMapViewDecelerationRate MLNMapViewDecelerationRateNormal =
@@ -3070,6 +3098,32 @@ static void *windowScreenContext = &windowScreenContext;
self.mbglMap.setDebug(options);
}
- (BOOL)isRetainedUnderlayEnabled {
return _mbglMap ? self.mbglMap.isRetainedUnderlayEnabled() : YES;
}
- (void)setRetainedUnderlayEnabled:(BOOL)enabled {
if (_mbglMap) self.mbglMap.setRetainedUnderlayEnabled(enabled);
}
- (MLNRetainedUnderlayDiagnostics *)retainedUnderlayDiagnostics {
if (!_mbglMap) {
return [[MLNRetainedUnderlayDiagnostics alloc] initWithEnabled:YES
build:0
reuse:0
fallback:0
invalidation:0
lastReason:@"unavailable"];
}
const auto snapshot = self.mbglMap.getRetainedUnderlayDiagnostics();
return [[MLNRetainedUnderlayDiagnostics alloc] initWithEnabled:snapshot.enabled
build:snapshot.build
reuse:snapshot.reuse
fallback:snapshot.fallback
invalidation:snapshot.invalidation
lastReason:[NSString stringWithUTF8String:snapshot.lastReason.c_str()]];
}
- (void)resetNorth {
MLNLogInfo(@"Resetting the map rotation to a northern heading — a direction of 0 degrees.");
[self resetNorthAnimated:YES];