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:
27
docs/30_设计/MapLibre粒子Android发布与ReactNative包设计_v1.md
Normal file
27
docs/30_设计/MapLibre粒子Android发布与ReactNative包设计_v1.md
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
# MapLibre 粒子 Android 发布与 React Native 包设计 v1
|
||||||
|
|
||||||
|
## 版本与坐标
|
||||||
|
|
||||||
|
真实基础为 MapLibre Native Android `13.4.1`,共同 core commit 为 `18545ca10d296704b2cba675b182ff2c8da24588`,本发行只准备 Android OpenGL。正式规则为 `<上游版本>-particle.<修订号>`;首发为 `13.4.1-particle.1`。正式 npm 包为 `@navsea/maplibre-particle-rn@13.4.1-particle.1`,正式 Maven 坐标为 `cc.navsea.maplibre:android-sdk-particle-opengl:13.4.1-particle.1`。
|
||||||
|
|
||||||
|
`step061` 是已经完成验证的开发阶段制品编号,不是正式发行版本。release manifest 是 `release/maplibre-particle/release-manifest.json`;registry URL 暂不决定,manifest 中 npm/Maven server 字段均为 null。
|
||||||
|
|
||||||
|
## Consumer 依赖关系
|
||||||
|
|
||||||
|
```text
|
||||||
|
Consumer
|
||||||
|
↓ 直接安装
|
||||||
|
@navsea/maplibre-particle-rn
|
||||||
|
↓ 唯一 MapLibre RN native module
|
||||||
|
@maplibre/maplibre-react-native 11.3.6
|
||||||
|
↓ Gradle dependency substitution
|
||||||
|
cc.navsea.maplibre:android-sdk-particle-opengl:13.4.1-particle.1
|
||||||
|
```
|
||||||
|
|
||||||
|
正式包 re-export 已有 MLRN `Map`、`Camera` 等公共 API,并包含 WeatherRuntime TurboModule。它不是 MLRN 的完整 fork;`@maplibre/maplibre-react-native` 是正式包的内部依赖,而 Consumer 不应再直接安装第二份 MLRN。package plugin 把 MLRN 唯一的 `org.maplibre.gl:android-sdk-opengl` 请求替换为正式 NavSea AAR,避免官方 AAR 与 Particle AAR 同时进入 APK。
|
||||||
|
|
||||||
|
WeatherColorLayer 与 RasterParticleLayer 是 Core-only style layer,不是 React 组件;包公开 `createParticleWeatherStyle()` 和稳定 IDs 以声明样式。field 仍只经既有 `metadataJson + local file:// payload → WeatherRuntime → RasterArraySource::submitHostField()` 进入 Native/Core;不创建第二套 ingress、Timeline 或 Runtime Window。
|
||||||
|
|
||||||
|
## 发布与升级边界
|
||||||
|
|
||||||
|
Consumer 不编译 MapLibre Native 源码,也不使用 workspace、relative source path、CMake path 或软链接。registry 未决定时,`.tgz` 与 Maven Local/临时 staging 只用于预发布验证,不是最终部署决定。未来升级到新的上游版本时,按 `13.5.0-particle.1` 等新发行线重新完成 Native、tarball Consumer 和 APK 验证;通过后才单独创建 release tag。
|
||||||
27
docs/50_Step记录/STEP-063_MapLibre粒子Android_v1发布工程化_v1.md
Normal file
27
docs/50_Step记录/STEP-063_MapLibre粒子Android_v1发布工程化_v1.md
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
# STEP-063 MapLibre 粒子 Android v1 发布工程化 v1
|
||||||
|
|
||||||
|
状态:`IMPLEMENTED / WAITING_MANUAL_RELEASE_VALIDATION`
|
||||||
|
|
||||||
|
## 目标与基线
|
||||||
|
|
||||||
|
- 实际 MapLibre Native Android 基础版本:`13.4.1`;core:`18545ca10d296704b2cba675b182ff2c8da24588`;renderer:OpenGL。
|
||||||
|
- 正式版本:`13.4.1-particle.1`;npm:`@navsea/maplibre-particle-rn@13.4.1-particle.1`;Maven:`cc.navsea.maplibre:android-sdk-particle-opengl:13.4.1-particle.1`。
|
||||||
|
- STEP-061 的 `13.4.1-step061` 是开发期已验收坐标,不作为正式 release/package/build config。
|
||||||
|
|
||||||
|
## 已完成
|
||||||
|
|
||||||
|
- [x] 建立机器可读 release manifest,registry 均明确为 `null`。
|
||||||
|
- [x] Android publication 配置改为正式 group/artifact/version,保留 OpenGL publication postfix。
|
||||||
|
- [x] 建立正式 RN facade package,含 JS/TS source、类型、Android bridge、autolinking、Expo plugin、README、CHANGELOG、LICENSE。
|
||||||
|
- [x] package plugin 将 MLRN 的单一 OpenGL SDK dependency substitution 到正式 NavSea AAR,避免双 Native AAR。
|
||||||
|
- [x] 建立只声明正式 npm 包的 Consumer Test Host;不引用源码 package、Native tree 或复制 bridge。
|
||||||
|
- [x] `npm pack`、tarball 内容审计、package JSON、plugin syntax、package TypeScript check、`git diff --check` 静态验证。
|
||||||
|
- [x] 设计、Step、完成报告和开发记录。
|
||||||
|
|
||||||
|
## 禁止项与未执行项
|
||||||
|
|
||||||
|
未改 Particle/Core/JNI/WeatherRuntime 业务功能,未接入 NavSea,未执行 Android Gradle、AAR build/publish、Maven Local publish、APK、ADB 或 Jest。未创建 release tag,未决定 npm/Maven server。
|
||||||
|
|
||||||
|
## 下一步
|
||||||
|
|
||||||
|
用户手工构建并在 Maven Local 或临时 staging 准备 `cc.navsea.maplibre:android-sdk-particle-opengl:13.4.1-particle.1`;以 npm tarball 安装 Consumer Host,手工编译/安装 APK 并验证 GSI、WeatherColor、Particle 与 Runtime READY。全部通过后才可进入 release freeze/tag 任务。
|
||||||
42
docs/报告/STEP-063_MapLibre粒子Android_v1发布工程化_Codex完成报告_v1.md
Normal file
42
docs/报告/STEP-063_MapLibre粒子Android_v1发布工程化_Codex完成报告_v1.md
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
# STEP-063 MapLibre 粒子 Android v1 发布工程化 — Codex完成报告 v1
|
||||||
|
|
||||||
|
结论:`IMPLEMENTED / WAITING_MANUAL_RELEASE_VALIDATION`。
|
||||||
|
|
||||||
|
## 真实基线与发布契约
|
||||||
|
|
||||||
|
实际确认 upstream Android 基础为 `13.4.1`,共同 core 为 `18545ca10d296704b2cba675b182ff2c8da24588`,当前发行 renderer 为 OpenGL。正式版本采用 `13.4.1-particle.1`;release manifest 位于 `release/maplibre-particle/release-manifest.json`。正式 npm 包路径为 `packages/maplibre-particle-rn/`,名称为 `@navsea/maplibre-particle-rn`;正式 Maven 坐标为 `cc.navsea.maplibre:android-sdk-particle-opengl:13.4.1-particle.1`。
|
||||||
|
|
||||||
|
原开发 AAR `org.maplibre.gl:android-sdk-opengl:13.4.1-step061` 只保留在历史 Step/验收说明中。正式 Android publication、RN package Android bridge 与 package plugin 均指向正式 NavSea coordinate;未继续把 step061 用作当前发行依赖。
|
||||||
|
|
||||||
|
## RN package 与 Consumer
|
||||||
|
|
||||||
|
正式 package 保留 Native WeatherRuntime bridge,re-export 当前 MLRN 公共 Map API。当前仓库不是完整 MLRN fork,故 package 将 `@maplibre/maplibre-react-native@11.3.6` 作为内部依赖;Consumer 只直接安装 NavSea package,不应额外直接安装官方 MLRN。package plugin 利用 Gradle dependency substitution,将 MLRN 的唯一 OpenGL SDK 请求替换为正式 NavSea AAR,避免两份 Native AAR。
|
||||||
|
|
||||||
|
Consumer Host 位于 `integration/react-native/MapLibreParticlePackageConsumerTestHost/`,只声明 `@navsea/maplibre-particle-rn@13.4.1-particle.1`,从其导入 Map、Camera、WeatherRuntime 和 style helper;没有 `file:`、workspace/source-path package dependency,也不复制 bridge 或引用 Native source tree。
|
||||||
|
|
||||||
|
## tarball 与静态验证
|
||||||
|
|
||||||
|
`npm pack` 成功生成 `navsea-maplibre-particle-rn-13.4.1-particle.1.tgz`(8,951 bytes,SHA-1 `2127d7b0e7ce71d0d9e429c92806cd93799c919e`)至 `/tmp/step063-package.*`。tarball 含 package JSON、TS source/types、Android bridge/Gradle、autolinking config、Expo plugin、README、CHANGELOG、LICENSE;不含 node_modules、.git、build cache、APK、Test Host、WeatherV2 assets、logs、无关 docs 或凭证。
|
||||||
|
|
||||||
|
package JSON 和两个 plugin Node syntax 检查 PASS;正式 package TypeScript check PASS(经 build-output-throttle);release manifest/package/Consumer version contract、无 `file:` consumer dependency、正式 build config 无 step061 与 `git diff --check` 均 PASS。Consumer 依赖的是尚未上传 registry 的正式版本,故本轮不执行其 `npm install`;这由后续 tarball 安装人工验证覆盖。没有运行 Jest。
|
||||||
|
|
||||||
|
## 未执行的人工验证与命令
|
||||||
|
|
||||||
|
Codex 未执行 Android Gradle、AAR build/publish、Maven Local publish、APK、ADB 或实体机测试。用户后续在具备 Android 环境时执行:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd platform/android
|
||||||
|
# 按当前正式 publication 配置手工构建并发布到 Maven Local 或临时 staging。
|
||||||
|
./gradlew :MapLibreAndroid:publishOpenglreleasePublicationToMavenLocal --console=plain
|
||||||
|
|
||||||
|
cd packages/maplibre-particle-rn
|
||||||
|
npm pack --pack-destination /tmp/maplibre-particle-release
|
||||||
|
|
||||||
|
cd integration/react-native/MapLibreParticlePackageConsumerTestHost
|
||||||
|
npm install /tmp/maplibre-particle-release/navsea-maplibre-particle-rn-13.4.1-particle.1.tgz --save-exact
|
||||||
|
npm install --ignore-scripts
|
||||||
|
npx expo prebuild --platform android --clean
|
||||||
|
npx expo run:android
|
||||||
|
```
|
||||||
|
|
||||||
|
随后验证 GSI、WeatherColor、Particle、canonical fixture submit 与 Runtime READY。通过前不得创建正式 release tag,也不得写 `RELEASED` 或 `CLOSED / PASS`。
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
import { useCallback, useState } from "react";
|
||||||
|
import { Button, SafeAreaView, StyleSheet, Text, View, findNodeHandle } from "react-native";
|
||||||
|
import { File, Paths } from "expo-file-system";
|
||||||
|
import { Camera, Map, WeatherRuntime, createParticleWeatherStyle } from "@navsea/maplibre-particle-rn";
|
||||||
|
|
||||||
|
const metadata = JSON.stringify({ schema: "maplibre-particle-field/v1", fieldId: "consumer-smoke", fieldType: "WIND", displayDomain: "GLOBAL", generation: 1, validTime: 1786060800000, dataVersion: 1, bounds: { west: -180, south: -85, east: 180, north: 85 }, grid: { width: 2, height: 2 }, vector: { layout: "interleaved-uv", encoding: "float32-le", noData: -9999 }, payload: { byteLength: 32 } });
|
||||||
|
|
||||||
|
function writeFixture() {
|
||||||
|
const bytes = new Uint8Array(32); const view = new DataView(bytes.buffer);
|
||||||
|
for (let offset = 0; offset < bytes.byteLength; offset += 8) { view.setFloat32(offset, 5, true); view.setFloat32(offset + 4, 1, true); }
|
||||||
|
const file = new File(Paths.cache, "navsea-particle-consumer-smoke.f32"); file.write(bytes); return file.uri;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function App() {
|
||||||
|
const [tag, setTag] = useState<number | null>(null); const [state, setState] = useState("STYLE_LOADING");
|
||||||
|
const onStyle = useCallback(async (event: unknown) => {
|
||||||
|
const value = findNodeHandle((event as { target?: unknown }).target as never); if (value == null) return;
|
||||||
|
try { await WeatherRuntime.attach(value); setTag(value); setState("WAITING_FOR_FIELDS"); } catch (error) { setState(String(error)); }
|
||||||
|
}, []);
|
||||||
|
const submit = useCallback(async () => {
|
||||||
|
if (tag == null) return;
|
||||||
|
try { await WeatherRuntime.clearWindFields(tag); const result = await WeatherRuntime.submitWindField(tag, metadata, writeFixture()); const runtime = await WeatherRuntime.getState(tag); setState(`${result} / ${runtime.state}`); } catch (error) { setState(String(error)); }
|
||||||
|
}, [tag]);
|
||||||
|
return <SafeAreaView style={styles.root}><View style={styles.map}><Map mapStyle={createParticleWeatherStyle()} onDidFinishLoadingStyle={onStyle}><Camera initialViewState={{ center: [135, 32], zoom: 3.9 }} /></Map></View><View style={styles.controls}><Text>Package smoke: {state}</Text><Button title="Submit canonical fixture" onPress={() => void submit()} /></View></SafeAreaView>;
|
||||||
|
}
|
||||||
|
const styles = StyleSheet.create({ root: { flex: 1 }, map: { flex: 1 }, controls: { padding: 12, backgroundColor: "white" } });
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
# MapLibreParticlePackageConsumerTestHost
|
||||||
|
|
||||||
|
这是发布包 Consumer smoke host,不引用 `packages/`、Test Host bridge 源码、MapLibre Native source tree 或 workspace dependency。其 `package.json` 只直接依赖 `@navsea/maplibre-particle-rn@13.4.1-particle.1`;Map、Camera、WeatherRuntime 和 style helper 全部从该发布包导入。
|
||||||
|
|
||||||
|
最终 registry 未决定时,先在包目录生成 tarball,再在本目录手工执行:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install /absolute/path/navsea-maplibre-particle-rn-13.4.1-particle.1.tgz --save-exact
|
||||||
|
npm install --ignore-scripts
|
||||||
|
npx expo prebuild --platform android --clean
|
||||||
|
npx expo run:android
|
||||||
|
```
|
||||||
|
|
||||||
|
该 `.tgz` 是发布物而非源码路径。用户还需将 `cc.navsea.maplibre:android-sdk-particle-opengl:13.4.1-particle.1` 手工发布至 Maven Local 或临时 staging;package plugin 会将 MLRN 的唯一 OpenGL SDK 请求替换为该正式 artifact。Codex 不执行上述 Android 命令。
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"expo": {
|
||||||
|
"name": "MapLibre Particle Package Consumer",
|
||||||
|
"slug": "maplibre-particle-package-consumer",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"orientation": "landscape",
|
||||||
|
"newArchEnabled": true,
|
||||||
|
"plugins": ["@navsea/maplibre-particle-rn/app.plugin"],
|
||||||
|
"android": { "package": "cc.navsea.maplibre.particle.consumer", "minSdkVersion": 24 }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
"name": "maplibre-particle-package-consumer-test-host",
|
||||||
|
"private": true,
|
||||||
|
"version": "0.1.0",
|
||||||
|
"packageManager": "npm@10.9.4",
|
||||||
|
"scripts": {
|
||||||
|
"typecheck": "tsc --noEmit",
|
||||||
|
"start": "expo start --port 8092",
|
||||||
|
"android": "expo run:android"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@navsea/maplibre-particle-rn": "13.4.1-particle.1",
|
||||||
|
"expo": "56.0.4",
|
||||||
|
"expo-file-system": "56.0.11",
|
||||||
|
"react": "19.2.3",
|
||||||
|
"react-native": "0.85.2"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/react": "19.2.5",
|
||||||
|
"typescript": "5.9.3"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"extends": "expo/tsconfig.base",
|
||||||
|
"compilerOptions": { "strict": true, "noEmit": true },
|
||||||
|
"include": ["App.tsx"]
|
||||||
|
}
|
||||||
@@ -14,7 +14,7 @@ android {
|
|||||||
dependencies {
|
dependencies {
|
||||||
implementation "com.facebook.react:react-android"
|
implementation "com.facebook.react:react-android"
|
||||||
implementation "org.jetbrains.kotlin:kotlin-stdlib:2.1.20"
|
implementation "org.jetbrains.kotlin:kotlin-stdlib:2.1.20"
|
||||||
implementation "org.maplibre.gl:android-sdk-opengl:13.4.1-step061"
|
implementation "cc.navsea.maplibre:android-sdk-particle-opengl:13.4.1-particle.1"
|
||||||
compileOnly project(":maplibre_maplibre-react-native")
|
compileOnly project(":maplibre_maplibre-react-native")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,16 +1,12 @@
|
|||||||
const { withProjectBuildGradle } = require("@expo/config-plugins");
|
const { withProjectBuildGradle } = require("@expo/config-plugins");
|
||||||
|
|
||||||
const marker = "// MapLibreParticleRNTestHost custom OpenGL SDK";
|
const marker = "// MapLibreParticleRNTestHost NavSea Particle release SDK";
|
||||||
|
|
||||||
/**
|
/** Resolves the one MLRN SDK request to the formal NavSea artifact without a second Native AAR. */
|
||||||
* Pins the one custom MapLibre Native SDK coordinate to Maven Local without deleting
|
|
||||||
* Expo/RN local repositories. Maven Central is filtered only for the custom OpenGL SDK,
|
|
||||||
* so a missing local custom AAR fails instead of silently using the official Central AAR.
|
|
||||||
*/
|
|
||||||
module.exports = function withParticleNativeAndroid(config) {
|
module.exports = function withParticleNativeAndroid(config) {
|
||||||
return withProjectBuildGradle(config, (project) => {
|
return withProjectBuildGradle(config, (project) => {
|
||||||
if (project.modResults.contents.includes(marker)) return 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.repositories.withType(org.gradle.api.artifacts.repositories.MavenArtifactRepository).configureEach { repo ->\n def repoUrl = repo.url.toString()\n if (repoUrl.contains(\"repo.maven.apache.org/maven2\") || repoUrl.contains(\"repo1.maven.org/maven2\")) {\n repo.content { excludeModule(\"org.maplibre.gl\", \"android-sdk-opengl\") }\n }\n }\n }\n}\nrootProject.ext.set(\"org.maplibre.reactnative.nativeVersion\", \"13.4.1-step061\")\nrootProject.ext.set(\"org.maplibre.reactnative.nativeVariant\", \"opengl\")\n`;
|
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;
|
return project;
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
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"]
|
||||||
|
}
|
||||||
@@ -1 +1 @@
|
|||||||
13.4.1-step061
|
13.4.1-particle.1
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
extra["mapLibreArtifactGroupId"] = "org.maplibre.gl"
|
extra["mapLibreArtifactGroupId"] = "cc.navsea.maplibre"
|
||||||
extra["mapLibreArtifactId"] = "android-sdk"
|
extra["mapLibreArtifactId"] = "android-sdk-particle"
|
||||||
extra["mapLibreArtifactTitle"] = "MapLibre Android"
|
extra["mapLibreArtifactTitle"] = "NavSea MapLibre Particle Android"
|
||||||
extra["mapLibreArtifactDescription"] = "MapLibre Android"
|
extra["mapLibreArtifactDescription"] = "NavSea MapLibre Particle Android"
|
||||||
extra["mapLibreDeveloperName"] = "MapLibre"
|
extra["mapLibreDeveloperName"] = "NavSea"
|
||||||
extra["mapLibreDeveloperId"] = "maplibre"
|
extra["mapLibreDeveloperId"] = "navsea"
|
||||||
extra["mapLibreArtifactUrl"] = "https://github.com/maplibre/maplibre-native"
|
extra["mapLibreArtifactUrl"] = "https://github.com/maplibre/maplibre-native"
|
||||||
extra["mapLibreArtifactScmUrl"] = "scm:git@github.com:maplibre/maplibre-native.git"
|
extra["mapLibreArtifactScmUrl"] = "scm:git@github.com:maplibre/maplibre-native.git"
|
||||||
extra["mapLibreArtifactLicenseName"] = "BSD"
|
extra["mapLibreArtifactLicenseName"] = "BSD"
|
||||||
|
|||||||
27
release/maplibre-particle/release-manifest.json
Normal file
27
release/maplibre-particle/release-manifest.json
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
{
|
||||||
|
"release": "13.4.1-particle.1",
|
||||||
|
"releaseState": "prepared-waiting-manual-release-validation",
|
||||||
|
"upstreamMapLibreNative": "13.4.1",
|
||||||
|
"upstreamCoreCommit": "18545ca10d296704b2cba675b182ff2c8da24588",
|
||||||
|
"extensionLine": "particle",
|
||||||
|
"extensionRevision": 1,
|
||||||
|
"platform": { "android": { "renderer": "opengl" } },
|
||||||
|
"reactNativePackage": {
|
||||||
|
"name": "@navsea/maplibre-particle-rn",
|
||||||
|
"version": "13.4.1-particle.1",
|
||||||
|
"mapLibreReactNative": "11.3.6"
|
||||||
|
},
|
||||||
|
"nativeArtifact": {
|
||||||
|
"group": "cc.navsea.maplibre",
|
||||||
|
"artifact": "android-sdk-particle-opengl",
|
||||||
|
"version": "13.4.1-particle.1"
|
||||||
|
},
|
||||||
|
"canonicalField": {
|
||||||
|
"schema": "maplibre-particle-field/v1",
|
||||||
|
"transport": "metadataJson + local file:// payload",
|
||||||
|
"encoding": "float32-le",
|
||||||
|
"layout": "interleaved-uv"
|
||||||
|
},
|
||||||
|
"publication": { "npmRegistry": null, "mavenRepository": null },
|
||||||
|
"releaseTag": null
|
||||||
|
}
|
||||||
13
粒子开发记录.md
13
粒子开发记录.md
@@ -587,3 +587,16 @@ WeatherColorLayer 的 `ParticleGpuValidationUBO` designated initializer 补齐 `
|
|||||||
- Android Validation 的 ECMWF HRES 20260807 canonical T0–T3 assets 已原样放入 Host assets,四帧各 3,155,976 bytes 且逐文件 SHA-256 一致。
|
- Android Validation 的 ECMWF HRES 20260807 canonical T0–T3 assets 已原样放入 Host assets,四帧各 3,155,976 bytes 且逐文件 SHA-256 一致。
|
||||||
- `expo-asset` 以静态 `.f32` require + `downloadAsync()` 输出 cache `file://` URI;只把 metadata string 与 local file reference 交给既有 STEP-061 bridge,绝不 materialize payload 为 array/JSON/base64/hex。
|
- `expo-asset` 以静态 `.f32` require + `downloadAsync()` 输出 cache `file://` URI;只把 metadata string 与 local file reference 交给既有 STEP-061 bridge,绝不 materialize payload 为 array/JSON/base64/hex。
|
||||||
- `LOAD REAL WEATHER T0-T3` clear 后严格断言 Pending、InitialWindowReady、NextSnapshot、NextSnapshot 和 Native READY;控制页提供 timestamp seek、playback/speed/FPS、独立 visibility/stops/colors 与视觉参数。静态 typecheck/Metro/asset hash 通过;RDC 追加 Android Expo export 也已确认四个 `.f32` 正常打包,功能提交 `a91e3601f37f3adfe5d8b7b6a6c3f7f04fcadb8f` 已 push;Android APK/实机待 CARBON7i 一次综合验收。
|
- `LOAD REAL WEATHER T0-T3` clear 后严格断言 Pending、InitialWindowReady、NextSnapshot、NextSnapshot 和 Native READY;控制页提供 timestamp seek、playback/speed/FPS、独立 visibility/stops/colors 与视觉参数。静态 typecheck/Metro/asset hash 通过;RDC 追加 Android Expo export 也已确认四个 `.f32` 正常打包,功能提交 `a91e3601f37f3adfe5d8b7b6a6c3f7f04fcadb8f` 已 push;Android APK/实机待 CARBON7i 一次综合验收。
|
||||||
|
|
||||||
|
# 2026-09-09 — STEP-062 Android APK-only 最终关闭
|
||||||
|
|
||||||
|
- 用户已在 Win11 Test Host 手工完成依赖安装、typecheck、APK 编译/安装和实体机综合验收;Metro 确认 `.f32=true`,Test Host 始终使用冻结 `org.maplibre.gl:android-sdk-opengl:13.4.1-step061`。
|
||||||
|
- GSI 日本标准底图、真实 WeatherV2 T0–T3、binary file ingress、既有 Native/Core 正式入口、WeatherColor、GPU 粒子、地图拖动/缩放/旋转联动、双层独立显示、视觉参数、四时次切换及其他可操作交互均经用户确认 OK;LOAD 后整体评价为“非常完美”。
|
||||||
|
- Timeline/Playback 可操作且未见明确异常;因四帧间隔 6 小时,短时自动播放视觉变化不明显。用户接受当前表现,不追加专用 Timeline 可观测性 UI,不构成关闭阻塞。
|
||||||
|
- 最终用户验收未额外执行完整 PID logcat 硬门槛扫描,未声称 logcat 0 error。未改功能代码/Native/Core/JNI/WeatherRuntime,未 build/publish AAR,未接入 NavSea,未执行 Gradle/APK/ADB/Jest;STEP-062 正式 `CLOSED / PASS`。
|
||||||
|
|
||||||
|
# 2026-09-09 — STEP-063 Android v1 发布工程化(IMPLEMENTED / WAITING_MANUAL_RELEASE_VALIDATION)
|
||||||
|
|
||||||
|
- 实际 upstream Android 基线确认是 MapLibre Native `13.4.1` / core `18545ca` / OpenGL;建立 `13.4.1-particle.1` release manifest、`@navsea/maplibre-particle-rn` 与 `cc.navsea.maplibre:android-sdk-particle-opengl` 正式契约。
|
||||||
|
- Android publication 配置、正式 bridge/package 和 Gradle substitution 统一到 NavSea AAR,MLRN 保持唯一 native module;Consumer Host 只依赖正式 npm package,不引用源码路径、Native tree 或 bridge source。
|
||||||
|
- `npm pack` 与 package TypeScript/static audit 完成;未执行 Android Gradle/AAR/APK/ADB/Jest,未决定 registry,未创建 release tag。下一步为用户手工 AAR publication 与 tarball Consumer APK/实体机验证。
|
||||||
|
|||||||
Reference in New Issue
Block a user