release: prepare MapLibre Particle Android v1 package
Some checks failed
validate-scripts / validate-scripts (push) Has been cancelled

This commit is contained in:
OpenAI Codex
2026-09-09 10:47:18 +08:00
parent 3664db0770
commit 17774373d0
28 changed files with 691 additions and 15 deletions

View File

@@ -0,0 +1,7 @@
# 变更记录
## 13.4.1-particle.1
- 基于 MapLibre Native 13.4.1 的 Android OpenGL Particle 发行线。
- 包含 WeatherColor、RasterParticle、HostField binary file ingress、Timeline / Playback 与 React Native WeatherRuntime。
- Native/RN binary ingress 已在 STEP-061 验证;GSI + 真实 WeatherV2 + Particle Android 综合验证已在 STEP-062 完成。

View File

@@ -0,0 +1,30 @@
BSD 2-Clause License
Copyright (c) 2021 MapLibre contributors
Copyright (c) 2018-2021 MapTiler.com
Copyright (c) 2014-2020 Mapbox
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in
the documentation and/or other materials provided with the
distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

View File

@@ -0,0 +1,9 @@
# @navsea/maplibre-particle-rn
`13.4.1-particle.1` 是 NavSea 维护的 MapLibre Particle React Native 入口。Consumer 只直接安装本包;它以唯一 `@maplibre/maplibre-react-native` native module 提供 `Map`、`Camera` 等 MapLibre RN API,并以 `WeatherRuntime` 提供既有 HostField/Timeline 低频控制。
Android 使用 `cc.navsea.maplibre:android-sdk-particle-opengl:13.4.1-particle.1`。Expo Consumer 应在 app config 加入 `@navsea/maplibre-particle-rn/app.plugin`;plugin 将 MLRN 对 OpenGL SDK 的请求替换为该唯一 NavSea artifact,避免官方与 Particle AAR 同时进入 APK。最终 registry 未决定时,可先安装本包的 `.tgz`,并由用户将正式 AAR 放入 Maven Local 或临时 staging。
WeatherColor 和 RasterParticle 是 Core-only style layer,不是 React 组件;请用 `createParticleWeatherStyle()` 或等价 style JSON 声明。binary submit 始终为 `metadataJson + local file:// payload`,通过 `WeatherRuntime.submitWindField()` 进入既有 Native/Core 链路;本包不创建第二套 Timeline 或 field ingress。
本包不包含 Test Host、WeatherV2 fixture、AAR、APK、cache 或凭证。发布前必须先通过正式 AAR 手工构建、tarball Consumer APK 和实体机 smoke;本任务不创建 release tag。

View File

@@ -0,0 +1,25 @@
plugins {
id "com.android.library"
id "org.jetbrains.kotlin.android"
}
apply plugin: "com.facebook.react"
android {
namespace "org.navsea.maplibre.particle.weather"
compileSdk 36
defaultConfig { minSdk 24 }
}
dependencies {
implementation "com.facebook.react:react-android"
implementation "org.jetbrains.kotlin:kotlin-stdlib:2.1.20"
implementation "cc.navsea.maplibre:android-sdk-particle-opengl:13.4.1-particle.1"
compileOnly project(":maplibre_maplibre-react-native")
}
react {
jsRootDir = file("../src/")
libraryName = "NavSeaMapLibreParticle"
codegenJavaPackageName = "org.navsea.maplibre.particle.weather"
}

View File

@@ -0,0 +1 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android" />

View File

@@ -0,0 +1,225 @@
package org.navsea.maplibre.particle.weather
import android.net.Uri
import android.view.View
import com.facebook.react.bridge.Arguments
import com.facebook.react.bridge.Promise
import com.facebook.react.bridge.ReactApplicationContext
import com.facebook.react.turbomodule.core.interfaces.TurboModule
import com.facebook.react.bridge.ReactMethod
import com.facebook.react.bridge.ReadableArray
import com.facebook.react.bridge.ReadableMap
import com.facebook.react.uimanager.IllegalViewOperationException
import com.facebook.react.uimanager.UIManagerHelper
import com.facebook.react.uimanager.common.UIManagerType
import org.maplibre.android.maps.MapLibreMap
import org.maplibre.android.maps.Style
import org.maplibre.android.maps.WeatherRuntime
import org.maplibre.reactnative.components.mapview.MLRNMapView
import java.io.ByteArrayOutputStream
import java.io.File
import java.io.IOException
import java.io.InputStream
/**
* A low-frequency, per-view bridge. It resolves the Fabric view for each call and never stores a
* reactTag, current map, timeline, playback state, or a second Map registry.
*/
class WeatherRuntimeBridgeModule(private val context: ReactApplicationContext) : NativeWeatherRuntimeBridgeSpec(context), TurboModule {
@ReactMethod
override fun attach(reactTag: Double, promise: Promise) = withMapView(reactTag, promise) { view, map ->
val style = map.style ?: return@withMapView reject(promise, "STYLE_NOT_READY", "The MapLibre style is not loaded")
val sourceExists = map.hasStyleSource(WeatherIds.SOURCE)
val weatherColorLayerExists = map.hasStyleLayer(WeatherIds.COLOR_LAYER)
val particleLayerExists = map.hasStyleLayer(WeatherIds.PARTICLE_LAYER)
if (!sourceExists || !weatherColorLayerExists || !particleLayerExists) {
return@withMapView reject(
promise,
"WEATHER_OBJECTS_NOT_READY",
weatherObjectsNotReadyDiagnostic(
reactTag.toInt(),
style,
sourceExists,
weatherColorLayerExists,
particleLayerExists,
),
)
}
val runtime = map.replaceWeatherRuntime(WeatherRuntime.Config(WeatherIds.SOURCE, WeatherIds.COLOR_LAYER, WeatherIds.PARTICLE_LAYER))
// MLRN drops its MapView by detaching it. This listener captures only that concrete map,
// disposes its map-owned facade, and cannot target a replacement view.
view.addOnAttachStateChangeListener(object : View.OnAttachStateChangeListener {
override fun onViewAttachedToWindow(v: View) = Unit
override fun onViewDetachedFromWindow(v: View) { map.disposeWeatherRuntime() }
})
promise.resolve(snapshot(runtime))
}
@ReactMethod
override fun detach(reactTag: Double, promise: Promise) = withMap(reactTag, promise) { map ->
if (map.weatherRuntime == null) reject(promise, "RUNTIME_DISPOSED", "No attached WeatherRuntime exists for this map")
else { map.disposeWeatherRuntime(); promise.resolve(null) }
}
@ReactMethod override fun getState(reactTag: Double, promise: Promise) = withRuntime(reactTag, promise) { runtime -> promise.resolve(snapshot(runtime)) }
@ReactMethod override fun clearWindFields(reactTag: Double, promise: Promise) = command(reactTag, promise) { it.clearWindFields() }
/** Binary ingress is a local file reference, never a JS number array or encoded string. */
@ReactMethod
override fun submitWindField(reactTag: Double, metadataJson: String, payloadFileUri: String, promise: Promise) {
if (metadataJson.isEmpty() || metadataJson.toByteArray(Charsets.UTF_8).size > MAX_METADATA_UTF8_BYTES) return reject(promise, "INVALID_ARGUMENT", "metadataJson UTF-8 payload must be non-empty and at most $MAX_METADATA_UTF8_BYTES bytes")
val payload = try { readLocalPayload(payloadFileUri) }
catch (error: IllegalArgumentException) { return reject(promise, "PAYLOAD_FILE_UNREADABLE", error.message ?: "payload file is unreadable") }
catch (error: IOException) { return reject(promise, "PAYLOAD_FILE_UNREADABLE", "payload file could not be read: ${error.message ?: "I/O error"}") }
catch (error: RuntimeException) { return reject(promise, "PAYLOAD_FILE_UNREADABLE", "payload file could not be read") }
if (payload.isEmpty()) return reject(promise, "PAYLOAD_FILE_UNREADABLE", "payload file is empty")
withRuntime(reactTag, promise) { runtime ->
try { promise.resolve(runtime.submitWindField(metadataJson, payload).name) }
catch (error: IllegalStateException) { reject(promise, "NATIVE_REJECTED", error.message ?: "WeatherRuntime rejected the field") }
}
}
@ReactMethod override fun setWeatherEnabled(reactTag: Double, enabled: Boolean, promise: Promise) = command(reactTag, promise) { it.setWeatherEnabled(enabled) }
@ReactMethod override fun setWeatherColorVisible(reactTag: Double, visible: Boolean, promise: Promise) = command(reactTag, promise) { it.setWeatherColorVisible(visible) }
@ReactMethod override fun setParticleVisible(reactTag: Double, visible: Boolean, promise: Promise) = command(reactTag, promise) { it.setParticleVisible(visible) }
@ReactMethod override fun setTargetFps(reactTag: Double, target: String, promise: Promise) = enumCommand(reactTag, promise, target, WeatherRuntime.TargetFps.values()) { runtime, value -> runtime.setTargetFps(value) }
@ReactMethod override fun setPlaybackState(reactTag: Double, playback: String, promise: Promise) = enumCommand(reactTag, promise, playback, WeatherRuntime.PlaybackState.values()) { runtime, value -> runtime.setPlaybackState(value) }
@ReactMethod override fun setPlaybackSpeed(reactTag: Double, speed: String, promise: Promise) = enumCommand(reactTag, promise, speed, WeatherRuntime.PlaybackSpeed.values()) { runtime, value -> runtime.setPlaybackSpeed(value) }
@ReactMethod
override fun setForecastTime(reactTag: Double, timestampMs: Double, promise: Promise) {
if (!timestampMs.isFinite() || timestampMs < 0 || timestampMs > Long.MAX_VALUE.toDouble() || timestampMs != kotlin.math.floor(timestampMs)) {
return reject(promise, "INVALID_ARGUMENT", "forecast timestamp must be a non-negative epoch millisecond integer")
}
command(reactTag, promise) { it.setForecastTime(timestampMs.toLong()) }
}
@ReactMethod override fun setParticleCount(reactTag: Double, count: Double, promise: Promise) {
if (!count.isFinite() || count < 0 || count > Int.MAX_VALUE || count != kotlin.math.floor(count)) return reject(promise, "INVALID_ARGUMENT", "particle count must be a non-negative integer")
command(reactTag, promise) { it.setParticleCount(count.toInt()) }
}
@ReactMethod override fun setParticleLineWidth(reactTag: Double, value: Double, promise: Promise) = floatCommand(reactTag, value, promise) { runtime, valid -> runtime.setParticleLineWidth(valid) }
@ReactMethod override fun setParticleLength(reactTag: Double, value: Double, promise: Promise) = floatCommand(reactTag, value, promise) { runtime, valid -> runtime.setParticleLength(valid) }
@ReactMethod override fun setParticleOpacity(reactTag: Double, value: Double, promise: Promise) = floatCommand(reactTag, value, promise) { runtime, valid -> runtime.setParticleOpacity(valid) }
@ReactMethod override fun setTrailOpacity(reactTag: Double, value: Double, promise: Promise) = floatCommand(reactTag, value, promise) { runtime, valid -> runtime.setTrailOpacity(valid) }
@ReactMethod override fun setTrailFade(reactTag: Double, value: Double, promise: Promise) = floatCommand(reactTag, value, promise) { runtime, valid -> runtime.setTrailFade(valid) }
@ReactMethod override fun setWeatherColorOpacity(reactTag: Double, value: Double, promise: Promise) = floatCommand(reactTag, value, promise) { runtime, valid -> runtime.setWeatherColorOpacity(valid) }
@ReactMethod override fun setParticleColor(reactTag: Double, red: Double, green: Double, blue: Double, alpha: Double, promise: Promise) = colorCommand(reactTag, red, green, blue, alpha, promise) { runtime, r, g, b, a -> runtime.setParticleColor(r, g, b, a) }
@ReactMethod override fun setTrailColor(reactTag: Double, red: Double, green: Double, blue: Double, alpha: Double, promise: Promise) = colorCommand(reactTag, red, green, blue, alpha, promise) { runtime, r, g, b, a -> runtime.setTrailColor(r, g, b, a) }
@ReactMethod
override fun setWeatherColorStops(reactTag: Double, stops: ReadableArray, promise: Promise) {
val parsed = ArrayList<WeatherRuntime.ColorStop>()
for (index in 0 until stops.size()) {
val stop: ReadableMap = try { stops.getMap(index) ?: throw IllegalArgumentException() } catch (_: RuntimeException) { return reject(promise, "INVALID_ARGUMENT", "color stop $index must be an object") }
try {
val speed = stop.getDouble("speedMps"); val red = stop.getDouble("red"); val green = stop.getDouble("green"); val blue = stop.getDouble("blue"); val alpha = stop.getDouble("alpha")
if (!speed.isFinite() || !red.isFinite() || !green.isFinite() || !blue.isFinite() || !alpha.isFinite() || red !in 0.0..1.0 || green !in 0.0..1.0 || blue !in 0.0..1.0 || alpha !in 0.0..1.0 || (parsed.isNotEmpty() && speed <= parsed.last().speedMps)) {
return reject(promise, "INVALID_ARGUMENT", "stops require finite, strictly increasing speedMps and finite RGBA in [0,1]")
}
parsed.add(WeatherRuntime.ColorStop(speed.toFloat(), red.toFloat(), green.toFloat(), blue.toFloat(), alpha.toFloat()))
} catch (_: RuntimeException) { return reject(promise, "INVALID_ARGUMENT", "color stop $index is incomplete") }
}
command(reactTag, promise) { it.setWeatherColorStops(parsed) }
}
private fun command(reactTag: Double, promise: Promise, fn: (WeatherRuntime) -> WeatherRuntime.CommandResult) = withRuntime(reactTag, promise) { runtime ->
try {
promise.resolve(fn(runtime).name)
} catch (error: IllegalStateException) {
// WeatherRuntime rejects disabled/disposed commands by contract. Do not let an
// exception escape the UI queue or turn a stale command into an implicit success.
reject(promise, "NATIVE_REJECTED", error.message ?: "WeatherRuntime rejected the command")
}
}
private fun floatCommand(reactTag: Double, value: Double, promise: Promise, fn: (WeatherRuntime, Float) -> WeatherRuntime.CommandResult) {
if (!value.isFinite()) return reject(promise, "INVALID_ARGUMENT", "value must be finite")
command(reactTag, promise) { fn(it, value.toFloat()) }
}
private fun colorCommand(reactTag: Double, red: Double, green: Double, blue: Double, alpha: Double, promise: Promise, fn: (WeatherRuntime, Float, Float, Float, Float) -> WeatherRuntime.CommandResult) {
if (listOf(red, green, blue, alpha).any { !it.isFinite() || it !in 0.0..1.0 }) return reject(promise, "INVALID_ARGUMENT", "RGBA must be finite values in [0,1]")
command(reactTag, promise) { fn(it, red.toFloat(), green.toFloat(), blue.toFloat(), alpha.toFloat()) }
}
private fun <T : Enum<T>> enumCommand(reactTag: Double, promise: Promise, raw: String, values: Array<T>, fn: (WeatherRuntime, T) -> WeatherRuntime.CommandResult) {
val value = values.firstOrNull { it.name == raw } ?: return reject(promise, "INVALID_ARGUMENT", "unsupported enum value: $raw")
command(reactTag, promise) { fn(it, value) }
}
private fun readLocalPayload(reference: String): ByteArray {
if (reference.isBlank()) throw IllegalArgumentException("payload file reference is empty")
val uri = Uri.parse(reference)
if (uri.scheme != null && uri.scheme != "file") throw IllegalArgumentException("payload file reference must use file:// or an absolute local path")
val file = if (uri.scheme == "file") File(uri.path ?: throw IllegalArgumentException("payload file URI has no path")) else File(reference).also {
if (!it.isAbsolute) throw IllegalArgumentException("payload path without a URI scheme must be absolute")
}
if (!file.isFile || !file.canRead()) throw IllegalArgumentException("payload file is not a readable local file")
if (file.length() <= 0L || file.length() > MAX_PAYLOAD_BYTES) throw IllegalArgumentException("payload file size must be in 1..$MAX_PAYLOAD_BYTES bytes")
return file.inputStream().use(::readBounded)
}
private fun readBounded(input: InputStream): ByteArray {
val output = ByteArrayOutputStream(); val buffer = ByteArray(DEFAULT_READ_BUFFER_BYTES)
while (true) {
val count = input.read(buffer)
if (count < 0) break
if (output.size().toLong() + count > MAX_PAYLOAD_BYTES) throw IllegalArgumentException("payload file exceeds $MAX_PAYLOAD_BYTES bytes")
output.write(buffer, 0, count)
}
return output.toByteArray()
}
private fun withRuntime(reactTag: Double, promise: Promise, fn: (WeatherRuntime) -> Unit) = withMap(reactTag, promise) { map ->
val runtime = map.weatherRuntime
if (runtime == null || runtime.isDisposed) reject(promise, "RUNTIME_DISPOSED", "WeatherRuntime is not attached to this map") else fn(runtime)
}
private fun withMap(reactTag: Double, promise: Promise, fn: (MapLibreMap) -> Unit) = withMapView(reactTag, promise) { _, map -> fn(map) }
private fun withMapView(reactTag: Double, promise: Promise, fn: (MLRNMapView, MapLibreMap) -> Unit) {
if (!reactTag.isFinite() || reactTag <= 0 || reactTag != kotlin.math.floor(reactTag)) return reject(promise, "INVALID_ARGUMENT", "reactTag must be a positive integer")
context.runOnUiQueueThread {
val view = try { UIManagerHelper.getUIManager(context, UIManagerType.FABRIC)?.resolveView(reactTag.toInt()) } catch (_: IllegalViewOperationException) { null }
if (view !is MLRNMapView) return@runOnUiQueueThread reject(promise, "MAP_VIEW_NOT_FOUND", "reactTag does not resolve to a current MLRNMapView")
val map = view.mapLibreMap ?: return@runOnUiQueueThread reject(promise, "MAP_NOT_READY", "MLRNMapView has no MapLibreMap yet")
fn(view, map)
}
}
private fun snapshot(runtime: WeatherRuntime) = Arguments.createMap().apply {
putString("state", runtime.state.name); putString("playbackState", runtime.playbackState.name); putString("targetFps", runtime.targetFps.name); putBoolean("disposed", runtime.isDisposed)
}
/**
* Core-only layer types do not have a Java Layer peer. Presence is checked directly in Core
* before this diagnostic is built; the style URI distinguishes a JSON style (empty URI) from
* a URI-backed style without materialising all sources or layers.
*/
private fun weatherObjectsNotReadyDiagnostic(
reactTag: Int,
style: Style,
sourceExists: Boolean,
weatherColorLayerExists: Boolean,
particleLayerExists: Boolean,
): String {
return buildString {
append("WEATHER_OBJECTS_NOT_READY reactTag=").append(reactTag)
append(" mapView=present mapLibreMap=present style=present")
append(" styleUri=").append(style.getUri().ifEmpty { "<empty>" })
append(" source[").append(WeatherIds.SOURCE).append("]=")
.append(presence(sourceExists))
append(" layer[").append(WeatherIds.COLOR_LAYER).append("]=")
.append(presence(weatherColorLayerExists))
append(" layer[").append(WeatherIds.PARTICLE_LAYER).append("]=")
.append(presence(particleLayerExists))
}
}
private fun presence(exists: Boolean) = if (exists) "present" else "missing"
private fun reject(promise: Promise, code: String, message: String) = promise.reject(code, message)
private object WeatherIds {
const val SOURCE = "rn-weather-fields"
const val COLOR_LAYER = "rn-weather-color"
const val PARTICLE_LAYER = "rn-weather-particles"
}
companion object {
const val NAME = "WeatherRuntimeBridge"
private const val MAX_METADATA_UTF8_BYTES = 1_048_576
private const val MAX_PAYLOAD_BYTES = 64L * 1024L * 1024L
private const val DEFAULT_READ_BUFFER_BYTES = 16 * 1024
}
}

View File

@@ -0,0 +1,20 @@
package org.navsea.maplibre.particle.weather
import com.facebook.react.bridge.NativeModule
import com.facebook.react.bridge.ReactApplicationContext
import com.facebook.react.module.model.ReactModuleInfo
import com.facebook.react.module.model.ReactModuleInfoProvider
import com.facebook.react.TurboReactPackage
class WeatherRuntimeBridgePackage : TurboReactPackage() {
override fun getModule(name: String, context: ReactApplicationContext): NativeModule? =
if (name == WeatherRuntimeBridgeModule.NAME) WeatherRuntimeBridgeModule(context) else null
override fun getReactModuleInfoProvider(): ReactModuleInfoProvider = ReactModuleInfoProvider {
mapOf(WeatherRuntimeBridgeModule.NAME to ReactModuleInfo(
WeatherRuntimeBridgeModule.NAME,
WeatherRuntimeBridgeModule::class.java.name,
false, false, false, false, true
))
}
}

View File

@@ -0,0 +1,12 @@
const { withProjectBuildGradle } = require("@expo/config-plugins");
const marker = "// @navsea/maplibre-particle-rn Android OpenGL release";
/** Resolves MLRN's single Native SDK request to the NavSea release artifact, never a second AAR. */
module.exports = function withNavSeaMapLibreParticle(config) {
return withProjectBuildGradle(config, (project) => {
if (project.modResults.contents.includes(marker)) return project;
project.modResults.contents += `\n${marker}\ngradle.projectsEvaluated {\n allprojects { p ->\n p.repositories.mavenLocal()\n p.configurations.configureEach { configuration ->\n configuration.resolutionStrategy.dependencySubstitution {\n substitute module("org.maplibre.gl:android-sdk-opengl") using module("cc.navsea.maplibre:android-sdk-particle-opengl:13.4.1-particle.1")\n }\n }\n }\n}\nrootProject.ext.set("org.maplibre.reactnative.nativeVersion", "13.4.1")\nrootProject.ext.set("org.maplibre.reactnative.nativeVariant", "opengl")\n`;
return project;
});
};

View File

@@ -0,0 +1,36 @@
{
"name": "@navsea/maplibre-particle-rn",
"version": "13.4.1-particle.1",
"description": "NavSea MapLibre Particle React Native facade for the Android OpenGL particle release.",
"license": "BSD-2-Clause",
"main": "src/index.ts",
"react-native": "src/index.ts",
"types": "src/index.ts",
"files": [
"src",
"android",
"app.plugin.js",
"react-native.config.js",
"README.md",
"CHANGELOG.md",
"LICENSE.md"
],
"keywords": ["navsea", "maplibre", "particle", "react-native", "weather"],
"repository": { "type": "git", "url": "https://git.eyaku.jp/tei/MapLibreParticleLab" },
"dependencies": {
"@maplibre/maplibre-react-native": "11.3.6"
},
"peerDependencies": {
"react": ">=19.1.0",
"react-native": ">=0.80.0"
},
"peerDependenciesMeta": {
"expo": { "optional": true }
},
"codegenConfig": {
"name": "WeatherRuntimeBridgeSpec",
"type": "modules",
"jsSrcsDir": "src",
"android": { "javaPackageName": "org.navsea.maplibre.particle.weather" }
}
}

View File

@@ -0,0 +1 @@
module.exports = { dependency: { platforms: { android: { sourceDir: "./android" } } } };

View File

@@ -0,0 +1,37 @@
import type { TurboModule } from "react-native";
import { TurboModuleRegistry } from "react-native";
type RuntimeState = "DISABLED" | "WAITING_FOR_FIELDS" | "READY" | "ERROR";
type PlaybackState = "PLAY" | "PAUSE";
type TargetFps = "AUTO" | "FPS_30" | "FPS_60";
type CommandResult = "ACCEPTED" | "REJECTED";
type FieldSubmitResult = "REJECTED_SOURCE_MODE" | "REJECTED_ADAPTER" | "REJECTED_INCOMPATIBLE" | "REJECTED_NON_MONOTONIC" | "REJECTED_CAPACITY" | "ACCEPTED_PENDING_INITIAL_WINDOW" | "INITIAL_WINDOW_READY" | "ACCEPTED_NEXT_SNAPSHOT" | "DUPLICATE" | "UNKNOWN";
type ColorStop = { speedMps: number; red: number; green: number; blue: number; alpha: number };
type RuntimeSnapshot = { state: RuntimeState; playbackState: PlaybackState; targetFps: TargetFps; disposed: boolean };
export interface Spec extends TurboModule {
attach(reactTag: number): Promise<RuntimeSnapshot>;
detach(reactTag: number): Promise<void>;
getState(reactTag: number): Promise<RuntimeSnapshot>;
clearWindFields(reactTag: number): Promise<CommandResult>;
submitWindField(reactTag: number, metadataJson: string, payloadFileUri: string): Promise<FieldSubmitResult>;
setWeatherEnabled(reactTag: number, enabled: boolean): Promise<CommandResult>;
setWeatherColorVisible(reactTag: number, visible: boolean): Promise<CommandResult>;
setParticleVisible(reactTag: number, visible: boolean): Promise<CommandResult>;
setTargetFps(reactTag: number, target: TargetFps): Promise<CommandResult>;
setForecastTime(reactTag: number, timestampMs: number): Promise<CommandResult>;
setPlaybackState(reactTag: number, playback: PlaybackState): Promise<CommandResult>;
setPlaybackSpeed(reactTag: number, speed: "HALF" | "NORMAL" | "DOUBLE"): Promise<CommandResult>;
setWeatherColorStops(reactTag: number, stops: ColorStop[]): Promise<CommandResult>;
setParticleColor(reactTag: number, red: number, green: number, blue: number, alpha: number): Promise<CommandResult>;
setTrailColor(reactTag: number, red: number, green: number, blue: number, alpha: number): Promise<CommandResult>;
setParticleCount(reactTag: number, count: number): Promise<CommandResult>;
setParticleLineWidth(reactTag: number, value: number): Promise<CommandResult>;
setParticleLength(reactTag: number, value: number): Promise<CommandResult>;
setParticleOpacity(reactTag: number, value: number): Promise<CommandResult>;
setTrailOpacity(reactTag: number, value: number): Promise<CommandResult>;
setTrailFade(reactTag: number, value: number): Promise<CommandResult>;
setWeatherColorOpacity(reactTag: number, value: number): Promise<CommandResult>;
}
export default TurboModuleRegistry.getEnforcing<Spec>("WeatherRuntimeBridge");

View File

@@ -0,0 +1,39 @@
export * from "@maplibre/maplibre-react-native";
export { default as WeatherRuntime } from "./NativeWeatherRuntimeBridge";
export type {
ColorStop,
CommandResult,
FieldSubmitResult,
PlaybackState,
RuntimeSnapshot,
RuntimeState,
TargetFps,
} from "./types";
export const particleWeatherIds = {
source: "rn-weather-fields",
weatherColorLayer: "rn-weather-color",
rasterParticleLayer: "rn-weather-particles",
} as const;
/**
* Core-only WeatherColor/RasterParticle layers are declared in a MapLibre style, rather than as
* React components. This helper reserves their stable IDs without creating a second field ingress.
*/
export function createParticleWeatherStyle(overrides: Record<string, unknown> = {}) {
return JSON.stringify({
version: 8,
sources: {
"gsi-standard": { type: "raster", tiles: ["https://cyberjapandata.gsi.go.jp/xyz/std/{z}/{x}/{y}.png"], tileSize: 256, maxzoom: 18, attribution: "国土地理院 / GSI" },
[particleWeatherIds.source]: { type: "raster-array", "host-field-submission": true },
},
layers: [
{ id: "particle-background", type: "background", paint: { "background-color": "#10243a" } },
{ id: "gsi-standard", type: "raster", source: "gsi-standard", paint: { "raster-opacity": 1 } },
{ id: particleWeatherIds.weatherColorLayer, type: "weather-color", source: particleWeatherIds.source, paint: { "weather-color-opacity": 0.62 } },
{ id: particleWeatherIds.rasterParticleLayer, type: "raster-particle", source: particleWeatherIds.source,
paint: { "raster-particle-active": true, "raster-particle-particle-count": 4096 } },
],
...overrides,
});
}

View File

@@ -0,0 +1,7 @@
export type RuntimeState = "DISABLED" | "WAITING_FOR_FIELDS" | "READY" | "ERROR";
export type PlaybackState = "PLAY" | "PAUSE";
export type TargetFps = "AUTO" | "FPS_30" | "FPS_60";
export type CommandResult = "ACCEPTED" | "REJECTED";
export type FieldSubmitResult = "REJECTED_SOURCE_MODE" | "REJECTED_ADAPTER" | "REJECTED_INCOMPATIBLE" | "REJECTED_NON_MONOTONIC" | "REJECTED_CAPACITY" | "ACCEPTED_PENDING_INITIAL_WINDOW" | "INITIAL_WINDOW_READY" | "ACCEPTED_NEXT_SNAPSHOT" | "DUPLICATE" | "UNKNOWN";
export type ColorStop = Readonly<{ speedMps: number; red: number; green: number; blue: number; alpha: number }>;
export type RuntimeSnapshot = Readonly<{ state: RuntimeState; playbackState: PlaybackState; targetFps: TargetFps; disposed: boolean }>;

View File

@@ -0,0 +1,17 @@
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@maplibre/maplibre-react-native": ["../../integration/react-native/MapLibreParticleRNTestHost/node_modules/@maplibre/maplibre-react-native/lib/typescript/module/index.d.ts"],
"react-native": ["../../integration/react-native/MapLibreParticleRNTestHost/node_modules/react-native/types/index.d.ts"]
},
"strict": true,
"noEmit": true,
"module": "esnext",
"moduleResolution": "bundler",
"jsx": "react-jsx",
"target": "esnext",
"skipLibCheck": true
},
"include": ["src"]
}