release: prepare MapLibre Particle Android v1 package
Some checks failed
validate-scripts / validate-scripts (push) Has been cancelled
Some checks failed
validate-scripts / validate-scripts (push) Has been cancelled
This commit is contained in:
7
packages/maplibre-particle-rn/CHANGELOG.md
Normal file
7
packages/maplibre-particle-rn/CHANGELOG.md
Normal 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 完成。
|
||||
30
packages/maplibre-particle-rn/LICENSE.md
Normal file
30
packages/maplibre-particle-rn/LICENSE.md
Normal 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.
|
||||
9
packages/maplibre-particle-rn/README.md
Normal file
9
packages/maplibre-particle-rn/README.md
Normal 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。
|
||||
25
packages/maplibre-particle-rn/android/build.gradle
Normal file
25
packages/maplibre-particle-rn/android/build.gradle
Normal 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"
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android" />
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
))
|
||||
}
|
||||
}
|
||||
12
packages/maplibre-particle-rn/app.plugin.js
Normal file
12
packages/maplibre-particle-rn/app.plugin.js
Normal 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;
|
||||
});
|
||||
};
|
||||
36
packages/maplibre-particle-rn/package.json
Normal file
36
packages/maplibre-particle-rn/package.json
Normal 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" }
|
||||
}
|
||||
}
|
||||
1
packages/maplibre-particle-rn/react-native.config.js
Normal file
1
packages/maplibre-particle-rn/react-native.config.js
Normal file
@@ -0,0 +1 @@
|
||||
module.exports = { dependency: { platforms: { android: { sourceDir: "./android" } } } };
|
||||
@@ -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");
|
||||
39
packages/maplibre-particle-rn/src/index.ts
Normal file
39
packages/maplibre-particle-rn/src/index.ts
Normal 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,
|
||||
});
|
||||
}
|
||||
7
packages/maplibre-particle-rn/src/types.ts
Normal file
7
packages/maplibre-particle-rn/src/types.ts
Normal 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 }>;
|
||||
17
packages/maplibre-particle-rn/tsconfig.json
Normal file
17
packages/maplibre-particle-rn/tsconfig.json
Normal 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"]
|
||||
}
|
||||
Reference in New Issue
Block a user