From e48fa087e01e9710c5d0c18ab062726226bbb6bd Mon Sep 17 00:00:00 2001 From: OpenAI Codex Date: Thu, 3 Sep 2026 19:47:47 +0800 Subject: [PATCH] feat: add particle visual runtime controls --- ...oid_OpenGL正式粒子视觉与运行时渲染控制_v1.md | 69 ++++++++++ ...¼�粒子视觉与运行时渲染控制_Codex完成报告_v1.md | 66 ++++++++++ .../shaders/gl/particle_gpu_validation.hpp | 62 +++++++-- .../shaders/particle_gpu_validation_ubo.hpp | 5 +- .../style/layers/raster_particle_layer.hpp | 12 ++ .../src/cpp/native_map_view.cpp | 29 ++++- .../ParticleCoreLayerValidationActivity.kt | 98 +++++++++++++- ...ctivity_particle_core_layer_validation.xml | 35 +++++ .../src/main/res/values/strings.xml | 14 ++ shaders/particle_gpu_validation.fragment.glsl | 28 +++- shaders/particle_gpu_validation.vertex.glsl | 34 ++++- .../render_particle_validation_layer.cpp | 122 ++++++++++++++---- .../particle_render_layer_properties.hpp | 22 +++- .../style/layers/raster_particle_layer.cpp | 113 ++++++++++++++++ .../layers/raster_particle_layer_impl.hpp | 6 + test/style/style_layer.test.cpp | 34 +++++ 粒子开发记录.md | 9 ++ 17 files changed, 704 insertions(+), 54 deletions(-) create mode 100644 docs/50_Step记录/STEP-046_Android_OpenGL正式粒子视觉与运行时渲染控制_v1.md create mode 100644 docs/报告/40-05_Android_OpenGL正式粒子视觉与运行时渲染控制_Codex完成报告_v1.md diff --git a/docs/50_Step记录/STEP-046_Android_OpenGL正式粒子视觉与运行时渲染控制_v1.md b/docs/50_Step记录/STEP-046_Android_OpenGL正式粒子视觉与运行时渲染控制_v1.md new file mode 100644 index 000000000..4c6e22459 --- /dev/null +++ b/docs/50_Step记录/STEP-046_Android_OpenGL正式粒子视觉与运行时渲染控制_v1.md @@ -0,0 +1,69 @@ +# STEP-046 Android OpenGL 正式粒子视觉与运行时渲染控制 + +日期:2026-09-03 +状态:IMPLEMENTED / WAITING FOR ANDROID VALIDATION + +## 目标 + +在已关闭的 40-04 Runtime Field 链路上,将正式 `RasterParticleLayer` 的 full particle draw 从固定蓝色 point 扩展为 GPU 速度驱动的短 streak,并一次性提供运行时视觉属性和 Android 验收入口。 + +正式链路保持唯一: + +```text +RasterArraySource T0/T1 + -> GPU time interpolation + -> State A/B + -> Trail A/B + -> GPU full streak draw + -> Composite +``` + +## 实现 + +- 每个 State texel 仍代表一颗正式粒子。静态 VBO 仅保存该 texel 的两个 line endpoint 编码;不包含 live position、direction 或 color。 +- vertex shader 从当前 State write texture 和 T0/T1 Field textures 获取位置与 `mix` 后的速度,按既有边界将 `V north-positive` 转换为 Mercator `dy=-v`,生成 canonical map-space 短 streak。无效/NoData/NaN/近零速度会 fail-closed,不绘制该 streak。 +- fragment shader 用最终插值速度 magnitude 走稳定的蓝→绿→橙 ramp;COLOR 控制可关闭 ramp 回退为固定蓝色。相同 magnitude 的 EAST/NORTH 保持同色,vortex 可显示局部速度差异。 +- 新增 Core visual paint properties: + - `raster-particle-line-width` + - `raster-particle-particle-length` + - `raster-particle-particle-opacity` + - `raster-particle-trail-opacity` + - `raster-particle-trail-fade` + - `raster-particle-speed-color-enabled` +- 所有属性是 Render/Visual property,不是 Field 或 simulation state。其运行时更新经既有 `MapLibreMap -> NativeMapView -> JNI -> RasterParticleLayer` boolean thin bridge,调用强类型 Core setter;不通过 Android Java Layer peer。 +- Android Validation 页面加入 VISUAL DEFAULT、SIZE、LENGTH、OPACITY、TRAIL、FADE 与 COLOR 控制,另有直接 VORTEX fixture 与 256K capacity 档;状态栏保留当前可见参数;Activity recreation 保存这些值。 + +## 不变量与观测 + +- visual property 更新只更新 RenderLayer/UBO;不会 reload Style、reload Source、上传 Field、重建 T0/T1、reset State/Trail 或清 Trail。 +- 40-04 的 A/B→B/C→C/D、`OLD_T1 -> NEW_T0` GPU reuse、Duplicate→NOOP、`NO_PRELOADED_FIELD` fail-closed 保持不变。 +- `PARTICLE_DRAW_RESOURCES_*` 记录 `particleDrawCount=N` 与 `streakVertexCount=2N`,明确两个顶点仍是一颗粒子的线段,不改变 40-03 已验证容量 `>= 1,048,576` 的含义。 +- `VISUAL_PROPERTY_APPLIED` 记录参数、renderLayerInstanceId、resourceGeneration、fieldUploadCount、resetCount;不按 frame 高频输出。没有可靠 GPU timer,因此不虚构 GPU ms。 + +## 静态验证 + +- `node shaders/generate_shader_code.mjs` 连续运行两次,生成物稳定。 +- Linux OpenGL 精确编译:`raster_particle_layer.cpp.o`、`render_particle_validation_layer.cpp.o`、`style_layer.test.cpp.o`:PASS。 +- 新增 `RasterParticleVisualProperties` C++ unit-level coverage,检查默认、运行时 setter 和 visual 参数边界。 +- `clang-format` 与 `git diff --check`:PASS。 +- 未运行 Android Gradle、ADB、真机或 Jest。 + +## Android 单轮验收 + +```bash +cd /mnt/sda1/MapLibreParticleLab/platform/android + +./gradlew :MapLibreAndroidTestApp:assembleOpenglDebug \ + -Pandroid.injected.build.abi=x86_64 \ + --console=plain +``` + +APK:`./MapLibreAndroidTestApp/build/intermediates/apk/opengl/debug/MapLibreAndroidTestApp-opengl-debug.apk` + +用户在同一 APK 中执行 T1--T8:EAST/NORTH/vortex geometry、MIX=0/50/100 连续性、VISUAL controls property-only、STATIC/Camera/Rotation、1M baseline,以及 Runtime advance/reuse/NOOP/fail-closed 回归。 + +当前 T1--T8 全部为 `PENDING USER ANDROID VALIDATION`;未将任何 Android 视觉、Camera、Mask、Lifecycle 或 capacity 结果写成 PASS。 + +下一建议阶段:用户完成这一次综合 Android 验收并提供结构化日志;若全部通过,才可进入 40-06 时间序列 Playback/preload/cache 的规划。 + +本阶段不包含 WeatherColorLayer、网络/GRIB/provider、React Native、Vulkan、iOS/Metal、生产 Land/Ocean mask、粒子箭头贴图或 APK packaging 技术债。 diff --git a/docs/报告/40-05_Android_OpenGL正式粒子视觉与运行时渲染控制_Codex完成报告_v1.md b/docs/报告/40-05_Android_OpenGL正式粒子视觉与运行时渲染控制_Codex完成报告_v1.md new file mode 100644 index 000000000..6445c91da --- /dev/null +++ b/docs/报告/40-05_Android_OpenGL正式粒子视觉与运行时渲染控制_Codex完成报告_v1.md @@ -0,0 +1,66 @@ +# 40-05 Android OpenGL 正式粒子视觉与运行时渲染控制 — Codex 完成报告 + +日期:2026-09-03 +状态:IMPLEMENTED / WAITING FOR ANDROID VALIDATION + +## 结果 + +40-05 / STEP-046 已完成代码、Android 验收入口与静态验证,尚未进行 Android 构建或实体机验收,因此不能标记 PASS。 + +### A. 正式方向 geometry + +`RenderRasterParticleLayer` 的正式 full draw 不再画固定蓝色 point。每个 State texel 创建两个静态 line vertices;vertex shader 读取当前 State write、T0/T1 Field,并以 `timeMix` 取得最终 GPU velocity。方向由 `vec2(u, -v)` 在 Mercator 边界生成,所以继续遵守 `U=east-positive`、`V=north-positive`、`dy=-v`。没有 CPU 每粒子 direction/color、GPU readback、第二 simulation 或 Overlay。 + +### B/C. 视觉属性与速度颜色 + +Core `RasterParticleLayer` 具有 line width、streak length、particle/trail opacity、trail fade、speed-color enabled 的强类型 setter 和 style property 支持。速度色使用 shader 内蓝→绿→橙最小 ramp,输入是最终插值后的 magnitude;NoData/invalid velocity 在 vertex shader 直接不绘制。Legacy `ParticleValidationLayer` 的旧验证显示没有被改为正式视觉路径。 + +### D. Android runtime 控制 + +Android Test App 通过既有 `setStyleLayerFloatProperty()` boolean JNI bridge 调用 Core 强类型 setter,不建立 Java RasterParticleLayer peer。请求失败时 Kotlin 不写入本地 visual state;成功后才更新状态栏。新增控制:VISUAL DEFAULT、SIZE ±、LENGTH ±、OPACITY ±、TRAIL ±、FADE ±、COLOR,以及直接 VORTEX 与 256K capacity 入口。 + +### E. 观测和回归 + +资源日志现在明确区分 `particleDrawCount`(粒子数)与 `streakVertexCount`(两端点数)。visual property 变化输出一次结构化摘要,含 Field upload/reset counters 与 resource identity;没有每粒子或每帧日志,也没有虚构 GPU timer。 + +## 保持不变 + +- 40-02 T0/T1 GPU interpolation 和 MIX property-only。 +- 40-03 `N State texels -> N actual particle draw` 与已验证 capacity `>=1,048,576`。 +- 40-04 Runtime Field A/B→B/C→C/D、OLD_T1→NEW_T0 reuse、Duplicate NOOP、候选耗尽 fail-closed。 +- State/Trail ping-pong、STATIC frozen composite、mask reuse、camera/world wrap/antimeridian 和 rotation lifecycle。 + +视觉参数改变预期 `FIELD_UPLOAD delta=0`、`SIMULATION_RESET delta=0`、`STYLE_LOAD delta=0`;本实现没有 visual GPU resource rebuild。 + +## 已执行验证 + +- shader generator 连续两次运行:PASS,OpenGL generated header 已同步。 +- Linux OpenGL 精确对象编译: + - `CMakeFiles/mbgl-core.dir/src/mbgl/style/layers/raster_particle_layer.cpp.o` + - `CMakeFiles/mbgl-core.dir/src/mbgl/renderer/layers/render_particle_validation_layer.cpp.o` + - `test/CMakeFiles/mbgl-test.dir/style/style_layer.test.cpp.o` + - 结果:PASS。 +- C++ `RasterParticleVisualProperties` 最小测试已加入并成功编译。 +- `clang-format`、`git diff --check`:PASS。 + +未执行:Android Gradle、ADB、安装、真机、Jest。 + +## 用户单次 Android 验收命令 + +```bash +cd /mnt/sda1/MapLibreParticleLab/platform/android + +./gradlew :MapLibreAndroidTestApp:assembleOpenglDebug \ + -Pandroid.injected.build.abi=x86_64 \ + --console=plain +``` + +APK 路径:`./MapLibreAndroidTestApp/build/intermediates/apk/opengl/debug/MapLibreAndroidTestApp-opengl-debug.apk` + +用户随后在一轮安装中完成 T1--T8,重点检查 EAST/NORTH/vortex streak direction、MIX 连续性、visual property-only counters、STATIC/camera/rotation、1M 和 Runtime Field 回归。 + +T1--T8 目前均为 `PENDING USER ANDROID VALIDATION`,因此 Camera/Projection、Mask、Lifecycle/Rotation 和实际 1K/16K/64K/256K/1M streak capacity 只能标记为“验收入口已完成,尚未由 Android 实机验证”。 + +## 范围外 + +未实现 WeatherColorLayer、真实天气 HTTP/GRIB/provider、React Native、Vulkan、iOS/Metal、生产 mask、粒子箭头/旋转或 APK ZIP 技术债。 diff --git a/include/mbgl/shaders/gl/particle_gpu_validation.hpp b/include/mbgl/shaders/gl/particle_gpu_validation.hpp index 5fde5acc2..111d2bcfb 100644 --- a/include/mbgl/shaders/gl/particle_gpu_validation.hpp +++ b/include/mbgl/shaders/gl/particle_gpu_validation.hpp @@ -12,30 +12,54 @@ struct ShaderSource 3.5) { - // 40-03: a_pos is a static State texel index generated only when the - // particle capacity changes. The position itself remains GPU-owned. - vec2 stateUV = (a_pos + vec2(0.5)) / u_mask.xy; + // 40-05: a_pos.x packs State texel X plus a static line endpoint. + // State position, interpolated Field velocity, speed color and streak + // orientation are all evaluated on GPU; the CPU never supplies live + // particle position, direction, or color data. + float endpoint = mod(a_pos.x, 2.0) * 2.0 - 1.0; + vec2 stateIndex = vec2(floor(a_pos.x * 0.5), a_pos.y); + vec2 stateUV = (stateIndex + vec2(0.5)) / u_mask.xy; vec2 position = texture(u_state_texture, stateUV).rg; + vec4 field0 = texture(u_field_texture, position); + vec4 field1 = texture(u_next_field_texture, position); + vec2 velocity = mix(field0.rg * 2.0 - 1.0, field1.rg * 2.0 - 1.0, u_marker.x); + bool valid = field0.a >= 0.5 && field1.a >= 0.5 && !any(isnan(velocity)); + vec2 mercatorVelocity = vec2(velocity.x, -velocity.y); + float speed = length(mercatorVelocity); v_uv = position; v_world = position; - gl_PointSize = 3.0; - gl_Position = vec4(position * 2.0 - 1.0, 0.0, 1.0); + v_particle_velocity = mercatorVelocity; + v_particle_valid = valid && speed > 0.00001 ? 1.0 : 0.0; + if (v_particle_valid < 0.5) { + gl_Position = vec4(-2.0, -2.0, 0.0, 1.0); + } else { + vec2 direction = mercatorVelocity / speed; + vec2 streakPosition = position + direction * endpoint * u_visual.y * 0.5; + gl_Position = vec4(streakPosition * 2.0 - 1.0, 0.0, 1.0); + } } else { // VALIDATION ONLY: a_pos is canonical z0 world geometry; u_matrix is // PaintParameters::matrixForTile() for the selected MapLibre world copy. @@ -47,6 +71,7 @@ void main() { highp vec4 u_pass; highp vec4 u_marker; highp vec4 u_mask; + highp vec4 u_visual; highp mat4 u_matrix; }; @@ -57,6 +82,16 @@ uniform sampler2D u_next_field_texture; uniform sampler2D u_state_texture; in vec2 v_uv; in vec2 v_world; +in vec2 v_particle_velocity; +in float v_particle_valid; + +vec3 particleSpeedColor(float speed) { + float t = clamp(speed * 0.75, 0.0, 1.0); + vec3 slow = vec3(0.10, 0.45, 1.00); + vec3 medium = vec3(0.10, 0.95, 0.72); + vec3 fast = vec3(1.00, 0.48, 0.12); + return mix(mix(slow, medium, smoothstep(0.0, 0.55, t)), fast, smoothstep(0.45, 1.0, t)); +} void main() { vec4 previous = texture(u_previous, v_uv); @@ -88,7 +123,7 @@ void main() { // Formal Trail first fades the previous target. Its full-particle point // draw follows in the same target and consumes every State texel. if (u_mask.z > 0.5) { - fragColor = vec4(previous.rgb * u_marker.w, 1.0); + fragColor = vec4(previous.rgb * u_visual.w, 1.0); return; } @@ -102,10 +137,12 @@ void main() { vec3 color = previous.rgb * u_marker.w + marker * vec3(0.08, 0.55, 1.0); fragColor = vec4(min(color, vec3(1.0)), 1.0); } else if (mode > 3.5) { - // The vertex shader fetched this particle's State texel directly from - // the current State write texture. Every point contributes one formal - // particle draw; no CPU position data is involved per frame. - fragColor = vec4(0.08, 0.55, 1.0, 1.0); + if (v_particle_valid < 0.5) discard; + float speed = length(v_particle_velocity); + vec3 color = u_marker.y > 0.5 ? particleSpeedColor(speed) : vec3(0.08, 0.55, 1.0); + // Trail targets are RGB accumulation buffers. Opacity changes source + // energy only; it never alters State, Field textures, or time windows. + fragColor = vec4(color * u_visual.x, 1.0); } else { // The formal RasterParticle layer displays the Trail texture written by // the current State -> Trail pass. Legacy ParticleValidation keeps its @@ -114,8 +151,9 @@ void main() { float ocean = texture(u_mask_texture, v_world).a; if (u_pass.z > 0.5 && ocean < 0.5) discard; if (u_mask.z > 0.5) { - float alpha = clamp(intensity * 1.25, 0.0, 0.96); - fragColor = vec4(previous.rgb * alpha, alpha); + vec3 color = previous.rgb * u_visual.z; + float alpha = clamp(max(color.r, max(color.g, color.b)), 0.0, 0.96); + fragColor = vec4(color, alpha); return; } diff --git a/include/mbgl/shaders/particle_gpu_validation_ubo.hpp b/include/mbgl/shaders/particle_gpu_validation_ubo.hpp index 3f9bee9b6..cea4263b0 100644 --- a/include/mbgl/shaders/particle_gpu_validation_ubo.hpp +++ b/include/mbgl/shaders/particle_gpu_validation_ubo.hpp @@ -10,9 +10,12 @@ struct alignas(16) ParticleGpuValidationUBO { std::array pass; std::array marker; std::array mask; + // Formal RasterParticle visual controls: particle opacity, canonical + // streak length, trail opacity, and Trail A/B fade multiplier. + std::array visual; std::array matrix; }; -static_assert(sizeof(ParticleGpuValidationUBO) == 7 * 16); +static_assert(sizeof(ParticleGpuValidationUBO) == 8 * 16); } // namespace shaders } // namespace mbgl diff --git a/include/mbgl/style/layers/raster_particle_layer.hpp b/include/mbgl/style/layers/raster_particle_layer.hpp index ea50e57dc..d3b496ad2 100644 --- a/include/mbgl/style/layers/raster_particle_layer.hpp +++ b/include/mbgl/style/layers/raster_particle_layer.hpp @@ -18,6 +18,18 @@ public: void setTimeMix(float); uint32_t getParticleCount() const; void setParticleCount(uint32_t); + float getLineWidth() const; + void setLineWidth(float); + float getParticleLength() const; + void setParticleLength(float); + float getParticleOpacity() const; + void setParticleOpacity(float); + float getTrailOpacity() const; + void setTrailOpacity(float); + float getTrailFade() const; + void setTrailFade(float); + bool isSpeedColorEnabled() const; + void setSpeedColorEnabled(bool); class Impl; const Impl& impl() const; Mutable mutableImpl() const; diff --git a/platform/android/MapLibreAndroid/src/cpp/native_map_view.cpp b/platform/android/MapLibreAndroid/src/cpp/native_map_view.cpp index 87597d760..ada3e7cbc 100644 --- a/platform/android/MapLibreAndroid/src/cpp/native_map_view.cpp +++ b/platform/android/MapLibreAndroid/src/cpp/native_map_view.cpp @@ -1117,7 +1117,10 @@ jni::jboolean NativeMapView::setStyleLayerFloatProperty(JNIEnv& env, auto* layer = map->getStyle().getLayer(jni::Make(env, layerId)); if (!layer) return false; const auto property = jni::Make(env, propertyName); - if (property != "raster-particle-time-mix" && property != "raster-particle-particle-count") { + if (property != "raster-particle-time-mix" && property != "raster-particle-particle-count" && + property != "raster-particle-line-width" && property != "raster-particle-particle-length" && + property != "raster-particle-particle-opacity" && property != "raster-particle-trail-opacity" && + property != "raster-particle-trail-fade" && property != "raster-particle-speed-color-enabled") { mbgl::Log::Error(mbgl::Event::JNI, "ParticleGPU: event=STYLE_PROPERTY_SET result=UNSUPPORTED_PROPERTY"); return false; } @@ -1126,10 +1129,14 @@ jni::jboolean NativeMapView::setStyleLayerFloatProperty(JNIEnv& env, mbgl::Log::Error(mbgl::Event::JNI, "ParticleGPU: event=STYLE_PROPERTY_SET result=UNSUPPORTED_LAYER"); return false; } + if (!std::isfinite(value)) { + mbgl::Log::Error(mbgl::Event::JNI, "ParticleGPU: event=STYLE_PROPERTY_SET result=INVALID_FLOAT"); + return false; + } if (property == "raster-particle-time-mix") { particleLayer->setTimeMix(value); - } else { - if (!std::isfinite(value) || value < 0.0f || std::floor(value) != value) { + } else if (property == "raster-particle-particle-count") { + if (value < 0.0f || std::floor(value) != value) { mbgl::Log::Error(mbgl::Event::JNI, "ParticleGPU: event=STYLE_PROPERTY_SET result=INVALID_PARTICLE_COUNT"); return false; } @@ -1139,6 +1146,22 @@ jni::jboolean NativeMapView::setStyleLayerFloatProperty(JNIEnv& env, mbgl::Log::Error(mbgl::Event::JNI, "ParticleGPU: event=STYLE_PROPERTY_SET result=INVALID_PARTICLE_COUNT"); return false; } + } else if (property == "raster-particle-line-width") { + particleLayer->setLineWidth(value); + } else if (property == "raster-particle-particle-length") { + particleLayer->setParticleLength(value); + } else if (property == "raster-particle-particle-opacity") { + particleLayer->setParticleOpacity(value); + } else if (property == "raster-particle-trail-opacity") { + particleLayer->setTrailOpacity(value); + } else if (property == "raster-particle-trail-fade") { + particleLayer->setTrailFade(value); + } else { + if (value != 0.0f && value != 1.0f) { + mbgl::Log::Error(mbgl::Event::JNI, "ParticleGPU: event=STYLE_PROPERTY_SET result=INVALID_COLOR_MODE"); + return false; + } + particleLayer->setSpeedColorEnabled(value > 0.5f); } mbgl::Log::Info(mbgl::Event::JNI, "ParticleGPU: event=STYLE_PROPERTY_SET layerId=" + jni::Make(env, layerId) + diff --git a/platform/android/MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/activity/particle/ParticleCoreLayerValidationActivity.kt b/platform/android/MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/activity/particle/ParticleCoreLayerValidationActivity.kt index 34fb8ade1..3ea551938 100644 --- a/platform/android/MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/activity/particle/ParticleCoreLayerValidationActivity.kt +++ b/platform/android/MapLibreAndroidTestApp/src/main/java/org/maplibre/android/testapp/activity/particle/ParticleCoreLayerValidationActivity.kt @@ -24,6 +24,12 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() { private var fixtureMode = FIXTURE_UNIFORM_EAST private var timeMix = 0.0f private var particleCount = PARTICLE_COUNT_256 + private var lineWidth = DEFAULT_LINE_WIDTH + private var particleLength = DEFAULT_PARTICLE_LENGTH + private var particleOpacity = DEFAULT_PARTICLE_OPACITY + private var trailOpacity = DEFAULT_TRAIL_OPACITY + private var trailFade = DEFAULT_TRAIL_FADE + private var speedColorEnabled = true private var timeWindowMode = false private var styleLoadEpoch = 0L private var styleReady = false @@ -35,6 +41,12 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() { 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 + 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 + trailOpacity = savedInstanceState?.getFloat(STATE_TRAIL_OPACITY, DEFAULT_TRAIL_OPACITY) ?: DEFAULT_TRAIL_OPACITY + trailFade = savedInstanceState?.getFloat(STATE_TRAIL_FADE, DEFAULT_TRAIL_FADE) ?: DEFAULT_TRAIL_FADE + speedColorEnabled = savedInstanceState?.getBoolean(STATE_SPEED_COLOR, true) ?: true timeWindowMode = savedInstanceState?.getBoolean(STATE_TIME_WINDOW, false) ?: false super.onCreate(savedInstanceState) Log.i( @@ -89,6 +101,11 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() { fixtureMode = FIXTURE_UNIFORM_NORTH if (this::maplibreMap.isInitialized) loadValidationStyle(active, enabled, "UNIFORM_NORTH") } + findViewById