merge(upgrade): port particle extensions onto official native 13.6.1 (unverified)

This commit is contained in:
tei
2026-10-09 09:37:07 +09:00
767 changed files with 85435 additions and 153 deletions

View File

@@ -195,6 +195,7 @@ void MapRenderer::requestSnapshot(SnapshotCallback callback) {
// Called on OpenGL thread //
void MapRenderer::resetRenderer() {
Log::Info(Event::Android, "MapRenderer::resetRenderer entered");
renderer.reset();
if (!asyncRendererCleanup) {
@@ -203,6 +204,7 @@ void MapRenderer::resetRenderer() {
window.reset();
swapBehaviorFlush = false;
Log::Info(Event::Android, "MapRenderer::resetRenderer completed");
}
void MapRenderer::scheduleSnapshot(std::unique_ptr<SnapshotCallback> callback) {
@@ -322,9 +324,11 @@ void MapRenderer::onSurfaceChanged([[maybe_unused]] JNIEnv& env, jint width, jin
}
void MapRenderer::onRendererReset(JNIEnv&) {
Log::Info(Event::Android, "MapRenderer::nativeReset entered");
// Make sure to destroy the renderer on the GL Thread
auto self = ActorRef<MapRenderer>(*this, mailboxData.getMailbox());
self.ask(&MapRenderer::resetRenderer).wait();
Log::Info(Event::Android, "MapRenderer::nativeReset completed");
}
// needs to be called on GL thread

View File

@@ -3,6 +3,10 @@
#include <cstdlib>
#include <ctime>
#include <cassert>
#include <algorithm>
#include <array>
#include <cmath>
#include <cstdint>
#include <memory>
#include <list>
#include <tuple>
@@ -26,9 +30,15 @@
#include <mln/util/platform.hpp>
#include <mln/util/projection.hpp>
#include <mln/style/style.hpp>
#include <mln/style/layers/raster_particle_layer.hpp>
#include <mln/style/layers/weather_color_layer.hpp>
#include <mln/style/sources/raster_array_source.hpp>
#include <mln/style/image.hpp>
#include <mln/style/filter.hpp>
#include <mln/renderer/query.hpp>
#include <mln/renderer/render_segment_json.hpp>
#include <mln/particle/field/field_profile_contract_json.hpp>
#include <mln/particle/ingress/encoded_field_transport.hpp>
// Java -> C++ conversion
#include "style/android_conversion.hpp"
@@ -61,6 +71,8 @@
namespace mln {
namespace android {
namespace {}
NativeMapView::NativeMapView(jni::JNIEnv& _env,
const jni::Object<NativeMapView>& _obj,
const jni::Object<FileSource>& jFileSource,
@@ -704,6 +716,76 @@ jni::jboolean NativeMapView::getDebug(JNIEnv&) {
return map->getDebug() != DebugOptions::NoDebug;
}
void NativeMapView::setRetainedUnderlayEnabled(JNIEnv&, jni::jboolean enabled) {
map->setRetainedUnderlayEnabled(enabled);
}
jni::jboolean NativeMapView::setContinuousLayerRange(JNIEnv& env,
const jni::String& startLayerId,
const jni::String& endLayerId) {
return map->setContinuousLayerRange(jni::Make<std::string>(env, startLayerId),
jni::Make<std::string>(env, endLayerId));
}
jni::jboolean NativeMapView::setRenderSegmentPlan(JNIEnv& env, const jni::String& planJson) {
auto plan = renderSegmentPlanFromJSONOrInvalid(jni::Make<std::string>(env, planJson));
return map->setRenderSegmentPlan(std::move(plan));
}
jni::Local<jni::String> NativeMapView::getRenderSegmentDiagnostics(JNIEnv& env) {
return jni::Make<jni::String>(env, renderSegmentDiagnosticsToJSON(map->getRenderSegmentDiagnostics()));
}
jni::Local<jni::Array<jni::String>> NativeMapView::getRetainedUnderlayDiagnostics(JNIEnv& env) {
const auto snapshot = map->getRetainedUnderlayDiagnostics();
const std::array<std::string, 39> values = {
snapshot.enabled ? "true" : "false",
std::to_string(snapshot.build),
std::to_string(snapshot.reuse),
std::to_string(snapshot.fallback),
std::to_string(snapshot.invalidation),
snapshot.lastReason,
snapshot.rangeConfigured ? "true" : "false",
snapshot.rangeValid ? "true" : "false",
snapshot.startLayerId,
snapshot.endLayerId,
std::to_string(snapshot.startIndex),
std::to_string(snapshot.endIndex),
std::to_string(snapshot.continuousBegin),
std::to_string(snapshot.continuousEndExclusive),
snapshot.bottomPresent ? "true" : "false",
snapshot.bottomCacheState,
std::to_string(snapshot.bottomBuild),
std::to_string(snapshot.bottomReuse),
std::to_string(snapshot.bottomInvalidation),
snapshot.bottomLastInvalidationReason,
std::to_string(snapshot.bottomTextureCreate),
std::to_string(snapshot.bottomCompositeCreate),
std::to_string(snapshot.bottomResourceRelease),
snapshot.topPresent ? "true" : "false",
snapshot.topCacheState,
std::to_string(snapshot.topBuild),
std::to_string(snapshot.topReuse),
std::to_string(snapshot.topInvalidation),
snapshot.topLastInvalidationReason,
std::to_string(snapshot.topTextureCreate),
std::to_string(snapshot.topCompositeCreate),
std::to_string(snapshot.topResourceRelease),
snapshot.rangeFallback ? "true" : "false",
snapshot.fastPathEligible ? "true" : "false",
snapshot.fastPathActive ? "true" : "false",
snapshot.fastPathFallbackReason,
std::to_string(snapshot.fastPathHit),
std::to_string(snapshot.fastPathMiss),
std::to_string(snapshot.fullCpuFrame),
};
auto result = jni::Array<jni::String>::New(env, values.size());
for (std::size_t index = 0; index < values.size(); ++index) {
result.Set(env, index, jni::Make<jni::String>(env, values[index]));
}
return result;
}
jni::Local<jni::Array<jni::String>> NativeMapView::getActionJournalLogFiles(JNIEnv& env) {
const auto& actionJournal = map->getActionJournal();
if (!actionJournal) {
@@ -1104,6 +1186,340 @@ jni::Local<jni::Object<Layer>> NativeMapView::getLayer(JNIEnv& env, const jni::S
return LayerManagerAndroid::get()->createJavaLayerPeer(env, *coreLayer);
}
jni::jboolean NativeMapView::hasStyleLayer(JNIEnv& env, const jni::String& layerId) {
return map->getStyle().getLayer(jni::Make<std::string>(env, layerId)) != nullptr;
}
jni::jboolean NativeMapView::setStyleLayerFloatProperty(JNIEnv& env,
const jni::String& layerId,
const jni::String& propertyName,
const jni::jfloat value) {
auto* layer = map->getStyle().getLayer(jni::Make<std::string>(env, layerId));
if (!layer) return false;
const auto property = jni::Make<std::string>(env, propertyName);
if (property == "weather-color-field-value-source") {
auto* weatherColorLayer = dynamic_cast<mln::style::WeatherColorLayer*>(layer);
if (!weatherColorLayer || (value != 0.0f && value != 1.0f)) {
mln::Log::Error(mln::Event::JNI, "ParticleField: event=FIELD_VALUE_SOURCE_SET result=REJECTED");
return false;
}
weatherColorLayer->setFieldValueSource(value == 0.0f ? mln::style::FieldValueSource::Scalar
: mln::style::FieldValueSource::VectorMagnitude);
return true;
}
if (property != "raster-particle-time-mix" && property != "raster-particle-particle-count" &&
property != "raster-particle-line-width" && property != "raster-particle-particle-length" &&
property != "raster-particle-particle-opacity" && property != "raster-particle-trail-opacity" &&
property != "raster-particle-trail-fade" && property != "raster-particle-speed-color-enabled" &&
property != "raster-particle-advection-scale") {
mln::Log::Error(mln::Event::JNI, "ParticleGPU: event=STYLE_PROPERTY_SET result=UNSUPPORTED_PROPERTY");
return false;
}
auto* particleLayer = dynamic_cast<mln::style::RasterParticleLayer*>(layer);
if (!particleLayer) {
mln::Log::Error(mln::Event::JNI, "ParticleGPU: event=STYLE_PROPERTY_SET result=UNSUPPORTED_LAYER");
return false;
}
if (!std::isfinite(value)) {
mln::Log::Error(mln::Event::JNI, "ParticleGPU: event=STYLE_PROPERTY_SET result=INVALID_FLOAT");
return false;
}
if (property == "raster-particle-time-mix") {
particleLayer->setTimeMix(value);
} else if (property == "raster-particle-particle-count") {
if (value < 0.0f || std::floor(value) != value) {
mln::Log::Error(mln::Event::JNI, "ParticleGPU: event=STYLE_PROPERTY_SET result=INVALID_PARTICLE_COUNT");
return false;
}
const auto requestedCount = static_cast<uint32_t>(value);
particleLayer->setParticleCount(requestedCount);
if (particleLayer->getParticleCount() != requestedCount) {
mln::Log::Error(mln::Event::JNI, "ParticleGPU: event=STYLE_PROPERTY_SET result=INVALID_PARTICLE_COUNT");
return false;
}
} else if (property == "raster-particle-line-width") {
particleLayer->setLineWidth(value);
} else if (property == "raster-particle-particle-length") {
particleLayer->setParticleLength(value);
} else if (property == "raster-particle-particle-opacity") {
particleLayer->setParticleOpacity(value);
} else if (property == "raster-particle-trail-opacity") {
particleLayer->setTrailOpacity(value);
} else if (property == "raster-particle-trail-fade") {
particleLayer->setTrailFade(value);
} else if (property == "raster-particle-advection-scale") {
if (value <= 0.0f) return false;
particleLayer->setAdvectionScale(value);
} else {
if (value != 0.0f && value != 1.0f) {
mln::Log::Error(mln::Event::JNI, "ParticleGPU: event=STYLE_PROPERTY_SET result=INVALID_COLOR_MODE");
return false;
}
particleLayer->setSpeedColorEnabled(value > 0.5f);
}
mln::Log::Info(mln::Event::JNI,
"ParticleGPU: event=STYLE_PROPERTY_SET layerId=" + jni::Make<std::string>(env, layerId) +
" property=" + property + " value=" + std::to_string(value) + " result=SUCCESS");
return true;
}
jni::jboolean NativeMapView::setWeatherColorOpacity(JNIEnv& env,
const jni::String& layerId,
const jni::jfloat opacity) {
if (!std::isfinite(opacity)) return false;
auto* layer = dynamic_cast<mln::style::WeatherColorLayer*>(
map->getStyle().getLayer(jni::Make<std::string>(env, layerId)));
if (!layer) return false;
layer->setOpacity(opacity);
return true;
}
jni::jboolean NativeMapView::setStyleLayerVisibility(JNIEnv& env, const jni::String& layerId, const jni::jboolean visible) {
auto* layer = map->getStyle().getLayer(jni::Make<std::string>(env, layerId));
if (!layer) return false;
layer->setVisibility(visible ? mln::style::VisibilityType::Visible : mln::style::VisibilityType::None);
return true;
}
jni::jboolean NativeMapView::setRasterParticleColors(JNIEnv& env,
const jni::String& layerId,
const jni::Array<jni::jfloat>& particle,
const jni::Array<jni::jfloat>& trail) {
auto* layer = dynamic_cast<mln::style::RasterParticleLayer*>(map->getStyle().getLayer(jni::Make<std::string>(env, layerId)));
if (!layer || particle.Length(env) != 4 || trail.Length(env) != 4) return false;
std::array<float, 4> particleColor, trailColor;
particle.GetRegion(env, 0, particleColor);
trail.GetRegion(env, 0, trailColor);
if (!mln::style::validWeatherRGBA(particleColor) || !mln::style::validWeatherRGBA(trailColor)) return false;
layer->setParticleColor(particleColor);
layer->setTrailColor(trailColor);
return true;
}
jni::jboolean NativeMapView::setRasterParticleColor(JNIEnv& env,
const jni::String& layerId,
const jni::Array<jni::jfloat>& particle) {
auto* layer = dynamic_cast<mln::style::RasterParticleLayer*>(map->getStyle().getLayer(jni::Make<std::string>(env, layerId)));
if (!layer || particle.Length(env) != 4) return false;
std::array<float, 4> color;
particle.GetRegion(env, 0, color);
if (!mln::style::validWeatherRGBA(color)) return false;
layer->setParticleColor(color);
return true;
}
jni::jboolean NativeMapView::setRasterParticleTrailColor(JNIEnv& env,
const jni::String& layerId,
const jni::Array<jni::jfloat>& trail) {
auto* layer = dynamic_cast<mln::style::RasterParticleLayer*>(map->getStyle().getLayer(jni::Make<std::string>(env, layerId)));
if (!layer || trail.Length(env) != 4) return false;
std::array<float, 4> color;
trail.GetRegion(env, 0, color);
if (!mln::style::validWeatherRGBA(color)) return false;
layer->setTrailColor(color);
return true;
}
jni::jboolean NativeMapView::setRasterArraySourceTargetFps(JNIEnv& env,
const jni::String& sourceId,
const jni::jint targetFps) {
auto* source = dynamic_cast<mln::style::RasterArraySource*>(map->getStyle().getSource(jni::Make<std::string>(env, sourceId)));
return source && source->setRuntimeTargetFps(static_cast<uint8_t>(targetFps));
}
jni::jint NativeMapView::getRasterArraySourceTargetFps(JNIEnv& env, const jni::String& sourceId) {
auto* source = dynamic_cast<mln::style::RasterArraySource*>(map->getStyle().getSource(jni::Make<std::string>(env, sourceId)));
return source ? source->runtimeTargetFps() : 0;
}
jni::jboolean NativeMapView::setWeatherColorStops(JNIEnv& env,
const jni::String& layerId,
const jni::Array<jni::jfloat>& speeds,
const jni::Array<jni::jfloat>& rgba) {
auto* layer = dynamic_cast<mln::style::WeatherColorLayer*>(map->getStyle().getLayer(jni::Make<std::string>(env, layerId)));
const auto count = speeds.Length(env);
if (!layer || count < 2 || count > static_cast<jni::jsize>(mln::style::MaxWeatherColorStops) || rgba.Length(env) != count * 4) return false;
std::vector<float> speedValues(count), rgbaValues(static_cast<size_t>(count) * 4);
speeds.GetRegion(env, 0, speedValues);
rgba.GetRegion(env, 0, rgbaValues);
std::vector<mln::style::WeatherColorStop> stops;
stops.reserve(count);
for (jni::jsize i = 0; i < count; ++i) {
std::array<float, 4> color;
std::copy_n(rgbaValues.data() + i * 4, 4, color.data());
stops.push_back({speedValues[i], color});
}
return layer->setColorStops(std::move(stops));
}
jni::jint NativeMapView::getWeatherColorStopCapacity(JNIEnv&) {
return static_cast<jni::jint>(mln::style::MaxWeatherColorStops);
}
jni::jboolean NativeMapView::advanceRasterArraySourceFieldWindow(JNIEnv& env, const jni::String& sourceId) {
const auto id = jni::Make<std::string>(env, sourceId);
auto* source = map->getStyle().getSource(id);
auto* rasterArraySource = dynamic_cast<mln::style::RasterArraySource*>(source);
if (!rasterArraySource) {
mln::Log::Error(mln::Event::JNI,
"ParticleField: event=FIELD_WINDOW_UPDATE_REJECTED reason=SOURCE_NOT_FOUND sourceId=" + id);
return false;
}
const auto result = rasterArraySource->advanceRuntimeFieldWindow();
const bool success = result == mln::style::RasterArrayRuntimeFieldUpdateResult::Applied ||
result == mln::style::RasterArrayRuntimeFieldUpdateResult::Noop;
mln::Log::Info(mln::Event::JNI,
"ParticleField: event=FIELD_WINDOW_UPDATE_BRIDGE sourceId=" + id +
" result=" + (success ? "SUCCESS" : "REJECTED"));
return success;
}
jni::jboolean NativeMapView::repeatRasterArraySourceFieldWindow(JNIEnv& env, const jni::String& sourceId) {
const auto id = jni::Make<std::string>(env, sourceId);
auto* source = map->getStyle().getSource(id);
auto* rasterArraySource = dynamic_cast<mln::style::RasterArraySource*>(source);
if (!rasterArraySource) {
mln::Log::Error(mln::Event::JNI,
"ParticleField: event=FIELD_WINDOW_UPDATE_REJECTED reason=SOURCE_NOT_FOUND sourceId=" + id);
return false;
}
const auto result = rasterArraySource->repeatRuntimeFieldWindow();
const bool success = result == mln::style::RasterArrayRuntimeFieldUpdateResult::Noop;
mln::Log::Info(mln::Event::JNI,
"ParticleField: event=FIELD_WINDOW_UPDATE_DUPLICATE_BRIDGE sourceId=" + id +
" result=" + (success ? "NOOP" : "REJECTED"));
return success;
}
jni::jint NativeMapView::submitRasterArraySourceHostField(JNIEnv& env,
const jni::String& sourceId,
const jni::String& metadata,
const jni::Array<jbyte>& payload) {
const auto id = jni::Make<std::string>(env, sourceId);
auto* source = dynamic_cast<mln::style::RasterArraySource*>(map->getStyle().getSource(id));
if (!source) {
mln::Log::Error(mln::Event::JNI,
"ParticleField: event=HOST_FIELD_SUBMIT_REJECTED reason=SOURCE_NOT_FOUND sourceId=" + id);
return static_cast<jni::jint>(mln::style::RasterArrayHostFieldSubmitResult::RejectedSourceMode);
}
NullCheck(env, &payload);
std::vector<char> bytes(payload.Length(env));
if (!bytes.empty()) {
jni::GetArrayRegion(env, *payload, 0, bytes.size(), reinterpret_cast<jbyte*>(bytes.data()));
}
const auto result = source->submitHostField(jni::Make<std::string>(env, metadata), bytes);
mln::Log::Info(mln::Event::JNI,
"ParticleField: event=HOST_FIELD_SUBMIT_BRIDGE sourceId=" + id +
" result=" + mln::style::rasterArrayHostFieldSubmitResultName(result));
return static_cast<jni::jint>(result);
}
jni::jint NativeMapView::submitRasterArraySourceHostFieldV2(JNIEnv& env,
const jni::String& sourceId,
const jni::String& metadata,
const jni::Array<jbyte>& packedPlanes,
const jni::jboolean scalarPresent,
const jni::jint scalarLength,
const jni::jboolean vectorPresent,
const jni::jint vectorLength,
const jni::jint validityLength) {
const auto id = jni::Make<std::string>(env, sourceId);
auto* source = dynamic_cast<mln::style::RasterArraySource*>(map->getStyle().getSource(id));
if (!source) return static_cast<jni::jint>(mln::style::RasterArrayHostFieldSubmitResult::RejectedSourceMode);
NullCheck(env, &packedPlanes);
if (scalarLength < 0 || vectorLength < 0 || validityLength <= 0 || (!scalarPresent && scalarLength != 0) ||
(!vectorPresent && vectorLength != 0) || (scalarPresent && scalarLength == 0) ||
(vectorPresent && vectorLength == 0) || (!scalarPresent && !vectorPresent)) {
return static_cast<jni::jint>(mln::style::RasterArrayHostFieldSubmitResult::RejectedAdapter);
}
const uint64_t total = static_cast<uint64_t>(scalarLength) + static_cast<uint64_t>(vectorLength) +
static_cast<uint64_t>(validityLength);
if (total != packedPlanes.Length(env)) {
return static_cast<jni::jint>(mln::style::RasterArrayHostFieldSubmitResult::RejectedAdapter);
}
std::vector<char> packed(total);
if (!packed.empty()) jni::GetArrayRegion(env, *packedPlanes, 0, packed.size(), reinterpret_cast<jbyte*>(packed.data()));
size_t offset = 0;
const auto take = [&packed, &offset](const size_t length) {
std::vector<char> result(packed.begin() + static_cast<std::ptrdiff_t>(offset),
packed.begin() + static_cast<std::ptrdiff_t>(offset + length));
offset += length;
return result;
};
mln::particle::ParticleFieldPayloadBundle payloads{
.scalar = scalarPresent ? std::optional<std::vector<char>>(take(static_cast<size_t>(scalarLength))) : std::nullopt,
.vector = vectorPresent ? std::optional<std::vector<char>>(take(static_cast<size_t>(vectorLength))) : std::nullopt,
.validity = take(static_cast<size_t>(validityLength)),
};
const auto result = source->submitHostField(jni::Make<std::string>(env, metadata), payloads);
mln::Log::Info(mln::Event::JNI,
"ParticleField: event=HOST_FIELD_V2_SUBMIT_BRIDGE sourceId=" + id + " scalar=" +
(scalarPresent ? "present" : "absent") + " vector=" + (vectorPresent ? "present" : "absent") +
" result=" + mln::style::rasterArrayHostFieldSubmitResultName(result));
return static_cast<jni::jint>(result);
}
jni::jboolean NativeMapView::clearRasterArraySourceHostFields(JNIEnv& env, const jni::String& sourceId) {
const auto id = jni::Make<std::string>(env, sourceId);
auto* source = dynamic_cast<mln::style::RasterArraySource*>(map->getStyle().getSource(id));
if (!source) return false;
const bool cleared = source->clearHostFields();
mln::Log::Info(mln::Event::JNI,
"WeatherRuntime: event=CLEAR_FIELDS sourceId=" + id + " result=" +
(cleared ? "SUCCESS" : "REJECTED"));
return cleared;
}
jni::jboolean NativeMapView::resetRasterArraySourceWeatherRuntime(JNIEnv& env, const jni::String& sourceId) {
const auto id = jni::Make<std::string>(env, sourceId);
auto* source = dynamic_cast<mln::style::RasterArraySource*>(map->getStyle().getSource(id));
if (!source) return false;
const bool reset = source->resetWeatherRuntimeState();
mln::Log::Info(mln::Event::JNI, "WeatherRuntime: event=TERMINAL_RESET sourceId=" + id +
" result=" + (reset ? "SUCCESS" : "REJECTED"));
return reset;
}
jni::jboolean NativeMapView::seekRasterArraySourceTimelineToTimestamp(JNIEnv& env,
const jni::String& sourceId,
const jni::jlong timestamp) {
const auto id = jni::Make<std::string>(env, sourceId);
auto* source = dynamic_cast<mln::style::RasterArraySource*>(map->getStyle().getSource(id));
return source && timestamp >= 0 && source->seekRuntimeTimelineToTimestamp(static_cast<uint64_t>(timestamp));
}
jni::jboolean NativeMapView::controlRasterArraySourceTimeline(JNIEnv& env,
const jni::String& sourceId,
const jni::jint command,
const jni::jfloat value) {
const auto id = jni::Make<std::string>(env, sourceId);
auto* source = dynamic_cast<mln::style::RasterArraySource*>(map->getStyle().getSource(id));
if (!source) return false;
const bool success = source->controlRuntimeTimeline(command, value);
mln::Log::Info(mln::Event::JNI,
"ParticleTimeline: event=COMMAND sourceId=" + id + " command=" + std::to_string(command) +
" result=" + (success ? "SUCCESS" : "REJECTED"));
return success;
}
jni::jint NativeMapView::submitRasterArraySourceEncodedField(JNIEnv& env, const jni::String& sourceId, const jni::String& fieldKey, const jni::String& fieldId, jni::jlong generation, jni::jlong dataVersion, const jni::String& contractJson, const jni::Array<jbyte>& qbin) {
const auto id=jni::Make<std::string>(env,sourceId); auto* source=dynamic_cast<mln::style::RasterArraySource*>(map->getStyle().getSource(id));
if(!source) return 0; // bridge-layer SOURCE_MISSING sentinel, never a Core result
if(generation<=0 || dataVersion<0 || !qbin || !mln::particle::ingress::isValidEncodedFieldTransportSize(qbin.Length(env))) return static_cast<jni::jint>(mln::style::RasterArrayEncodedFieldSubmitResult::RejectedDecode);
const auto contract=mln::particle::parseResolvedFieldProfileContractJson(jni::Make<std::string>(env,contractJson));
if(!contract) return static_cast<jni::jint>(mln::style::RasterArrayEncodedFieldSubmitResult::RejectedContract);
std::vector<uint8_t> bytes(qbin.Length(env)); if(!bytes.empty()) jni::GetArrayRegion(env,*qbin,0,bytes.size(),reinterpret_cast<jbyte*>(bytes.data()));
const auto result=source->submitEncodedField(jni::Make<std::string>(env,fieldKey), {jni::Make<std::string>(env,fieldId),static_cast<uint64_t>(generation),static_cast<uint64_t>(dataVersion)}, *contract, bytes);
return static_cast<jni::jint>(result);
}
jni::jint NativeMapView::clearRasterArraySourceEncodedField(JNIEnv& env,const jni::String& sourceId,const jni::String& fieldKey) { auto* s=dynamic_cast<mln::style::RasterArraySource*>(map->getStyle().getSource(jni::Make<std::string>(env,sourceId))); if(!s)return 0; return s->clearEncodedField(jni::Make<std::string>(env,fieldKey)) ? 1 : -1; }
jni::jint NativeMapView::clearAllRasterArraySourceEncodedFields(JNIEnv& env,const jni::String& sourceId) { auto* s=dynamic_cast<mln::style::RasterArraySource*>(map->getStyle().getSource(jni::Make<std::string>(env,sourceId))); if(!s)return 0; s->clearAllEncodedFields(); return 1; }
jni::jint NativeMapView::setRasterArraySourceEncodedColorBinding(JNIEnv& env, const jni::String& sourceId, const jni::String& fieldKey, jni::jint semanticID, jni::jint groupID, jni::jint valueClass, jni::jint unitID) { auto* s=dynamic_cast<mln::style::RasterArraySource*>(map->getStyle().getSource(jni::Make<std::string>(env,sourceId))); if(!s)return 0; if(semanticID<=0||semanticID>UINT16_MAX||groupID<0||groupID>UINT16_MAX||unitID<0||unitID>UINT16_MAX||(valueClass<0||valueClass>2))return -1; s->setColorBinding({jni::Make<std::string>(env,fieldKey),{static_cast<uint16_t>(semanticID),static_cast<uint16_t>(groupID)},static_cast<mln::particle::FieldValueClass>(valueClass),static_cast<uint16_t>(unitID)}); return 1; }
jni::jint NativeMapView::setRasterArraySourceEncodedParticleBinding(JNIEnv& env, const jni::String& sourceId, const jni::String& fieldKey, jni::jint mode, jni::jint uSemanticID, jni::jint uGroupID, jni::jint vSemanticID, jni::jint vGroupID, jni::jint directionSemanticID, jni::jint directionGroupID, jni::jint unitID) { auto* s=dynamic_cast<mln::style::RasterArraySource*>(map->getStyle().getSource(jni::Make<std::string>(env,sourceId))); if(!s)return 0; const auto valid=[](jni::jint semantic,jni::jint group){return semantic>0&&semantic<=UINT16_MAX&&group>=0&&group<=UINT16_MAX;}; const auto inactive=[](jni::jint semantic,jni::jint group){return semantic==0&&group==0;}; if(unitID<0||unitID>UINT16_MAX)return -1; mln::particle::ParticleBinding binding; binding.fieldKey=jni::Make<std::string>(env,fieldKey); binding.expectedUnitID=static_cast<uint16_t>(unitID); if(mode==0&&valid(uSemanticID,uGroupID)&&valid(vSemanticID,vGroupID)&&inactive(directionSemanticID,directionGroupID)){binding.mode=mln::particle::ParticleBindingMode::VectorComponents;binding.uPlane={static_cast<uint16_t>(uSemanticID),static_cast<uint16_t>(uGroupID)};binding.vPlane={static_cast<uint16_t>(vSemanticID),static_cast<uint16_t>(vGroupID)};} else if(mode==1&&inactive(uSemanticID,uGroupID)&&inactive(vSemanticID,vGroupID)&&valid(directionSemanticID,directionGroupID)){binding.mode=mln::particle::ParticleBindingMode::DirectionAngle;binding.directionPlane={static_cast<uint16_t>(directionSemanticID),static_cast<uint16_t>(directionGroupID)};} else return -1; s->setParticleBinding(std::move(binding)); return 1; }
jni::jint NativeMapView::setRasterArraySourceEncodedForecastTime(JNIEnv& env,const jni::String& sourceId,jni::jlong timestamp) { auto* s=dynamic_cast<mln::style::RasterArraySource*>(map->getStyle().getSource(jni::Make<std::string>(env,sourceId))); if(!s)return 0; return timestamp>=0&&s->setRequestedForecastTime(static_cast<uint64_t>(timestamp))?1:-1; }
jni::jint NativeMapView::setRasterArraySourceEncodedForecastPlaybackState(JNIEnv& env,const jni::String& sourceId,jni::jboolean playing) { auto* s=dynamic_cast<mln::style::RasterArraySource*>(map->getStyle().getSource(jni::Make<std::string>(env,sourceId))); if(!s)return 0; s->setForecastPlaybackState(playing ? mln::particle::ForecastPlaybackState::Playing : mln::particle::ForecastPlaybackState::Paused); return 1; }
jni::jint NativeMapView::setRasterArraySourceEncodedForecastPlaybackSpeed(JNIEnv& env,const jni::String& sourceId,jni::jfloat speed) { auto* s=dynamic_cast<mln::style::RasterArraySource*>(map->getStyle().getSource(jni::Make<std::string>(env,sourceId))); if(!s)return 0; return s->setForecastPlaybackSpeed(speed)?1:-1; }
void NativeMapView::addLayer(JNIEnv& env, jlong nativeLayerPtr, const jni::String& before) {
assert(nativeLayerPtr != 0);
@@ -1255,6 +1671,10 @@ jni::Local<jni::Object<Source>> NativeMapView::getSource(JNIEnv& env, const jni:
return jni::NewLocal(env, Source::peerForCoreSource(env, *coreSource, *rendererFrontend, *map));
}
jni::jboolean NativeMapView::hasStyleSource(JNIEnv& env, const jni::String& sourceId) {
return map->getStyle().getSource(jni::Make<std::string>(env, sourceId)) != nullptr;
}
void NativeMapView::addSource(JNIEnv& env, const jni::Object<Source>& obj, jlong sourcePtr) {
assert(sourcePtr != 0);
@@ -1454,6 +1874,11 @@ void NativeMapView::registerNative(jni::JNIEnv& env) {
METHOD(&NativeMapView::addMarkers, "nativeAddMarkers"),
METHOD(&NativeMapView::setDebug, "nativeSetDebug"),
METHOD(&NativeMapView::getDebug, "nativeGetDebug"),
METHOD(&NativeMapView::setRetainedUnderlayEnabled, "nativeSetRetainedUnderlayEnabled"),
METHOD(&NativeMapView::setContinuousLayerRange, "nativeSetContinuousLayerRange"),
METHOD(&NativeMapView::setRenderSegmentPlan, "nativeSetRenderSegmentPlan"),
METHOD(&NativeMapView::getRenderSegmentDiagnostics, "nativeGetRenderSegmentDiagnostics"),
METHOD(&NativeMapView::getRetainedUnderlayDiagnostics, "nativeGetRetainedUnderlayDiagnostics"),
METHOD(&NativeMapView::getActionJournalLogFiles, "nativeGetActionJournalLogFiles"),
METHOD(&NativeMapView::getActionJournalLog, "nativeGetActionJournalLog"),
METHOD(&NativeMapView::clearActionJournalLog, "nativeClearActionJournalLog"),
@@ -1487,6 +1912,33 @@ void NativeMapView::registerNative(jni::JNIEnv& env) {
METHOD(&NativeMapView::getLight, "nativeGetLight"),
METHOD(&NativeMapView::getLayers, "nativeGetLayers"),
METHOD(&NativeMapView::getLayer, "nativeGetLayer"),
METHOD(&NativeMapView::hasStyleLayer, "nativeHasStyleLayer"),
METHOD(&NativeMapView::setStyleLayerFloatProperty, "nativeSetStyleLayerFloatProperty"),
METHOD(&NativeMapView::setWeatherColorOpacity, "nativeSetWeatherColorOpacity"),
METHOD(&NativeMapView::setStyleLayerVisibility, "nativeSetStyleLayerVisibility"),
METHOD(&NativeMapView::setRasterParticleColors, "nativeSetRasterParticleColors"),
METHOD(&NativeMapView::setRasterParticleColor, "nativeSetRasterParticleColor"),
METHOD(&NativeMapView::setRasterParticleTrailColor, "nativeSetRasterParticleTrailColor"),
METHOD(&NativeMapView::setRasterArraySourceTargetFps, "nativeSetRasterArraySourceTargetFps"),
METHOD(&NativeMapView::getRasterArraySourceTargetFps, "nativeGetRasterArraySourceTargetFps"),
METHOD(&NativeMapView::setWeatherColorStops, "nativeSetWeatherColorStops"),
METHOD(&NativeMapView::getWeatherColorStopCapacity, "nativeGetWeatherColorStopCapacity"),
METHOD(&NativeMapView::advanceRasterArraySourceFieldWindow, "nativeAdvanceRasterArraySourceFieldWindow"),
METHOD(&NativeMapView::repeatRasterArraySourceFieldWindow, "nativeRepeatRasterArraySourceFieldWindow"),
METHOD(&NativeMapView::submitRasterArraySourceHostField, "nativeSubmitRasterArraySourceHostField"),
METHOD(&NativeMapView::submitRasterArraySourceHostFieldV2, "nativeSubmitRasterArraySourceHostFieldV2"),
METHOD(&NativeMapView::clearRasterArraySourceHostFields, "nativeClearRasterArraySourceHostFields"),
METHOD(&NativeMapView::resetRasterArraySourceWeatherRuntime, "nativeResetRasterArraySourceWeatherRuntime"),
METHOD(&NativeMapView::seekRasterArraySourceTimelineToTimestamp, "nativeSeekRasterArraySourceTimelineToTimestamp"),
METHOD(&NativeMapView::controlRasterArraySourceTimeline, "nativeControlRasterArraySourceTimeline"),
METHOD(&NativeMapView::submitRasterArraySourceEncodedField, "nativeSubmitRasterArraySourceEncodedField"),
METHOD(&NativeMapView::clearRasterArraySourceEncodedField, "nativeClearRasterArraySourceEncodedField"),
METHOD(&NativeMapView::clearAllRasterArraySourceEncodedFields, "nativeClearAllRasterArraySourceEncodedFields"),
METHOD(&NativeMapView::setRasterArraySourceEncodedColorBinding, "nativeSetRasterArraySourceEncodedColorBinding"),
METHOD(&NativeMapView::setRasterArraySourceEncodedParticleBinding, "nativeSetRasterArraySourceEncodedParticleBinding"),
METHOD(&NativeMapView::setRasterArraySourceEncodedForecastTime, "nativeSetRasterArraySourceEncodedForecastTime"),
METHOD(&NativeMapView::setRasterArraySourceEncodedForecastPlaybackState, "nativeSetRasterArraySourceEncodedForecastPlaybackState"),
METHOD(&NativeMapView::setRasterArraySourceEncodedForecastPlaybackSpeed, "nativeSetRasterArraySourceEncodedForecastPlaybackSpeed"),
METHOD(&NativeMapView::addLayer, "nativeAddLayer"),
METHOD(&NativeMapView::addLayerAbove, "nativeAddLayerAbove"),
METHOD(&NativeMapView::addLayerAt, "nativeAddLayerAt"),
@@ -1494,6 +1946,7 @@ void NativeMapView::registerNative(jni::JNIEnv& env) {
METHOD(&NativeMapView::removeLayer, "nativeRemoveLayer"),
METHOD(&NativeMapView::getSources, "nativeGetSources"),
METHOD(&NativeMapView::getSource, "nativeGetSource"),
METHOD(&NativeMapView::hasStyleSource, "nativeHasStyleSource"),
METHOD(&NativeMapView::addSource, "nativeAddSource"),
METHOD(&NativeMapView::removeSource, "nativeRemoveSource"),
METHOD(&NativeMapView::addImage, "nativeAddImage"),

View File

@@ -201,6 +201,16 @@ public:
jni::jboolean getDebug(JNIEnv&);
void setRetainedUnderlayEnabled(JNIEnv&, jni::jboolean);
jni::jboolean setContinuousLayerRange(JNIEnv&, const jni::String&, const jni::String&);
jni::jboolean setRenderSegmentPlan(JNIEnv&, const jni::String&);
jni::Local<jni::String> getRenderSegmentDiagnostics(JNIEnv&);
jni::Local<jni::Array<jni::String>> getRetainedUnderlayDiagnostics(JNIEnv&);
jni::Local<jni::Array<jni::String>> getActionJournalLogFiles(JNIEnv&);
jni::Local<jni::Array<jni::String>> getActionJournalLog(JNIEnv&);
@@ -281,6 +291,44 @@ public:
jni::Local<jni::Array<jni::Object<Layer>>> getLayers(JNIEnv&);
jni::Local<jni::Object<Layer>> getLayer(JNIEnv&, const jni::String&);
jni::jboolean hasStyleLayer(JNIEnv&, const jni::String&);
jni::jboolean setStyleLayerFloatProperty(JNIEnv&, const jni::String&, const jni::String&, jni::jfloat);
jni::jboolean setWeatherColorOpacity(JNIEnv&, const jni::String&, jni::jfloat);
jni::jboolean setStyleLayerVisibility(JNIEnv&, const jni::String&, jni::jboolean);
jni::jboolean setRasterParticleColors(JNIEnv&, const jni::String&, const jni::Array<jni::jfloat>&, const jni::Array<jni::jfloat>&);
jni::jboolean setRasterParticleColor(JNIEnv&, const jni::String&, const jni::Array<jni::jfloat>&);
jni::jboolean setRasterParticleTrailColor(JNIEnv&, const jni::String&, const jni::Array<jni::jfloat>&);
jni::jboolean setRasterArraySourceTargetFps(JNIEnv&, const jni::String&, jni::jint);
jni::jint getRasterArraySourceTargetFps(JNIEnv&, const jni::String&);
jni::jboolean setWeatherColorStops(JNIEnv&, const jni::String&, const jni::Array<jni::jfloat>&, const jni::Array<jni::jfloat>&);
jni::jint getWeatherColorStopCapacity(JNIEnv&);
jni::jboolean advanceRasterArraySourceFieldWindow(JNIEnv&, const jni::String&);
jni::jboolean repeatRasterArraySourceFieldWindow(JNIEnv&, const jni::String&);
jni::jint submitRasterArraySourceHostField(JNIEnv&,
const jni::String&,
const jni::String&,
const jni::Array<jbyte>&);
jni::jint submitRasterArraySourceHostFieldV2(JNIEnv&,
const jni::String&,
const jni::String&,
const jni::Array<jbyte>&,
jni::jboolean,
jni::jint,
jni::jboolean,
jni::jint,
jni::jint);
jni::jboolean clearRasterArraySourceHostFields(JNIEnv&, const jni::String&);
jni::jboolean resetRasterArraySourceWeatherRuntime(JNIEnv&, const jni::String&);
jni::jboolean seekRasterArraySourceTimelineToTimestamp(JNIEnv&, const jni::String&, jni::jlong);
jni::jboolean controlRasterArraySourceTimeline(JNIEnv&, const jni::String&, jni::jint, jni::jfloat);
jni::jint submitRasterArraySourceEncodedField(JNIEnv&, const jni::String&, const jni::String&, const jni::String&, jni::jlong, jni::jlong, const jni::String&, const jni::Array<jbyte>&);
jni::jint clearRasterArraySourceEncodedField(JNIEnv&, const jni::String&, const jni::String&);
jni::jint clearAllRasterArraySourceEncodedFields(JNIEnv&, const jni::String&);
jni::jint setRasterArraySourceEncodedColorBinding(JNIEnv&, const jni::String&, const jni::String&, jni::jint, jni::jint, jni::jint, jni::jint);
jni::jint setRasterArraySourceEncodedParticleBinding(JNIEnv&, const jni::String&, const jni::String&, jni::jint, jni::jint, jni::jint, jni::jint, jni::jint, jni::jint, jni::jint, jni::jint);
jni::jint setRasterArraySourceEncodedForecastTime(JNIEnv&, const jni::String&, jni::jlong);
jni::jint setRasterArraySourceEncodedForecastPlaybackState(JNIEnv&, const jni::String&, jni::jboolean);
jni::jint setRasterArraySourceEncodedForecastPlaybackSpeed(JNIEnv&, const jni::String&, jni::jfloat);
void addLayer(JNIEnv&, jlong, const jni::String&);
@@ -295,6 +343,7 @@ public:
jni::Local<jni::Array<jni::Object<Source>>> getSources(JNIEnv&);
jni::Local<jni::Object<Source>> getSource(JNIEnv&, const jni::String&);
jni::jboolean hasStyleSource(JNIEnv&, const jni::String&);
void addSource(JNIEnv&, const jni::Object<Source>&, jlong nativePtr);

View File

@@ -19,12 +19,19 @@
#include "symbol_layer.hpp"
#include "fill_extrusion_layer.hpp"
#include "location_indicator_layer.hpp"
#include <mln/layermanager/particle_validation_layer_factory.hpp>
#include <mln/layermanager/raster_particle_layer_factory.hpp>
#include <mln/layermanager/weather_color_layer_factory.hpp>
namespace mln {
namespace android {
LayerManagerAndroid::LayerManagerAndroid() {
// VALIDATION ONLY: JSON style access; intentionally no Android product API/JNI peer.
addLayerTypeCoreOnly(std::make_unique<ParticleValidationLayerFactory>());
addLayerTypeCoreOnly(std::make_unique<RasterParticleLayerFactory>());
addLayerTypeCoreOnly(std::make_unique<WeatherColorLayerFactory>());
#if defined(MBGL_LAYER_FILL_DISABLE_RUNTIME)
addLayerTypeCoreOnly(std::make_unique<FillLayerFactory>());
#elif !defined(MBGL_LAYER_FILL_DISABLE_ALL)

View File

@@ -12,6 +12,7 @@ import androidx.annotation.FloatRange;
import androidx.annotation.IntRange;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RestrictTo;
import androidx.annotation.Size;
import androidx.annotation.UiThread;
@@ -43,6 +44,7 @@ import org.maplibre.android.log.Logger;
import org.maplibre.android.offline.OfflineRegionDefinition;
import org.maplibre.android.style.expressions.Expression;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.List;
@@ -68,6 +70,7 @@ public final class MapLibreMap {
private final OnGesturesManagerInteractionListener onGesturesManagerInteractionListener;
private final List<Style.OnStyleLoaded> awaitingStyleGetters = new ArrayList<>();
private final List<OnDeveloperAnimationListener> developerAnimationStartedListeners;
private final List<OnDestroyListener> onDestroyListeners = new ArrayList<>();
@Nullable
private Style.OnStyleLoaded styleLoadedCallback;
@@ -81,8 +84,29 @@ public final class MapLibreMap {
@Nullable
private Style style;
/**
* Optional weather facade attached to this concrete map instance. It is deliberately owned by
* the map rather than by an application or bridge registry, so a style replacement and a map
* teardown cannot accidentally leave a facade pointing at another map.
*/
@Nullable
private WeatherRuntime weatherRuntime;
private boolean debugActive;
private boolean started;
private boolean destroyed;
/** Internal map-lifetime observer used by map-owned integrations. */
@RestrictTo(RestrictTo.Scope.LIBRARY)
public interface OnDestroyListener {
void onDestroy();
}
/** Removable registration for an internal observer of this concrete map's destruction. */
@RestrictTo(RestrictTo.Scope.LIBRARY)
public interface OnDestroyListenerRegistration {
void remove();
}
MapLibreMap(NativeMap map, Transform transform, UiSettings ui, Projection projection,
OnGesturesManagerInteractionListener listener, CameraChangeDispatcher cameraChangeDispatcher,
@@ -103,6 +127,38 @@ public final class MapLibreMap {
nativeMapView.triggerRepaint();
}
/** Enables or disables the renderer's map-scoped retained static underlay optimization. */
public void setRetainedUnderlayEnabled(boolean enabled) {
nativeMapView.setRetainedUnderlayEnabled(enabled);
}
/** Configures the inclusive Style-layer interval rendered continuously. */
public boolean setContinuousLayerRange(@NonNull String startLayerId, @NonNull String endLayerId) {
return nativeMapView.setContinuousLayerRange(startLayerId, endLayerId);
}
/** Configures the renderer execution topology using schema-versioned JSON. */
public boolean setRenderSegmentPlan(@NonNull String planJson) {
return nativeMapView.setRenderSegmentPlan(planJson);
}
/** Returns the pull-based Style-to-Segment-to-Layer diagnostics snapshot as JSON. */
@NonNull
public String getRenderSegmentDiagnostics() {
return nativeMapView.getRenderSegmentDiagnostics();
}
/** Returns whether the retained static underlay optimization is enabled for this map. */
public boolean isRetainedUnderlayEnabled() {
return nativeMapView.getRetainedUnderlayDiagnostics().enabled;
}
/** Returns a thread-safe, pull-based retained-underlay diagnostics snapshot. */
@NonNull
public RetainedUnderlayDiagnostics getRetainedUnderlayDiagnostics() {
return nativeMapView.getRetainedUnderlayDiagnostics();
}
/**
* Query rendering statistics overlay status.
*/
@@ -171,6 +227,67 @@ public final class MapLibreMap {
}
}
/**
* Returns whether the fully loaded Core style contains {@code layerId}.
*
* This intentionally queries Core by id instead of materialising a Java {@link
* org.maplibre.android.style.layers.Layer} peer. It therefore also supports layer types that
* are registered Core-only.
*/
public boolean hasStyleLayer(@NonNull String layerId) {
return nativeMapView.hasStyleLayer(layerId);
}
/**
* Returns whether the fully loaded Core style contains {@code sourceId}.
*/
public boolean hasStyleSource(@NonNull String sourceId) {
return nativeMapView.hasStyleSource(sourceId);
}
/**
* Replaces this map's style-scoped weather facade. The caller must have already verified the
* configured source and layers in the currently loaded style.
*/
@NonNull
public WeatherRuntime replaceWeatherRuntime(@NonNull WeatherRuntime.Config config) {
disposeWeatherRuntime();
weatherRuntime = new WeatherRuntime(this, config);
return weatherRuntime;
}
/** Returns this map's attached weather facade, or null when no style-ready facade exists. */
@Nullable
public WeatherRuntime getWeatherRuntime() {
return weatherRuntime;
}
/** Disposes and detaches the style-scoped weather facade. Safe to call repeatedly. */
public void disposeWeatherRuntime() {
if (weatherRuntime != null) {
weatherRuntime.dispose();
weatherRuntime = null;
}
}
/** Registers a removable observer for destruction of this concrete map instance. */
@NonNull
@RestrictTo(RestrictTo.Scope.LIBRARY)
public OnDestroyListenerRegistration addOnDestroyListener(@NonNull OnDestroyListener listener) {
if (destroyed) {
listener.onDestroy();
return () -> { };
}
onDestroyListeners.add(listener);
WeakReference<MapLibreMap> mapReference = new WeakReference<>(this);
return () -> {
MapLibreMap map = mapReference.get();
if (map != null) {
map.onDestroyListeners.remove(listener);
}
};
}
/**
* Called when the hosting Activity/Fragment onStart() method is called.
*/
@@ -221,6 +338,16 @@ public final class MapLibreMap {
* Called when the hosting Activity/Fragment onDestroy()/onDestroyView() method is called.
*/
void onDestroy() {
if (destroyed) {
return;
}
destroyed = true;
List<OnDestroyListener> listeners = new ArrayList<>(onDestroyListeners);
onDestroyListeners.clear();
for (OnDestroyListener listener : listeners) {
listener.onDestroy();
}
disposeWeatherRuntime();
locationComponent.onDestroy();
if (style != null) {
style.clear();
@@ -286,6 +413,119 @@ public final class MapLibreMap {
return nativeMapView.getNativePtr();
}
/** Updates a constant property on an existing Core style layer without requiring a Java Layer peer. */
public boolean setStyleLayerFloatProperty(String layerId, String propertyName, float value) {
return nativeMapView.setStyleLayerFloatProperty(layerId, propertyName, value);
}
/** Updates the opacity of an existing Core-owned WeatherColor layer without a Java Layer peer. */
public boolean setWeatherColorOpacity(String layerId, float opacity) {
return nativeMapView.setWeatherColorOpacity(layerId, opacity);
}
/** Low-frequency style visibility used by the typed WeatherRuntime facade. */
public boolean setStyleLayerVisibility(String layerId, boolean visible) {
return nativeMapView.setStyleLayerVisibility(layerId, visible);
}
/** Sets independent particle and trail RGBA colours without a field upload. */
public boolean setRasterParticleColors(String layerId, float[] particleColor, float[] trailColor) {
return nativeMapView.setRasterParticleColors(layerId, particleColor, trailColor);
}
public boolean setRasterParticleColor(String layerId, float[] particleColor) {
return nativeMapView.setRasterParticleColor(layerId, particleColor);
}
public boolean setRasterParticleTrailColor(String layerId, float[] trailColor) {
return nativeMapView.setRasterParticleTrailColor(layerId, trailColor);
}
/** Sets the source-local weather cadence (0=auto, 30 or 60 FPS). */
public boolean setRasterArraySourceTargetFps(String sourceId, int targetFps) {
return nativeMapView.setRasterArraySourceTargetFps(sourceId, targetFps);
}
public int getRasterArraySourceTargetFps(String sourceId) {
return nativeMapView.getRasterArraySourceTargetFps(sourceId);
}
/** Installs a validated WeatherColor speed ramp without changing particle style. */
public boolean setWeatherColorStops(String layerId, float[] speedsMps, float[] rgbaStops) {
return nativeMapView.setWeatherColorStops(layerId, speedsMps, rgbaStops);
}
/** Returns the Core-owned WeatherColor stop capacity for typed Host validation. */
public int getWeatherColorStopCapacity() {
return nativeMapView.getWeatherColorStopCapacity();
}
/** Advances a Source-owned raster-array runtime Field window without reloading the Style. */
public boolean advanceRasterArraySourceFieldWindow(String sourceId) {
return nativeMapView.advanceRasterArraySourceFieldWindow(sourceId);
}
/** Resubmits the Source-owned current T1 to validate the Runtime Field NOOP path. */
public boolean repeatRasterArraySourceFieldWindow(String sourceId) {
return nativeMapView.repeatRasterArraySourceFieldWindow(sourceId);
}
/**
* Submits one canonical particle Field as provider-neutral metadata plus contiguous binary U/V bytes.
* This is a low-frequency ingress; Timeline and Runtime Field Window ownership remain in Core.
*/
public int submitRasterArraySourceHostField(String sourceId, String metadata, byte[] payload) {
return nativeMapView.submitRasterArraySourceHostField(sourceId, metadata, payload);
}
/** Explicit v2 ingress: packed bytes are split by presence flags and lengths in JNI, never in JS. */
public int submitRasterArraySourceHostFieldV2(String sourceId, String metadata, byte[] packedPlanes,
boolean scalarPresent, int scalarLength,
boolean vectorPresent, int vectorLength, int validityLength) {
return nativeMapView.submitRasterArraySourceHostFieldV2(sourceId, metadata, packedPlanes, scalarPresent,
scalarLength, vectorPresent, vectorLength, validityLength);
}
/** Clears Host-owned canonical fields while retaining the raster-array Source and Style layers. */
public boolean clearRasterArraySourceHostFields(String sourceId) {
return nativeMapView.clearRasterArraySourceHostFields(sourceId);
}
/** Internal terminal reset for V3+V4 state on the same existing Style source. */
boolean resetRasterArraySourceWeatherRuntime(String sourceId) {
return nativeMapView.resetRasterArraySourceWeatherRuntime(sourceId);
}
/** Requests a Core-owned T0/T1/MIX selection for a producer timestamp in milliseconds. */
public boolean seekRasterArraySourceTimelineToTimestamp(String sourceId, long timestamp) {
return nativeMapView.seekRasterArraySourceTimelineToTimestamp(sourceId, timestamp);
}
/** Sends a low-frequency playback command to the Core-owned raster-array timeline. */
public boolean controlRasterArraySourceTimeline(String sourceId, int command, float value) {
return nativeMapView.controlRasterArraySourceTimeline(sourceId, command, value);
}
/** V4-only QBIN ingress. It never enters the V3 runtime timeline. */
public int submitRasterArraySourceEncodedField(String sourceId, String fieldKey, String fieldId, long generation, long dataVersion, String contractJson, byte[] qbin) { return nativeMapView.submitRasterArraySourceEncodedField(sourceId, fieldKey, fieldId, generation, dataVersion, contractJson, qbin); }
public int clearRasterArraySourceEncodedField(String sourceId, String fieldKey) { return nativeMapView.clearRasterArraySourceEncodedField(sourceId, fieldKey); }
public int clearAllRasterArraySourceEncodedFields(String sourceId) { return nativeMapView.clearAllRasterArraySourceEncodedFields(sourceId); }
public int setRasterArraySourceEncodedColorBinding(String sourceId, String fieldKey, int semanticID, int groupID, int valueClass, int unitID) { return nativeMapView.setRasterArraySourceEncodedColorBinding(sourceId, fieldKey, semanticID, groupID, valueClass, unitID); }
public int setRasterArraySourceEncodedParticleBinding(String sourceId, String fieldKey, int mode, int uSemanticID, int uGroupID, int vSemanticID, int vGroupID, int directionSemanticID, int directionGroupID, int unitID) { return nativeMapView.setRasterArraySourceEncodedParticleBinding(sourceId, fieldKey, mode, uSemanticID, uGroupID, vSemanticID, vGroupID, directionSemanticID, directionGroupID, unitID); }
public int setRasterArraySourceEncodedForecastTime(String sourceId, long timestamp) { return nativeMapView.setRasterArraySourceEncodedForecastTime(sourceId, timestamp); }
public int setRasterArraySourceEncodedForecastPlaybackState(String sourceId, boolean playing) { return nativeMapView.setRasterArraySourceEncodedForecastPlaybackState(sourceId, playing); }
public int setRasterArraySourceEncodedForecastPlaybackSpeed(String sourceId, float speed) { return nativeMapView.setRasterArraySourceEncodedForecastPlaybackSpeed(sourceId, speed); }
public static final int HOST_FIELD_REJECTED_SOURCE_MODE = -1;
public static final int HOST_FIELD_REJECTED_ADAPTER = -2;
public static final int HOST_FIELD_REJECTED_INCOMPATIBLE = -3;
public static final int HOST_FIELD_REJECTED_NON_MONOTONIC = -4;
public static final int HOST_FIELD_REJECTED_CAPACITY = -5;
public static final int HOST_FIELD_ACCEPTED_PENDING_INITIAL_WINDOW = 1;
public static final int HOST_FIELD_INITIAL_WINDOW_READY = 2;
public static final int HOST_FIELD_ACCEPTED_NEXT_SNAPSHOT = 3;
public static final int HOST_FIELD_DUPLICATE_NOOP = 4;
// Style
/**
@@ -1138,6 +1378,9 @@ public final class MapLibreMap {
* @see Style
*/
public void setStyle(Style.Builder builder, final Style.OnStyleLoaded callback) {
// A WeatherRuntime only controls objects from one loaded style. Never let it survive a
// replacement where identical IDs could refer to newly-created style objects.
disposeWeatherRuntime();
styleLoadedCallback = callback;
locationComponent.onStartLoadingMap();
if (style != null) {

View File

@@ -139,6 +139,54 @@ interface NativeMap {
Layer getLayer(String layerId);
boolean hasStyleLayer(@NonNull String layerId);
boolean setStyleLayerFloatProperty(String layerId, String propertyName, float value);
boolean setWeatherColorOpacity(String layerId, float opacity);
boolean setStyleLayerVisibility(String layerId, boolean visible);
boolean setRasterParticleColors(String layerId, float[] particleColor, float[] trailColor);
boolean setRasterParticleColor(String layerId, float[] particleColor);
boolean setRasterParticleTrailColor(String layerId, float[] trailColor);
boolean setRasterArraySourceTargetFps(String sourceId, int targetFps);
int getRasterArraySourceTargetFps(String sourceId);
boolean setWeatherColorStops(String layerId, float[] speedsMps, float[] rgbaStops);
int getWeatherColorStopCapacity();
boolean advanceRasterArraySourceFieldWindow(String sourceId);
boolean repeatRasterArraySourceFieldWindow(String sourceId);
int submitRasterArraySourceHostField(String sourceId, String metadata, byte[] payload);
int submitRasterArraySourceHostFieldV2(String sourceId, String metadata, byte[] packedPlanes,
boolean scalarPresent, int scalarLength,
boolean vectorPresent, int vectorLength, int validityLength);
boolean clearRasterArraySourceHostFields(String sourceId);
boolean resetRasterArraySourceWeatherRuntime(String sourceId);
boolean seekRasterArraySourceTimelineToTimestamp(String sourceId, long timestamp);
boolean controlRasterArraySourceTimeline(String sourceId, int command, float value);
int submitRasterArraySourceEncodedField(String sourceId, String fieldKey, String fieldId, long generation, long dataVersion, String contractJson, byte[] qbin);
int clearRasterArraySourceEncodedField(String sourceId, String fieldKey);
int clearAllRasterArraySourceEncodedFields(String sourceId);
int setRasterArraySourceEncodedColorBinding(String sourceId, String fieldKey, int semanticID, int groupID, int valueClass, int unitID);
int setRasterArraySourceEncodedParticleBinding(String sourceId, String fieldKey, int mode, int uSemanticID, int uGroupID, int vSemanticID, int vGroupID, int directionSemanticID, int directionGroupID, int unitID);
int setRasterArraySourceEncodedForecastTime(String sourceId, long timestamp);
int setRasterArraySourceEncodedForecastPlaybackState(String sourceId, boolean playing);
int setRasterArraySourceEncodedForecastPlaybackSpeed(String sourceId, float speed);
boolean removeLayer(@NonNull String layerId);
boolean removeLayer(@NonNull Layer layer);
@@ -152,6 +200,8 @@ interface NativeMap {
Source getSource(@NonNull String sourceId);
boolean hasStyleSource(@NonNull String sourceId);
boolean removeSource(@NonNull String sourceId);
boolean removeSource(@NonNull Source source);
@@ -237,6 +287,16 @@ interface NativeMap {
boolean getDebug();
void setRetainedUnderlayEnabled(boolean enabled);
boolean setContinuousLayerRange(String startLayerId, String endLayerId);
boolean setRenderSegmentPlan(String planJson);
String getRenderSegmentDiagnostics();
RetainedUnderlayDiagnostics getRetainedUnderlayDiagnostics();
String[] getActionJournalLogFiles();
String[] getActionJournalLog();

View File

@@ -653,6 +653,41 @@ final class NativeMapView implements NativeMap {
return nativeGetDebug();
}
@Override
public void setRetainedUnderlayEnabled(boolean enabled) {
if (checkState("setRetainedUnderlayEnabled")) {
return;
}
nativeSetRetainedUnderlayEnabled(enabled);
}
@Override
public boolean setContinuousLayerRange(@NonNull String startLayerId, @NonNull String endLayerId) {
if (checkState("setContinuousLayerRange")) return false;
return nativeSetContinuousLayerRange(startLayerId, endLayerId);
}
@Override
public boolean setRenderSegmentPlan(@NonNull String planJson) {
if (checkState("setRenderSegmentPlan")) return false;
return nativeSetRenderSegmentPlan(planJson);
}
@NonNull
@Override
public String getRenderSegmentDiagnostics() {
if (checkState("getRenderSegmentDiagnostics")) return "{\"schemaVersion\":1,\"valid\":false}";
return nativeGetRenderSegmentDiagnostics();
}
@Override
public RetainedUnderlayDiagnostics getRetainedUnderlayDiagnostics() {
if (checkState("getRetainedUnderlayDiagnostics")) {
return RetainedUnderlayDiagnostics.unavailable();
}
return new RetainedUnderlayDiagnostics(nativeGetRetainedUnderlayDiagnostics());
}
@Override
public String[] getActionJournalLogFiles() {
if (checkState("getActionJournalLogFiles")) {
@@ -950,6 +985,118 @@ final class NativeMapView implements NativeMap {
return nativeGetLayer(layerId);
}
@Override
public boolean hasStyleLayer(@NonNull String layerId) {
return !checkState("hasStyleLayer") && nativeHasStyleLayer(layerId);
}
@Override
public boolean setStyleLayerFloatProperty(String layerId, String propertyName, float value) {
return !checkState("setStyleLayerFloatProperty")
&& nativeSetStyleLayerFloatProperty(layerId, propertyName, value);
}
@Override
public boolean setWeatherColorOpacity(String layerId, float opacity) {
return !checkState("setWeatherColorOpacity") && nativeSetWeatherColorOpacity(layerId, opacity);
}
public boolean setStyleLayerVisibility(String layerId, boolean visible) {
return !checkState("setStyleLayerVisibility") && nativeSetStyleLayerVisibility(layerId, visible);
}
public boolean setRasterParticleColors(String layerId, float[] particleColor, float[] trailColor) {
return !checkState("setRasterParticleColors") && nativeSetRasterParticleColors(layerId, particleColor, trailColor);
}
public boolean setRasterParticleColor(String layerId, float[] particleColor) {
return !checkState("setRasterParticleColor") && nativeSetRasterParticleColor(layerId, particleColor);
}
public boolean setRasterParticleTrailColor(String layerId, float[] trailColor) {
return !checkState("setRasterParticleTrailColor") && nativeSetRasterParticleTrailColor(layerId, trailColor);
}
@Override
public boolean setRasterArraySourceTargetFps(String sourceId, int targetFps) {
return !checkState("setRasterArraySourceTargetFps") && nativeSetRasterArraySourceTargetFps(sourceId, targetFps);
}
@Override
public int getRasterArraySourceTargetFps(String sourceId) {
return checkState("getRasterArraySourceTargetFps") ? 0 : nativeGetRasterArraySourceTargetFps(sourceId);
}
public boolean setWeatherColorStops(String layerId, float[] speedsMps, float[] rgbaStops) {
return !checkState("setWeatherColorStops") && nativeSetWeatherColorStops(layerId, speedsMps, rgbaStops);
}
public int getWeatherColorStopCapacity() {
return checkState("getWeatherColorStopCapacity") ? 0 : nativeGetWeatherColorStopCapacity();
}
@Override
public boolean advanceRasterArraySourceFieldWindow(String sourceId) {
return !checkState("advanceRasterArraySourceFieldWindow")
&& nativeAdvanceRasterArraySourceFieldWindow(sourceId);
}
@Override
public boolean repeatRasterArraySourceFieldWindow(String sourceId) {
return !checkState("repeatRasterArraySourceFieldWindow")
&& nativeRepeatRasterArraySourceFieldWindow(sourceId);
}
@Override
public int submitRasterArraySourceHostField(String sourceId, String metadata, byte[] payload) {
return checkState("submitRasterArraySourceHostField")
? MapLibreMap.HOST_FIELD_REJECTED_SOURCE_MODE
: nativeSubmitRasterArraySourceHostField(sourceId, metadata, payload);
}
@Override
public int submitRasterArraySourceHostFieldV2(String sourceId, String metadata, byte[] packedPlanes,
boolean scalarPresent, int scalarLength,
boolean vectorPresent, int vectorLength, int validityLength) {
return checkState("submitRasterArraySourceHostFieldV2")
? MapLibreMap.HOST_FIELD_REJECTED_SOURCE_MODE
: nativeSubmitRasterArraySourceHostFieldV2(sourceId, metadata, packedPlanes, scalarPresent, scalarLength,
vectorPresent, vectorLength, validityLength);
}
@Override
public boolean clearRasterArraySourceHostFields(String sourceId) {
return !checkState("clearRasterArraySourceHostFields")
&& nativeClearRasterArraySourceHostFields(sourceId);
}
@Override
public boolean resetRasterArraySourceWeatherRuntime(String sourceId) {
return !checkState("resetRasterArraySourceWeatherRuntime")
&& nativeResetRasterArraySourceWeatherRuntime(sourceId);
}
@Override
public boolean seekRasterArraySourceTimelineToTimestamp(String sourceId, long timestamp) {
return !checkState("seekRasterArraySourceTimelineToTimestamp")
&& nativeSeekRasterArraySourceTimelineToTimestamp(sourceId, timestamp);
}
@Override
public boolean controlRasterArraySourceTimeline(String sourceId, int command, float value) {
return !checkState("controlRasterArraySourceTimeline")
&& nativeControlRasterArraySourceTimeline(sourceId, command, value);
}
@Override public int submitRasterArraySourceEncodedField(String sourceId, String fieldKey, String fieldId, long generation, long dataVersion, String contractJson, byte[] qbin) { return checkState("submitRasterArraySourceEncodedField") ? 0 : nativeSubmitRasterArraySourceEncodedField(sourceId, fieldKey, fieldId, generation, dataVersion, contractJson, qbin); }
@Override public int clearRasterArraySourceEncodedField(String sourceId, String fieldKey) { return checkState("clearRasterArraySourceEncodedField") ? 0 : nativeClearRasterArraySourceEncodedField(sourceId, fieldKey); }
@Override public int clearAllRasterArraySourceEncodedFields(String sourceId) { return checkState("clearAllRasterArraySourceEncodedFields") ? 0 : nativeClearAllRasterArraySourceEncodedFields(sourceId); }
@Override public int setRasterArraySourceEncodedColorBinding(String sourceId, String fieldKey, int semanticID, int groupID, int valueClass, int unitID) { return checkState("setRasterArraySourceEncodedColorBinding") ? 0 : nativeSetRasterArraySourceEncodedColorBinding(sourceId, fieldKey, semanticID, groupID, valueClass, unitID); }
@Override public int setRasterArraySourceEncodedParticleBinding(String sourceId, String fieldKey, int mode, int uSemanticID, int uGroupID, int vSemanticID, int vGroupID, int directionSemanticID, int directionGroupID, int unitID) { return checkState("setRasterArraySourceEncodedParticleBinding") ? 0 : nativeSetRasterArraySourceEncodedParticleBinding(sourceId, fieldKey, mode, uSemanticID, uGroupID, vSemanticID, vGroupID, directionSemanticID, directionGroupID, unitID); }
@Override public int setRasterArraySourceEncodedForecastTime(String sourceId, long timestamp) { return checkState("setRasterArraySourceEncodedForecastTime") ? 0 : nativeSetRasterArraySourceEncodedForecastTime(sourceId, timestamp); }
@Override public int setRasterArraySourceEncodedForecastPlaybackState(String sourceId, boolean playing) { return checkState("setRasterArraySourceEncodedForecastPlaybackState") ? 0 : nativeSetRasterArraySourceEncodedForecastPlaybackState(sourceId, playing); }
@Override public int setRasterArraySourceEncodedForecastPlaybackSpeed(String sourceId, float speed) { return checkState("setRasterArraySourceEncodedForecastPlaybackSpeed") ? 0 : nativeSetRasterArraySourceEncodedForecastPlaybackSpeed(sourceId, speed); }
@Override
public void addLayer(@NonNull Layer layer) {
if (checkState("addLayer")) {
@@ -1039,6 +1186,11 @@ final class NativeMapView implements NativeMap {
return nativeGetSource(sourceId);
}
@Override
public boolean hasStyleSource(@NonNull String sourceId) {
return !checkState("hasStyleSource") && nativeHasStyleSource(sourceId);
}
@Override
public void addSource(@NonNull Source source) {
if (checkState("addSource")) {
@@ -1598,6 +1750,21 @@ final class NativeMapView implements NativeMap {
@Keep
private native boolean nativeGetDebug();
@Keep
private native void nativeSetRetainedUnderlayEnabled(boolean enabled);
@Keep
private native boolean nativeSetContinuousLayerRange(String startLayerId, String endLayerId);
@Keep
private native boolean nativeSetRenderSegmentPlan(String planJson);
@Keep
private native String nativeGetRenderSegmentDiagnostics();
@Keep
private native String[] nativeGetRetainedUnderlayDiagnostics();
@Keep
private native String[] nativeGetActionJournalLogFiles();
@@ -1688,6 +1855,57 @@ final class NativeMapView implements NativeMap {
@Keep
private native Layer nativeGetLayer(String layerId);
@Keep
private native boolean nativeHasStyleLayer(String layerId);
@Keep
private native boolean nativeSetStyleLayerFloatProperty(String layerId, String propertyName, float value);
@Keep private native boolean nativeSetWeatherColorOpacity(String layerId, float opacity);
@Keep private native boolean nativeSetStyleLayerVisibility(String layerId, boolean visible);
@Keep private native boolean nativeSetRasterParticleColors(String layerId, float[] particleColor, float[] trailColor);
@Keep private native boolean nativeSetRasterParticleColor(String layerId, float[] particleColor);
@Keep private native boolean nativeSetRasterParticleTrailColor(String layerId, float[] trailColor);
@Keep private native boolean nativeSetRasterArraySourceTargetFps(String sourceId, int targetFps);
@Keep private native int nativeGetRasterArraySourceTargetFps(String sourceId);
@Keep private native boolean nativeSetWeatherColorStops(String layerId, float[] speedsMps, float[] rgbaStops);
@Keep private native int nativeGetWeatherColorStopCapacity();
@Keep
private native boolean nativeAdvanceRasterArraySourceFieldWindow(String sourceId);
@Keep
private native boolean nativeRepeatRasterArraySourceFieldWindow(String sourceId);
@Keep
private native int nativeSubmitRasterArraySourceHostField(String sourceId, String metadata, byte[] payload);
@Keep
private native int nativeSubmitRasterArraySourceHostFieldV2(String sourceId, String metadata, byte[] packedPlanes,
boolean scalarPresent, int scalarLength,
boolean vectorPresent, int vectorLength, int validityLength);
@Keep
private native boolean nativeClearRasterArraySourceHostFields(String sourceId);
@Keep
private native boolean nativeResetRasterArraySourceWeatherRuntime(String sourceId);
@Keep
private native boolean nativeSeekRasterArraySourceTimelineToTimestamp(String sourceId, long timestamp);
@Keep
private native boolean nativeControlRasterArraySourceTimeline(String sourceId, int command, float value);
@Keep private native int nativeSubmitRasterArraySourceEncodedField(String sourceId, String fieldKey, String fieldId, long generation, long dataVersion, String contractJson, byte[] qbin);
@Keep private native int nativeClearRasterArraySourceEncodedField(String sourceId, String fieldKey);
@Keep private native int nativeClearAllRasterArraySourceEncodedFields(String sourceId);
@Keep private native int nativeSetRasterArraySourceEncodedColorBinding(String sourceId, String fieldKey, int semanticID, int groupID, int valueClass, int unitID);
@Keep private native int nativeSetRasterArraySourceEncodedParticleBinding(String sourceId, String fieldKey, int mode, int uSemanticID, int uGroupID, int vSemanticID, int vGroupID, int directionSemanticID, int directionGroupID, int unitID);
@Keep private native int nativeSetRasterArraySourceEncodedForecastTime(String sourceId, long timestamp);
@Keep private native int nativeSetRasterArraySourceEncodedForecastPlaybackState(String sourceId, boolean playing);
@Keep private native int nativeSetRasterArraySourceEncodedForecastPlaybackSpeed(String sourceId, float speed);
@Keep
private native void nativeAddLayer(long layerPtr, String before) throws CannotAddLayerException;
@@ -1711,6 +1929,9 @@ final class NativeMapView implements NativeMap {
@Keep
private native Source nativeGetSource(String sourceId);
@Keep
private native boolean nativeHasStyleSource(String sourceId);
@Keep
private native void nativeAddSource(Source source, long sourcePtr) throws CannotAddSourceException;

View File

@@ -0,0 +1,102 @@
package org.maplibre.android.maps;
import androidx.annotation.Keep;
import androidx.annotation.NonNull;
/** A pull-based snapshot of this map's retained-underlay renderer state. */
@Keep
public final class RetainedUnderlayDiagnostics {
public final boolean enabled;
public final long build;
public final long reuse;
public final long fallback;
public final long invalidation;
@NonNull public final String lastReason;
public final boolean rangeConfigured;
public final boolean rangeValid;
@NonNull public final String startLayerId;
@NonNull public final String endLayerId;
public final int startIndex;
public final int endIndex;
public final int continuousBegin;
public final int continuousEndExclusive;
public final boolean bottomPresent;
@NonNull public final String bottomCacheState;
public final long bottomBuild;
public final long bottomReuse;
public final long bottomInvalidation;
@NonNull public final String bottomLastInvalidationReason;
public final long bottomTextureCreate;
public final long bottomCompositeCreate;
public final long bottomResourceRelease;
public final boolean topPresent;
@NonNull public final String topCacheState;
public final long topBuild;
public final long topReuse;
public final long topInvalidation;
@NonNull public final String topLastInvalidationReason;
public final long topTextureCreate;
public final long topCompositeCreate;
public final long topResourceRelease;
public final boolean rangeFallback;
public final boolean fastPathEligible;
public final boolean fastPathActive;
@NonNull public final String fastPathFallbackReason;
public final long fastPathHit;
public final long fastPathMiss;
public final long fullCpuFrame;
RetainedUnderlayDiagnostics(@NonNull String[] values) {
if (values.length != 39) {
throw new IllegalArgumentException("Retained-range diagnostics require 39 values");
}
enabled = Boolean.parseBoolean(values[0]);
build = Long.parseLong(values[1]);
reuse = Long.parseLong(values[2]);
fallback = Long.parseLong(values[3]);
invalidation = Long.parseLong(values[4]);
lastReason = values[5];
rangeConfigured = Boolean.parseBoolean(values[6]);
rangeValid = Boolean.parseBoolean(values[7]);
startLayerId = values[8];
endLayerId = values[9];
startIndex = Integer.parseInt(values[10]);
endIndex = Integer.parseInt(values[11]);
continuousBegin = Integer.parseInt(values[12]);
continuousEndExclusive = Integer.parseInt(values[13]);
bottomPresent = Boolean.parseBoolean(values[14]);
bottomCacheState = values[15];
bottomBuild = Long.parseLong(values[16]);
bottomReuse = Long.parseLong(values[17]);
bottomInvalidation = Long.parseLong(values[18]);
bottomLastInvalidationReason = values[19];
bottomTextureCreate = Long.parseLong(values[20]);
bottomCompositeCreate = Long.parseLong(values[21]);
bottomResourceRelease = Long.parseLong(values[22]);
topPresent = Boolean.parseBoolean(values[23]);
topCacheState = values[24];
topBuild = Long.parseLong(values[25]);
topReuse = Long.parseLong(values[26]);
topInvalidation = Long.parseLong(values[27]);
topLastInvalidationReason = values[28];
topTextureCreate = Long.parseLong(values[29]);
topCompositeCreate = Long.parseLong(values[30]);
topResourceRelease = Long.parseLong(values[31]);
rangeFallback = Boolean.parseBoolean(values[32]);
fastPathEligible = Boolean.parseBoolean(values[33]);
fastPathActive = Boolean.parseBoolean(values[34]);
fastPathFallbackReason = values[35];
fastPathHit = Long.parseLong(values[36]);
fastPathMiss = Long.parseLong(values[37]);
fullCpuFrame = Long.parseLong(values[38]);
}
static RetainedUnderlayDiagnostics unavailable() {
return new RetainedUnderlayDiagnostics(new String[] {
"true", "0", "0", "0", "0", "unavailable", "false", "false", "", "", "-1", "-1",
"-1", "-1", "false", "disabled", "0", "0", "0", "none", "0", "0", "0",
"false", "disabled", "0", "0", "0", "none", "0", "0", "0", "false", "false",
"false", "no-prepared-tree", "0", "0", "0"
});
}
}

View File

@@ -0,0 +1,286 @@
package org.maplibre.android.maps;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import android.os.Handler;
import android.os.Looper;
import java.util.List;
import java.util.HashSet;
import java.util.Set;
import org.json.JSONException;
import org.json.JSONObject;
/**
* Main-thread, low-frequency Host facade for one raster-array weather runtime.
* It owns no provider, network, route, ETA, Snapshot, T0/T1, or GPU state.
*/
public final class WeatherRuntime {
private static final long MAX_FIELD_PAYLOAD_BYTES = 64L * 1024L * 1024L;
public enum State { DISABLED, WAITING_FOR_FIELDS, READY, ERROR }
public enum PlaybackState { PLAY, PAUSE }
public enum PlaybackSpeed { HALF(0.5f), NORMAL(1.0f), DOUBLE(2.0f); final float value; PlaybackSpeed(float value) { this.value = value; } }
public enum TargetFps { AUTO(0), FPS_30(30), FPS_60(60); final int value; TargetFps(int value) { this.value = value; } }
public enum FieldSubmitResult {
REJECTED_SOURCE_MODE(-1), REJECTED_ADAPTER(-2), REJECTED_INCOMPATIBLE(-3), REJECTED_NON_MONOTONIC(-4), REJECTED_CAPACITY(-5),
ACCEPTED_PENDING_INITIAL_WINDOW(1), INITIAL_WINDOW_READY(2), ACCEPTED_NEXT_SNAPSHOT(3), DUPLICATE(4), UNKNOWN(Integer.MIN_VALUE);
final int code; FieldSubmitResult(int code) { this.code = code; }
public int getCode() { return code; }
static FieldSubmitResult fromCode(int code) { for (FieldSubmitResult value : values()) if (value.code == code) return value; return UNKNOWN; }
public boolean accepted() { return code > 0; }
}
public enum CommandResult { ACCEPTED, REJECTED }
public enum EncodedFieldSubmitResult {
ACCEPTED_INSERTED(1), ACCEPTED_REPLACED_REVISION(2), DUPLICATE(3), REJECTED_DECODE(-1), REJECTED_CONTRACT(-2), REJECTED_INVALID_FIELD_KEY(-3), REJECTED_IDENTITY_CONFLICT(-4), REJECTED_STALE_REVISION(-5), REJECTED_INCOMPATIBLE(-6), REJECTED_CAPACITY(-7);
final int code; EncodedFieldSubmitResult(int code) { this.code = code; }
static EncodedFieldSubmitResult fromCode(int code) { for (EncodedFieldSubmitResult v : values()) if (v.code == code) return v; throw new IllegalArgumentException("unknown encoded submit code"); }
}
public static final class Config {
public final String sourceId;
public final String weatherColorLayerId;
public final String particleLayerId;
public Config(@NonNull String sourceId, @NonNull String weatherColorLayerId, @NonNull String particleLayerId) {
if (sourceId.isEmpty() || weatherColorLayerId.isEmpty() || particleLayerId.isEmpty()) throw new IllegalArgumentException("weather runtime IDs must be non-empty");
this.sourceId = sourceId; this.weatherColorLayerId = weatherColorLayerId; this.particleLayerId = particleLayerId;
}
}
public static final class ColorStop {
public final float speedMps, red, green, blue, alpha;
public ColorStop(float speedMps, float red, float green, float blue, float alpha) {
this.speedMps = speedMps; this.red = red; this.green = green; this.blue = blue; this.alpha = alpha;
}
}
private final MapLibreMap map;
private final Config config;
private final int weatherColorStopCapacity;
private State state = State.WAITING_FOR_FIELDS;
private boolean weatherColorVisible = true;
private boolean particleVisible = true;
private PlaybackState playbackState = PlaybackState.PAUSE;
private TargetFps targetFps = TargetFps.AUTO;
// V4 owns its window in Core, so it never transitions the legacy V3 state
// to READY. This only owns Android's next-frame wake, not a second timeline.
// This is a Host-command bookkeeping hint for scheduling only. Core still
// decides whether a resolved V4 window may actually advance simulation.
@Nullable private String v4ParticleBindingFieldKey;
private boolean v4ForecastPlaying;
private final Set<String> submittedEncodedFieldKeys = new HashSet<>();
private boolean disposed;
private Handler cadenceHandler;
private final Runnable cadenceWake = new Runnable() {
@Override public void run() { runCadenceWake(); }
};
public WeatherRuntime(@NonNull MapLibreMap map, @NonNull Config config) {
this.map = map; this.config = config; this.weatherColorStopCapacity = map.getWeatherColorStopCapacity();
}
public State getState() { return state; }
public Config getConfig() { return config; }
public boolean isDisposed() { return disposed; }
/** Ends this map/style-scoped facade. It is safe to call repeatedly. */
public void dispose() {
if (disposed) return;
cancelCadenceWake();
// If Style has already dropped the source, there is no Source-owned V4 store
// left to reset; otherwise disposal must not claim success before Core reset.
if (map.hasStyleSource(config.sourceId)) {
map.setStyleLayerVisibility(config.weatherColorLayerId, false);
map.setStyleLayerVisibility(config.particleLayerId, false);
if (!map.resetRasterArraySourceWeatherRuntime(config.sourceId)) {
throw new IllegalStateException("WeatherRuntime terminal Core reset failed");
}
}
submittedEncodedFieldKeys.clear();
v4ParticleBindingFieldKey = null;
v4ForecastPlaying = false;
playbackState = PlaybackState.PAUSE;
state = State.DISABLED;
disposed = true;
}
/** Core-owned capacity; Host never sees the shader's UBO layout. */
public int getWeatherColorStopCapacity() { return weatherColorStopCapacity; }
public FieldSubmitResult submitWindField(@NonNull String metadataJson, @NonNull byte[] payloadBytes) {
requireAttached();
requireEnabled();
if (metadataJson.isEmpty() || payloadBytes.length == 0) return FieldSubmitResult.REJECTED_ADAPTER;
FieldSubmitResult result = FieldSubmitResult.fromCode(map.submitRasterArraySourceHostField(config.sourceId, metadataJson, payloadBytes));
state = stateAfterSubmit(state, result);
if (state == State.READY && result.accepted()) scheduleCadenceWake();
return result;
}
/** Isolated V4 ingress: it deliberately does not mutate V3 state/cadence/timeline. */
public EncodedFieldSubmitResult submitEncodedField(@NonNull String fieldKey, @NonNull String fieldId, long generation, long dataVersion, @NonNull String contractJson, @NonNull byte[] qbin) {
requireAttached(); requireEnabled();
if (!WeatherRuntimeV4Guards.hasValidEncodedIdUtf8Length(fieldKey) || !WeatherRuntimeV4Guards.hasValidEncodedIdUtf8Length(fieldId) || !WeatherRuntimeV4Guards.hasValidEncodedContractUtf8Length(contractJson) || !hasValidEncodedContractProfileId(contractJson) || generation <= 0 || dataVersion < 0 || !WeatherRuntimeV4Guards.hasValidEncodedQbin(qbin)) throw new IllegalArgumentException("invalid encoded field argument");
int code = map.submitRasterArraySourceEncodedField(config.sourceId, fieldKey, fieldId, generation, dataVersion, contractJson, qbin);
if (code == 0) throw new IllegalStateException("SOURCE_MISSING");
EncodedFieldSubmitResult result = EncodedFieldSubmitResult.fromCode(code);
if (result.code > 0) { submittedEncodedFieldKeys.add(fieldKey); cancelCadenceWake(); scheduleCadenceWake(); }
return result;
}
private static boolean hasValidEncodedContractProfileId(String contractJson) {
if (contractJson == null) return false;
try {
return WeatherRuntimeV4Guards.hasValidEncodedIdUtf8Length(new JSONObject(contractJson).optString("profileID", ""));
} catch (JSONException exception) {
return false;
}
}
public CommandResult clearEncodedField(@NonNull String fieldKey) { requireAttached(); if (!WeatherRuntimeV4Guards.hasValidEncodedIdUtf8Length(fieldKey)) return CommandResult.REJECTED; CommandResult result = encodedCommand(map.clearRasterArraySourceEncodedField(config.sourceId, fieldKey)); if (result == CommandResult.ACCEPTED) { submittedEncodedFieldKeys.remove(fieldKey); cancelCadenceWake(); scheduleCadenceWake(); } return result; }
public CommandResult clearAllEncodedFields() { requireAttached(); CommandResult result = encodedCommand(map.clearAllRasterArraySourceEncodedFields(config.sourceId)); if (result == CommandResult.ACCEPTED) { submittedEncodedFieldKeys.clear(); cancelCadenceWake(); scheduleCadenceWake(); } return result; }
/** V4 binding intent; unit and all mode selectors reach Core unchanged. */
public CommandResult setEncodedColorBinding(@NonNull String fieldKey, int semanticID, int groupID, int valueClass, int expectedUnitID) {
requireAttached(); return !WeatherRuntimeV4Guards.hasValidEncodedIdUtf8Length(fieldKey) ? CommandResult.REJECTED : encodedCommand(map.setRasterArraySourceEncodedColorBinding(config.sourceId, fieldKey, semanticID, groupID, valueClass, expectedUnitID));
}
/** ABI reserves all selector slots: inactive slots must be exactly (0, 0). */
public CommandResult setEncodedParticleBinding(@NonNull String fieldKey, int mode, int uSemanticID, int uGroupID, int vSemanticID, int vGroupID, int directionSemanticID, int directionGroupID, int expectedUnitID) {
requireAttached(); if (!WeatherRuntimeV4Guards.hasValidEncodedIdUtf8Length(fieldKey)) return CommandResult.REJECTED; CommandResult result = encodedCommand(map.setRasterArraySourceEncodedParticleBinding(config.sourceId, fieldKey, mode, uSemanticID, uGroupID, vSemanticID, vGroupID, directionSemanticID, directionGroupID, expectedUnitID)); if (result == CommandResult.ACCEPTED) { v4ParticleBindingFieldKey = fieldKey; cancelCadenceWake(); scheduleCadenceWake(); } return result;
}
public CommandResult setEncodedForecastTime(long timestamp) { requireAttached(); return timestamp < 0 ? CommandResult.REJECTED : encodedCommand(map.setRasterArraySourceEncodedForecastTime(config.sourceId, timestamp)); }
/** State and speed intentionally remain independent V4 Core commands. */
public CommandResult setEncodedForecastPlaybackState(boolean playing) { requireAttached(); CommandResult result = encodedCommand(map.setRasterArraySourceEncodedForecastPlaybackState(config.sourceId, playing)); if (result == CommandResult.ACCEPTED) { v4ForecastPlaying = playing; cancelCadenceWake(); scheduleCadenceWake(); } return result; }
public CommandResult setEncodedForecastPlaybackSpeed(float speed) { requireAttached(); return !Float.isFinite(speed) || speed <= 0 ? CommandResult.REJECTED : encodedCommand(map.setRasterArraySourceEncodedForecastPlaybackSpeed(config.sourceId, speed)); }
/** v2 provider-neutral ingress; missing scalar/vector is expressed only by its explicit presence flag. */
public FieldSubmitResult submitField(@NonNull String metadataJson, @Nullable byte[] scalarBytes, @Nullable byte[] vectorBytes, @NonNull byte[] validityBytes) {
requireAttached(); requireEnabled();
if (metadataJson.isEmpty() || validityBytes.length == 0 || (scalarBytes == null && vectorBytes == null)) return FieldSubmitResult.REJECTED_ADAPTER;
final int scalarLength = scalarBytes == null ? 0 : scalarBytes.length;
final int vectorLength = vectorBytes == null ? 0 : vectorBytes.length;
if ((scalarBytes != null && scalarLength == 0) || (vectorBytes != null && vectorLength == 0)) return FieldSubmitResult.REJECTED_ADAPTER;
final long total = (long) scalarLength + vectorLength + validityBytes.length;
// Do not rely on Kotlin's file-reference guard: Java callers can bypass it.
if (total > MAX_FIELD_PAYLOAD_BYTES || total > Integer.MAX_VALUE) return FieldSubmitResult.REJECTED_ADAPTER;
final byte[] packed = new byte[(int) total]; int offset = 0;
if (scalarBytes != null) { System.arraycopy(scalarBytes, 0, packed, offset, scalarLength); offset += scalarLength; }
if (vectorBytes != null) { System.arraycopy(vectorBytes, 0, packed, offset, vectorLength); offset += vectorLength; }
System.arraycopy(validityBytes, 0, packed, offset, validityBytes.length);
FieldSubmitResult result = FieldSubmitResult.fromCode(map.submitRasterArraySourceHostFieldV2(config.sourceId, metadataJson,
packed, scalarBytes != null, scalarLength, vectorBytes != null, vectorLength, validityBytes.length));
state = stateAfterSubmit(state, result); if (state == State.READY && result.accepted()) scheduleCadenceWake(); return result;
}
public CommandResult clearFields() { if (disposed || state == State.DISABLED) return CommandResult.REJECTED; boolean ok = map.clearRasterArraySourceHostFields(config.sourceId); state = stateAfterClear(state, ok); if (ok) cancelCadenceWake(); return result(ok); }
/** @deprecated v1 compatibility alias. */
public CommandResult clearWindFields() { return clearFields(); }
public CommandResult setWeatherEnabled(boolean enabled) {
if (disposed) return CommandResult.REJECTED;
if (!enabled) {
if (state == State.DISABLED) return CommandResult.ACCEPTED;
map.controlRasterArraySourceTimeline(config.sourceId, 1, 0f);
boolean color = map.setStyleLayerVisibility(config.weatherColorLayerId, false);
boolean particle = map.setStyleLayerVisibility(config.particleLayerId, false);
boolean clear = map.resetRasterArraySourceWeatherRuntime(config.sourceId);
state = stateAfterDisable(color && particle && clear);
if (clear) {
submittedEncodedFieldKeys.clear();
v4ParticleBindingFieldKey = null;
v4ForecastPlaying = false;
playbackState = PlaybackState.PAUSE;
}
if (state != State.READY) cancelCadenceWake();
return result(color && particle && clear);
}
if (state == State.ERROR) return CommandResult.REJECTED;
if (state != State.DISABLED) return CommandResult.ACCEPTED;
boolean color = map.setStyleLayerVisibility(config.weatherColorLayerId, weatherColorVisible);
boolean particle = map.setStyleLayerVisibility(config.particleLayerId, particleVisible);
state = stateAfterEnable(state, color && particle);
cancelCadenceWake();
scheduleCadenceWake();
return result(color && particle);
}
public CommandResult setWeatherColorVisible(boolean visible) { if (disposed) return CommandResult.REJECTED; weatherColorVisible = visible; CommandResult result = result(map.setStyleLayerVisibility(config.weatherColorLayerId, visible && state != State.DISABLED)); cancelCadenceWake(); scheduleCadenceWake(); return result; }
public CommandResult setParticleVisible(boolean visible) { if (disposed) return CommandResult.REJECTED; particleVisible = visible; CommandResult result = result(map.setStyleLayerVisibility(config.particleLayerId, visible && state != State.DISABLED)); cancelCadenceWake(); scheduleCadenceWake(); return result; }
public CommandResult setForecastTime(long timestampMs) { requireEnabled(); return result(timestampMs >= 0 && map.seekRasterArraySourceTimelineToTimestamp(config.sourceId, timestampMs)); }
public CommandResult setPlaybackState(@NonNull PlaybackState playback) { requireEnabled(); CommandResult result = result(map.controlRasterArraySourceTimeline(config.sourceId, playback == PlaybackState.PLAY ? 0 : 1, 0f)); if (result == CommandResult.ACCEPTED) { playbackState = playback; cancelCadenceWake(); scheduleCadenceWake(); } return result; }
public CommandResult setPlaybackSpeed(@NonNull PlaybackSpeed speed) { requireEnabled(); return result(map.controlRasterArraySourceTimeline(config.sourceId, 2, speed.value)); }
public CommandResult setWeatherColorOpacity(float opacity) { return disposed ? CommandResult.REJECTED : result(Float.isFinite(opacity) && map.setWeatherColorOpacity(config.weatherColorLayerId, opacity)); }
public CommandResult setParticleCount(int count) { return setParticleFloat("raster-particle-particle-count", count); }
public CommandResult setParticleLineWidth(float width) { return setParticleFloat("raster-particle-line-width", width); }
public CommandResult setParticleLength(float length) { return setParticleFloat("raster-particle-particle-length", length); }
public CommandResult setParticleOpacity(float opacity) { return setParticleFloat("raster-particle-particle-opacity", opacity); }
public CommandResult setTrailOpacity(float opacity) { return setParticleFloat("raster-particle-trail-opacity", opacity); }
public CommandResult setTrailFade(float fade) { return setParticleFloat("raster-particle-trail-fade", fade); }
public CommandResult setParticleAdvectionScale(float scale) { return disposed || !Float.isFinite(scale) || scale <= 0f ? CommandResult.REJECTED : setParticleFloat("raster-particle-advection-scale", scale); }
public CommandResult setFieldValueSource(@NonNull FieldValueSource source) {
if (disposed) return CommandResult.REJECTED;
return result(map.setStyleLayerFloatProperty(config.weatherColorLayerId, "weather-color-field-value-source", source.code));
}
public CommandResult setParticleSpeedColorEnabled(boolean enabled) { return setParticleFloat("raster-particle-speed-color-enabled", enabled ? 1f : 0f); }
/** Sets only the particle RGBA; trail color and WeatherColor ramp are unchanged. */
public CommandResult setParticleColor(float red, float green, float blue, float alpha) {
if (disposed) return CommandResult.REJECTED;
return result(map.setRasterParticleColor(config.particleLayerId, new float[]{red, green, blue, alpha}));
}
/** Sets only the trail RGBA; particle color and WeatherColor ramp are unchanged. */
public CommandResult setTrailColor(float red, float green, float blue, float alpha) {
if (disposed) return CommandResult.REJECTED;
return result(map.setRasterParticleTrailColor(config.particleLayerId, new float[]{red, green, blue, alpha}));
}
/** Compatibility helper for existing callers; new Host code should use the independent setters. */
public CommandResult setParticleColors(float red, float green, float blue, float alpha, float trailRed, float trailGreen, float trailBlue, float trailAlpha) {
if (disposed) return CommandResult.REJECTED;
return result(map.setRasterParticleColors(config.particleLayerId, new float[]{red, green, blue, alpha}, new float[]{trailRed, trailGreen, trailBlue, trailAlpha}));
}
public CommandResult setTargetFps(@NonNull TargetFps target) {
if (disposed) return CommandResult.REJECTED;
if (!map.setRasterArraySourceTargetFps(config.sourceId, target.value)) return CommandResult.REJECTED;
targetFps = target;
cancelCadenceWake();
scheduleCadenceWake();
return CommandResult.ACCEPTED;
}
/** Native-authoritative playback mode for a low-frequency Host snapshot. */
public PlaybackState getPlaybackState() { return playbackState; }
public TargetFps getTargetFps() { return targetFps; }
public CommandResult setWeatherColorStops(@NonNull List<ColorStop> stops) {
if (disposed) return CommandResult.REJECTED;
if (weatherColorStopCapacity < 2 || stops.size() < 2 || stops.size() > weatherColorStopCapacity) return CommandResult.REJECTED;
float[] speeds = new float[stops.size()]; float[] colors = new float[stops.size() * 4];
for (int i = 0; i < stops.size(); i++) { ColorStop stop = stops.get(i); speeds[i] = stop.speedMps; colors[i * 4] = stop.red; colors[i * 4 + 1] = stop.green; colors[i * 4 + 2] = stop.blue; colors[i * 4 + 3] = stop.alpha; }
return result(map.setWeatherColorStops(config.weatherColorLayerId, speeds, colors));
}
private static CommandResult result(boolean accepted) { return accepted ? CommandResult.ACCEPTED : CommandResult.REJECTED; }
/** JNI 0 is private source/type lifecycle sentinel, never a Core V4 result. */
private static CommandResult encodedCommand(int code) { if (code == 0) throw new IllegalStateException("SOURCE_MISSING"); return code > 0 ? CommandResult.ACCEPTED : CommandResult.REJECTED; }
public enum FieldValueSource { SCALAR(0f), VECTOR_MAGNITUDE(1f); final float code; FieldValueSource(float code) { this.code = code; } }
private boolean shouldWakeWeather() {
if (disposed || state == State.DISABLED) return false;
if (v4ForecastPlaying && !submittedEncodedFieldKeys.isEmpty()) return true;
if (targetFps == TargetFps.AUTO) return false;
final boolean v4ParticleFieldReady = v4ParticleBindingFieldKey != null && submittedEncodedFieldKeys.contains(v4ParticleBindingFieldKey);
return (v4ParticleFieldReady && particleVisible) ||
(state == State.READY && (particleVisible || (weatherColorVisible && playbackState == PlaybackState.PLAY)));
}
private void scheduleCadenceWake() {
if (!shouldWakeWeather()) return;
try {
if (cadenceHandler == null) cadenceHandler = new Handler(Looper.getMainLooper());
cadenceHandler.removeCallbacks(cadenceWake);
cadenceHandler.postDelayed(cadenceWake, targetFps == TargetFps.FPS_60 ? 17L : 34L);
} catch (RuntimeException ignored) {
// Local JVM tests may not provide an Android Looper; Core cadence remains authoritative.
}
}
private void cancelCadenceWake() { if (cadenceHandler != null) cadenceHandler.removeCallbacks(cadenceWake); }
void runCadenceWake() { if (!shouldWakeWeather()) return; map.triggerRepaint(); scheduleCadenceWake(); }
private CommandResult setParticleFloat(String property, float value) { return disposed ? CommandResult.REJECTED : result(Float.isFinite(value) && map.setStyleLayerFloatProperty(config.particleLayerId, property, value)); }
static State stateAfterSubmit(State current, FieldSubmitResult submit) {
if (submit == FieldSubmitResult.ACCEPTED_PENDING_INITIAL_WINDOW) return State.WAITING_FOR_FIELDS;
if (submit == FieldSubmitResult.INITIAL_WINDOW_READY || submit == FieldSubmitResult.ACCEPTED_NEXT_SNAPSHOT) return State.READY;
if (submit == FieldSubmitResult.DUPLICATE) return current;
return submit == FieldSubmitResult.REJECTED_SOURCE_MODE || submit == FieldSubmitResult.UNKNOWN ? State.ERROR : current;
}
static State stateAfterClear(State current, boolean accepted) { return accepted ? State.WAITING_FOR_FIELDS : current; }
static State stateAfterDisable(boolean accepted) { return accepted ? State.DISABLED : State.ERROR; }
static State stateAfterEnable(State current, boolean accepted) {
if (!accepted) return State.ERROR;
if (current == State.DISABLED) return State.WAITING_FOR_FIELDS;
return current;
}
private void requireAttached() { if (disposed) throw new IllegalStateException("WeatherRuntime is disposed"); }
private void requireEnabled() { requireAttached(); if (state == State.DISABLED) throw new IllegalStateException("WeatherRuntime is disabled; enable and submit fields first"); }
}

View File

@@ -0,0 +1,27 @@
package org.maplibre.android.maps;
import java.nio.charset.StandardCharsets;
/** Small transport guards shared by Java facade checks; they do not define QBIN decoding. */
final class WeatherRuntimeV4Guards {
static final int MAX_ENCODED_ID_UTF8_BYTES = 64;
static final int MAX_CONTRACT_UTF8_BYTES = 256 * 1024;
static final int MAX_ENCODED_QBIN_BYTES = 64 * 1024 * 1024;
private WeatherRuntimeV4Guards() {}
static boolean hasValidEncodedIdUtf8Length(String value) {
if (value == null) return false;
final int length = value.getBytes(StandardCharsets.UTF_8).length;
return length >= 1 && length <= MAX_ENCODED_ID_UTF8_BYTES;
}
static boolean hasValidEncodedQbin(byte[] qbin) {
return qbin != null && qbin.length >= 1 && qbin.length <= MAX_ENCODED_QBIN_BYTES;
}
static boolean hasValidEncodedContractUtf8Length(String contractJson) {
return contractJson != null && !contractJson.isEmpty()
&& contractJson.getBytes(StandardCharsets.UTF_8).length <= MAX_CONTRACT_UTF8_BYTES;
}
}

View File

@@ -1,6 +1,7 @@
package org.maplibre.android.maps.renderer.surfaceview;
import android.content.Context;
import android.util.Log;
import android.view.Surface;
import android.view.View;
@@ -28,11 +29,13 @@ public class SurfaceViewMapRenderer extends MapRenderer {
surfaceView.setDetachedListener(new MapLibreSurfaceView.OnSurfaceViewDetachedListener() {
@Override
public void onSurfaceViewDetached() {
Log.i("Mbgl-SurfaceRenderer", "SurfaceView detached; nativeReset entered");
// because the GL thread is destroyed when the view is detached from window,
// we need to ensure releasing the native renderer as well.
// This avoids releasing it only when the view is being recreated, which is already on a new GL thread,
// and leads to JNI crashes like https://github.com/mapbox/mapbox-gl-native/issues/14618
nativeReset();
Log.i("Mbgl-SurfaceRenderer", "nativeReset returned");
}
});
}

View File

@@ -0,0 +1,52 @@
package org.maplibre.android.maps;
import org.junit.Test;
import org.mockito.Mockito;
import java.util.Collections;
import java.util.concurrent.atomic.AtomicInteger;
import static org.junit.Assert.assertEquals;
/** Exercises the concrete MapLibreMap.onDestroy() dispatch path used by Android integrations. */
public class MapLibreMapDestroyListenerTest {
private MapLibreMap newMap() {
MapLibreMap map = new MapLibreMap(
null,
null,
null,
null,
null,
Mockito.mock(CameraChangeDispatcher.class),
Collections.emptyList());
map.injectLocationComponent(Mockito.mock(org.maplibre.android.location.LocationComponent.class));
return map;
}
@Test
public void onDestroyNotifiesRegisteredListenerOnceAndLateRegistrationImmediately() {
MapLibreMap map = newMap();
AtomicInteger notifications = new AtomicInteger();
map.addOnDestroyListener(notifications::incrementAndGet);
map.onDestroy();
map.onDestroy();
assertEquals(1, notifications.get());
map.addOnDestroyListener(notifications::incrementAndGet);
assertEquals(2, notifications.get());
}
@Test
public void removedListenerIsNotCalledByOnDestroy() {
MapLibreMap map = newMap();
AtomicInteger notifications = new AtomicInteger();
MapLibreMap.OnDestroyListenerRegistration registration =
map.addOnDestroyListener(notifications::incrementAndGet);
registration.remove();
map.onDestroy();
assertEquals(0, notifications.get());
}
}

View File

@@ -0,0 +1,165 @@
package org.maplibre.android.maps;
import org.junit.Test;
import org.mockito.Mockito;
import org.mockito.ArgumentCaptor;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
/** Unit coverage for facade-only state transitions; no renderer frame callback is involved. */
public class WeatherRuntimeStateTest {
@Test public void enableIsIdempotentAndDisabledEnableWaitsForFields() {
assertEquals(WeatherRuntime.State.READY, WeatherRuntime.stateAfterEnable(WeatherRuntime.State.READY, true));
assertEquals(WeatherRuntime.State.WAITING_FOR_FIELDS, WeatherRuntime.stateAfterEnable(WeatherRuntime.State.DISABLED, true));
}
@Test public void submitRecoversErrorAndReachesReadyOnlyWithWindow() {
assertEquals(WeatherRuntime.State.WAITING_FOR_FIELDS, WeatherRuntime.stateAfterSubmit(WeatherRuntime.State.ERROR, WeatherRuntime.FieldSubmitResult.ACCEPTED_PENDING_INITIAL_WINDOW));
assertEquals(WeatherRuntime.State.READY, WeatherRuntime.stateAfterSubmit(WeatherRuntime.State.WAITING_FOR_FIELDS, WeatherRuntime.FieldSubmitResult.INITIAL_WINDOW_READY));
assertEquals(WeatherRuntime.State.READY, WeatherRuntime.stateAfterSubmit(WeatherRuntime.State.READY, WeatherRuntime.FieldSubmitResult.DUPLICATE));
}
@Test public void ordinarySubmitRejectionsPreserveRuntimeState() {
WeatherRuntime.FieldSubmitResult[] rejected = {
WeatherRuntime.FieldSubmitResult.REJECTED_ADAPTER,
WeatherRuntime.FieldSubmitResult.REJECTED_INCOMPATIBLE,
WeatherRuntime.FieldSubmitResult.REJECTED_NON_MONOTONIC,
WeatherRuntime.FieldSubmitResult.REJECTED_CAPACITY};
for (WeatherRuntime.FieldSubmitResult result : rejected) {
assertEquals(WeatherRuntime.State.READY, WeatherRuntime.stateAfterSubmit(WeatherRuntime.State.READY, result));
assertEquals(WeatherRuntime.State.WAITING_FOR_FIELDS, WeatherRuntime.stateAfterSubmit(WeatherRuntime.State.WAITING_FOR_FIELDS, result));
}
}
@Test public void onlyFacadeContractFailuresEnterError() {
assertEquals(WeatherRuntime.State.ERROR, WeatherRuntime.stateAfterSubmit(WeatherRuntime.State.READY, WeatherRuntime.FieldSubmitResult.REJECTED_SOURCE_MODE));
assertEquals(WeatherRuntime.State.ERROR, WeatherRuntime.stateAfterSubmit(WeatherRuntime.State.READY, WeatherRuntime.FieldSubmitResult.UNKNOWN));
assertEquals(WeatherRuntime.State.ERROR, WeatherRuntime.stateAfterEnable(WeatherRuntime.State.ERROR, false));
assertEquals(WeatherRuntime.State.ERROR, WeatherRuntime.stateAfterEnable(WeatherRuntime.State.ERROR, true));
}
@Test public void clearAndDisableUseSuccessfulCoreResult() {
assertEquals(WeatherRuntime.State.WAITING_FOR_FIELDS, WeatherRuntime.stateAfterClear(WeatherRuntime.State.READY, true));
assertEquals(WeatherRuntime.State.DISABLED, WeatherRuntime.stateAfterDisable(true));
assertEquals(WeatherRuntime.State.ERROR, WeatherRuntime.stateAfterDisable(false));
}
@Test public void publicFlowKeepsReadyOnInvalidSubmission() {
MapLibreMap map = Mockito.mock(MapLibreMap.class);
Mockito.when(map.getWeatherColorStopCapacity()).thenReturn(8);
Mockito.when(map.submitRasterArraySourceHostField(Mockito.anyString(), Mockito.anyString(), Mockito.any(byte[].class)))
.thenReturn(MapLibreMap.HOST_FIELD_INITIAL_WINDOW_READY)
.thenReturn(MapLibreMap.HOST_FIELD_REJECTED_ADAPTER);
WeatherRuntime runtime = new WeatherRuntime(map, new WeatherRuntime.Config("source", "color", "particle"));
assertEquals(WeatherRuntime.FieldSubmitResult.INITIAL_WINDOW_READY, runtime.submitWindField("{}", new byte[] {1}));
assertEquals(WeatherRuntime.FieldSubmitResult.REJECTED_ADAPTER, runtime.submitWindField("{}", new byte[] {1}));
assertEquals(WeatherRuntime.State.READY, runtime.getState());
assertEquals(WeatherRuntime.FieldSubmitResult.REJECTED_ADAPTER, runtime.submitWindField("", new byte[] {1}));
assertEquals(WeatherRuntime.State.READY, runtime.getState());
}
@Test public void publicFlowRejectsEnableFromErrorWithoutVisibilityRecovery() {
MapLibreMap map = Mockito.mock(MapLibreMap.class);
Mockito.when(map.getWeatherColorStopCapacity()).thenReturn(8);
WeatherRuntime runtime = new WeatherRuntime(map, new WeatherRuntime.Config("source", "color", "particle"));
assertEquals(WeatherRuntime.FieldSubmitResult.UNKNOWN, runtime.submitWindField("{}", new byte[] {1}));
assertEquals(WeatherRuntime.State.ERROR, runtime.getState());
assertEquals(WeatherRuntime.CommandResult.REJECTED, runtime.setWeatherEnabled(true));
assertEquals(WeatherRuntime.State.ERROR, runtime.getState());
Mockito.verify(map, Mockito.never()).setStyleLayerVisibility(Mockito.anyString(), Mockito.anyBoolean());
}
@Test public void particleAndTrailColorsAreIndependentTypedCommands() {
MapLibreMap map = Mockito.mock(MapLibreMap.class);
Mockito.when(map.getWeatherColorStopCapacity()).thenReturn(8);
Mockito.when(map.setRasterParticleColor(Mockito.anyString(), Mockito.any(float[].class))).thenReturn(true);
Mockito.when(map.setRasterParticleTrailColor(Mockito.anyString(), Mockito.any(float[].class))).thenReturn(true);
WeatherRuntime runtime = new WeatherRuntime(map, new WeatherRuntime.Config("source", "color", "particle"));
assertEquals(WeatherRuntime.CommandResult.ACCEPTED, runtime.setParticleColor(0.1f, 0.2f, 0.3f, 1.0f));
assertEquals(WeatherRuntime.CommandResult.ACCEPTED, runtime.setTrailColor(0.9f, 0.8f, 0.7f, 1.0f));
ArgumentCaptor<float[]> particleColor = ArgumentCaptor.forClass(float[].class);
ArgumentCaptor<float[]> trailColor = ArgumentCaptor.forClass(float[].class);
Mockito.verify(map).setRasterParticleColor(Mockito.eq("particle"), particleColor.capture());
Mockito.verify(map).setRasterParticleTrailColor(Mockito.eq("particle"), trailColor.capture());
assertArrayEquals(new float[] {0.1f, 0.2f, 0.3f, 1.0f}, particleColor.getValue(), 0.0f);
assertArrayEquals(new float[] {0.9f, 0.8f, 0.7f, 1.0f}, trailColor.getValue(), 0.0f);
Mockito.verify(map, Mockito.never()).submitRasterArraySourceHostField(Mockito.anyString(), Mockito.anyString(), Mockito.any(byte[].class));
}
@Test public void invalidIndependentColorIsRejectedByNativeContract() {
MapLibreMap map = Mockito.mock(MapLibreMap.class);
Mockito.when(map.getWeatherColorStopCapacity()).thenReturn(8);
Mockito.when(map.setRasterParticleColor(Mockito.anyString(), Mockito.any(float[].class))).thenReturn(false);
Mockito.when(map.setRasterParticleTrailColor(Mockito.anyString(), Mockito.any(float[].class))).thenReturn(false);
WeatherRuntime runtime = new WeatherRuntime(map, new WeatherRuntime.Config("source", "color", "particle"));
assertEquals(WeatherRuntime.CommandResult.REJECTED, runtime.setParticleColor(Float.NaN, 0f, 0f, 1f));
assertEquals(WeatherRuntime.CommandResult.REJECTED, runtime.setTrailColor(0f, 0f, 0f, 2f));
}
@Test public void targetFpsIsTypedAndDoesNotSubmitFields() {
MapLibreMap map = Mockito.mock(MapLibreMap.class);
Mockito.when(map.getWeatherColorStopCapacity()).thenReturn(8);
Mockito.when(map.setRasterArraySourceTargetFps("source", 30)).thenReturn(true);
WeatherRuntime runtime = new WeatherRuntime(map, new WeatherRuntime.Config("source", "color", "particle"));
assertEquals(WeatherRuntime.CommandResult.ACCEPTED, runtime.setTargetFps(WeatherRuntime.TargetFps.FPS_30));
assertEquals(WeatherRuntime.TargetFps.FPS_30, runtime.getTargetFps());
Mockito.verify(map, Mockito.never()).submitRasterArraySourceHostField(Mockito.anyString(), Mockito.anyString(), Mockito.any(byte[].class));
}
@Test public void disableResetsV3AndV4ViaOneCoreCommandAndCanEnableAgain() {
MapLibreMap map = Mockito.mock(MapLibreMap.class);
Mockito.when(map.getWeatherColorStopCapacity()).thenReturn(8);
Mockito.when(map.setStyleLayerVisibility(Mockito.anyString(), Mockito.anyBoolean())).thenReturn(true);
Mockito.when(map.resetRasterArraySourceWeatherRuntime("source")).thenReturn(true);
WeatherRuntime runtime = new WeatherRuntime(map, new WeatherRuntime.Config("source", "color", "particle"));
assertEquals(WeatherRuntime.CommandResult.ACCEPTED, runtime.setWeatherEnabled(false));
assertEquals(WeatherRuntime.State.DISABLED, runtime.getState());
Mockito.verify(map).resetRasterArraySourceWeatherRuntime("source");
Mockito.verify(map, Mockito.never()).clearRasterArraySourceHostFields(Mockito.anyString());
assertEquals(WeatherRuntime.CommandResult.ACCEPTED, runtime.setWeatherEnabled(true));
assertEquals(WeatherRuntime.State.WAITING_FOR_FIELDS, runtime.getState());
}
@Test public void terminalDisposeResetsSourceAndFailureDoesNotClaimDisposed() {
MapLibreMap map = Mockito.mock(MapLibreMap.class);
Mockito.when(map.getWeatherColorStopCapacity()).thenReturn(8);
Mockito.when(map.hasStyleSource("source")).thenReturn(true);
Mockito.when(map.resetRasterArraySourceWeatherRuntime("source")).thenReturn(false).thenReturn(true);
WeatherRuntime runtime = new WeatherRuntime(map, new WeatherRuntime.Config("source", "color", "particle"));
try {
runtime.dispose();
org.junit.Assert.fail("Core reset failure must be reported");
} catch (IllegalStateException expected) {
assertEquals(false, runtime.isDisposed());
}
runtime.dispose();
assertEquals(true, runtime.isDisposed());
Mockito.verify(map, Mockito.times(2)).resetRasterArraySourceWeatherRuntime("source");
Mockito.clearInvocations(map);
runtime.dispose();
Mockito.verifyNoInteractions(map);
}
@Test public void disposeIsIdempotentAndRejectsFurtherControls() {
MapLibreMap map = Mockito.mock(MapLibreMap.class);
Mockito.when(map.getWeatherColorStopCapacity()).thenReturn(8);
WeatherRuntime runtime = new WeatherRuntime(map, new WeatherRuntime.Config("source", "color", "particle"));
runtime.dispose();
runtime.dispose();
assertEquals(true, runtime.isDisposed());
Mockito.clearInvocations(map);
assertEquals(WeatherRuntime.CommandResult.REJECTED, runtime.setTargetFps(WeatherRuntime.TargetFps.FPS_30));
assertEquals(WeatherRuntime.CommandResult.REJECTED, runtime.setWeatherColorVisible(false));
assertEquals(WeatherRuntime.CommandResult.REJECTED, runtime.setParticleVisible(false));
assertEquals(WeatherRuntime.CommandResult.REJECTED, runtime.setWeatherEnabled(false));
Mockito.verifyNoInteractions(map);
}
@Test public void disposedWakeCannotRepaintOrRescheduleOldMap() {
MapLibreMap map = Mockito.mock(MapLibreMap.class);
Mockito.when(map.getWeatherColorStopCapacity()).thenReturn(8);
WeatherRuntime oldRuntime = new WeatherRuntime(map, new WeatherRuntime.Config("source", "color", "particle"));
oldRuntime.dispose();
oldRuntime.runCadenceWake();
Mockito.verify(map, Mockito.never()).triggerRepaint();
WeatherRuntime replacement = new WeatherRuntime(map, new WeatherRuntime.Config("source", "color", "particle"));
assertEquals(false, replacement.isDisposed());
}
}

View File

@@ -0,0 +1,17 @@
package org.maplibre.android.maps;
/** JDK-only directed guard test; Android facade compilation remains a later Android validation. */
public final class WeatherRuntimeV4GuardsTest {
public static void main(String[] args) {
require(WeatherRuntimeV4Guards.hasValidEncodedIdUtf8Length(repeat("a", 64)));
require(!WeatherRuntimeV4Guards.hasValidEncodedIdUtf8Length(repeat("风", 33)));
require(WeatherRuntimeV4Guards.hasValidEncodedQbin(new byte[64 * 1024 * 1024]));
require(!WeatherRuntimeV4Guards.hasValidEncodedQbin(new byte[0]));
require(!WeatherRuntimeV4Guards.hasValidEncodedQbin(new byte[64 * 1024 * 1024 + 1]));
require(WeatherRuntimeV4Guards.hasValidEncodedContractUtf8Length("{}"));
require(!WeatherRuntimeV4Guards.hasValidEncodedContractUtf8Length("a".repeat(256 * 1024 + 1)));
}
private static String repeat(String value, int count) { return value.repeat(count); }
private static void require(boolean value) { if (!value) throw new AssertionError("V4 transport guard failed"); }
}

View File

@@ -16,6 +16,9 @@ fun obtainTestBuildType(): String {
}
}
val stripValidationNativeDebugSymbols =
providers.gradleProperty("maplibre.validation.stripNativeDebugSymbols").orNull == "true"
android {
ndkVersion = Versions.ndkVersion
@@ -51,7 +54,9 @@ android {
packaging {
jniLibs {
keepDebugSymbols += "**/*.so"
if (!stripValidationNativeDebugSymbols) {
keepDebugSymbols += "**/*.so"
}
}
}

View File

@@ -286,6 +286,19 @@
android:name="android.support.PARENT_ACTIVITY"
android:value=".activity.FeatureOverviewActivity" />
</activity>
<activity
android:name=".activity.particle.ParticleCoreLayerValidationActivity"
android:configChanges="orientation|keyboardHidden|screenSize"
android:description="@string/description_particle_core_layer_validation"
android:exported="true"
android:label="@string/activity_particle_core_layer_validation">
<meta-data
android:name="@string/category"
android:value="@string/category_custom_layer" />
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value=".activity.FeatureOverviewActivity" />
</activity>
<activity
android:name=".activity.annotation.PolygonActivity"
android:description="@string/description_polygon"

View File

@@ -0,0 +1,15 @@
{
"schema": "maplibre-particle-fixture/v1",
"fixtureId": "nodata",
"fieldType": "WIND",
"displayDomain": "GLOBAL",
"dataVersion": 1,
"timeIndex": 0,
"timestamp": null,
"bounds": {"west": -180.0, "south": -85.0, "east": 180.0, "north": 85.0},
"crs": "EPSG:4326",
"grid": {"width": 16, "height": 16, "registration": "node", "origin": "north-west", "rowOrder": "north-to-south", "columnOrder": "west-to-east"},
"vector": {"uPositive": "east", "vPositive": "north", "unit": "m/s", "layout": "interleaved-uv", "encoding": "float32-le", "noData": -9999.0, "file": "field.f32"},
"mask": {"mode": "none"},
"integrity": {"fieldByteLength": 2048, "fieldSha256": "5756a6ac403a435ff001ff7261160de718c4a2964434ba7b9be2bfe74f071ff0"}
}

View File

@@ -0,0 +1,15 @@
{
"schema": "maplibre-particle-fixture/v1",
"fixtureId": "uniform-east",
"fieldType": "WIND",
"displayDomain": "GLOBAL",
"dataVersion": 1,
"timeIndex": 0,
"timestamp": null,
"bounds": {"west": -180.0, "south": -85.0, "east": 180.0, "north": 85.0},
"crs": "EPSG:4326",
"grid": {"width": 16, "height": 16, "registration": "node", "origin": "north-west", "rowOrder": "north-to-south", "columnOrder": "west-to-east"},
"vector": {"uPositive": "east", "vPositive": "north", "unit": "m/s", "layout": "interleaved-uv", "encoding": "float32-le", "noData": -9999.0, "file": "field.f32"},
"mask": {"mode": "none"},
"integrity": {"fieldByteLength": 2048, "fieldSha256": "0da648fdcf61b5ea6383960583bdc74287c3fa758c6e5a7eb7d234f2d7c3283f"}
}

View File

@@ -0,0 +1,15 @@
{
"schema": "maplibre-particle-fixture/v1",
"fixtureId": "uniform-north",
"fieldType": "WIND",
"displayDomain": "GLOBAL",
"dataVersion": 1,
"timeIndex": 0,
"timestamp": null,
"bounds": {"west": -180.0, "south": -85.0, "east": 180.0, "north": 85.0},
"crs": "EPSG:4326",
"grid": {"width": 16, "height": 16, "registration": "node", "origin": "north-west", "rowOrder": "north-to-south", "columnOrder": "west-to-east"},
"vector": {"uPositive": "east", "vPositive": "north", "unit": "m/s", "layout": "interleaved-uv", "encoding": "float32-le", "noData": -9999.0, "file": "field.f32"},
"mask": {"mode": "none"},
"integrity": {"fieldByteLength": 2048, "fieldSha256": "20eb0da1812e616cee1740ee8e22a7bda377d6785cc6e7007fb26afa5ab255bc"}
}

View File

@@ -0,0 +1,15 @@
{
"schema": "maplibre-particle-fixture/v1",
"fixtureId": "vortex",
"fieldType": "WIND",
"displayDomain": "GLOBAL",
"dataVersion": 1,
"timeIndex": 0,
"timestamp": null,
"bounds": {"west": -180.0, "south": -85.0, "east": 180.0, "north": 85.0},
"crs": "EPSG:4326",
"grid": {"width": 16, "height": 16, "registration": "node", "origin": "north-west", "rowOrder": "north-to-south", "columnOrder": "west-to-east"},
"vector": {"uPositive": "east", "vPositive": "north", "unit": "m/s", "layout": "interleaved-uv", "encoding": "float32-le", "noData": -9999.0, "file": "field.f32"},
"mask": {"mode": "none"},
"integrity": {"fieldByteLength": 2048, "fieldSha256": "aa7fd26d5e4605dc363636b99e17c6bb495d51350a5a2cd8abe5bf0ada741c5a"}
}

View File

@@ -0,0 +1,4 @@
{
"schema": "maplibre-particle-field/v1", "fieldId": "production-wind-t00", "fieldType": "WIND", "displayDomain": "GLOBAL", "generation": 1, "validTime": 202609040000, "dataVersion": 1,
"bounds": {"west": -180.0, "south": -85.0, "east": 180.0, "north": 85.0}, "grid": {"width": 16, "height": 16}, "vector": {"layout": "interleaved-uv", "encoding": "float32-le", "noData": -9999.0}, "payload": {"byteLength": 2048}
}

View File

@@ -0,0 +1,4 @@
{
"schema": "maplibre-particle-field/v1", "fieldId": "production-wind-t01", "fieldType": "WIND", "displayDomain": "GLOBAL", "generation": 2, "validTime": 202609040100, "dataVersion": 2,
"bounds": {"west": -180.0, "south": -85.0, "east": 180.0, "north": 85.0}, "grid": {"width": 16, "height": 16}, "vector": {"layout": "interleaved-uv", "encoding": "float32-le", "noData": -9999.0}, "payload": {"byteLength": 2048}
}

View File

@@ -0,0 +1,4 @@
{
"schema": "maplibre-particle-field/v1", "fieldId": "production-wind-t02", "fieldType": "WIND", "displayDomain": "GLOBAL", "generation": 3, "validTime": 202609040200, "dataVersion": 3,
"bounds": {"west": -180.0, "south": -85.0, "east": 180.0, "north": 85.0}, "grid": {"width": 16, "height": 16}, "vector": {"layout": "interleaved-uv", "encoding": "float32-le", "noData": -9999.0}, "payload": {"byteLength": 2048}
}

View File

@@ -0,0 +1,4 @@
{
"schema": "maplibre-particle-field/v1", "fieldId": "production-wind-t03", "fieldType": "WIND", "displayDomain": "GLOBAL", "generation": 4, "validTime": 202609040300, "dataVersion": 4,
"bounds": {"west": -180.0, "south": -85.0, "east": 180.0, "north": 85.0}, "grid": {"width": 16, "height": 16}, "vector": {"layout": "interleaved-uv", "encoding": "float32-le", "noData": -9999.0}, "payload": {"byteLength": 2048}
}

View File

@@ -0,0 +1,4 @@
{
"schema": "maplibre-particle-field/v1", "fieldId": "", "fieldType": "WIND", "displayDomain": "GLOBAL", "generation": 0, "validTime": 0, "dataVersion": 99,
"bounds": {"west": -180.0, "south": -85.0, "east": 180.0, "north": 85.0}, "grid": {"width": 16, "height": 16}, "vector": {"layout": "interleaved-uv", "encoding": "float32-le", "noData": -9999.0}, "payload": {"byteLength": 2048}
}

View File

@@ -0,0 +1,124 @@
{
"source": {
"format": "weather-v2-particle-bin-v1",
"magic": "WVECBIN1",
"targetTime": "2026080700",
"width": 513,
"height": 769,
"bounds": {
"west": 112.5,
"south": 0.0,
"east": 157.5,
"north": 55.77657301866769
},
"dtype": "float32 little-endian u plane, v plane, speed plane; uint8 valid plane",
"arrayOrder": "row-major; WeatherV2 tile y increases southward, so rows are north-to-south",
"uVUnit": "m/s",
"validCount": 394497,
"invalidCount": 0,
"bytes": 5128989
},
"output": {
"width": 513,
"height": 769,
"bounds": {
"west": 112.5,
"south": 0.0,
"east": 157.5,
"north": 55.77657301866769
},
"bytes": 3155976,
"sha256": "363e2cd2e77501f6bceecbb4aaa68c4f9ef46998a60319ae5b7aba3548995283",
"validCount": 394497,
"invalidCount": 0,
"resampled": false
},
"statistics": {
"u": {
"min": -35.172367095947266,
"max": 22.501087188720703,
"mean": 0.7285217046737671
},
"v": {
"min": -25.472291946411133,
"max": 28.200902938842773,
"mean": 1.1300384998321533
},
"speed": {
"min": 0.0036890381015837193,
"max": 35.27834701538086,
"mean": 6.401810169219971
}
},
"sourceToCanonical": {
"lossless": true,
"maxAbsErrorU": 0.0,
"maxAbsErrorV": 0.0,
"samples": [
{
"row": 384,
"column": 256,
"sourceU": -10.921929359436035,
"sourceV": 4.515348434448242,
"canonicalU": -10.921929359436035,
"canonicalV": 4.515348434448242
},
{
"row": 1,
"column": 1,
"sourceU": -0.980097770690918,
"sourceV": 0.4984455108642578,
"canonicalU": -0.980097770690918,
"canonicalV": 0.4984455108642578
},
{
"row": 1,
"column": 511,
"sourceU": 1.2943105697631836,
"sourceV": -0.6254873275756836,
"canonicalU": 1.2943105697631836,
"canonicalV": -0.6254873275756836
},
{
"row": 767,
"column": 1,
"sourceU": -2.738156318664551,
"sourceV": 1.1959524154663086,
"canonicalU": -2.738156318664551,
"canonicalV": 1.1959524154663086
},
{
"row": 767,
"column": 511,
"sourceU": -5.0848894119262695,
"sourceV": -0.26341915130615234,
"canonicalU": -5.0848894119262695,
"canonicalV": -0.26341915130615234
},
{
"row": 0,
"column": 0,
"sourceU": -1.1939868927001953,
"sourceV": 1.7741403579711914,
"canonicalU": -1.1939868927001953,
"canonicalV": 1.7741403579711914
},
{
"row": 0,
"column": 1,
"sourceU": -1.051393985748291,
"sourceV": 1.4262237548828125,
"canonicalU": -1.051393985748291,
"canonicalV": 1.4262237548828125
},
{
"row": 0,
"column": 2,
"sourceU": -0.6949124336242676,
"sourceV": 0.9043483734130859,
"canonicalU": -0.6949124336242676,
"canonicalV": 0.9043483734130859
}
]
}
}

View File

@@ -0,0 +1,41 @@
{
"schema": "maplibre-particle-field/v1",
"fieldId": "ecmwf-hres-wind-2026080700",
"fieldType": "WIND",
"displayDomain": "GLOBAL",
"generation": 1786060800000,
"validTime": 1786060800000,
"dataVersion": 1786060800000,
"bounds": {
"west": 112.5,
"south": 0.0,
"east": 157.5,
"north": 55.77657301866769
},
"grid": {
"width": 513,
"height": 769
},
"vector": {
"layout": "interleaved-uv",
"encoding": "float32-le",
"noData": -9999.0,
"unit": "m/s",
"origin": "north-west",
"rowOrder": "north-to-south",
"columnOrder": "west-to-east",
"uPositive": "east",
"vPositive": "north"
},
"payload": {
"byteLength": 3155976,
"sha256": "363e2cd2e77501f6bceecbb4aaa68c4f9ef46998a60319ae5b7aba3548995283"
},
"source": {
"kind": "WeatherV2 ECMWF HRES WVECBIN1",
"referenceTime": "2026080600",
"targetTime": "2026080700",
"sourceBytes": 5128989,
"resampled": false
}
}

View File

@@ -0,0 +1,124 @@
{
"source": {
"format": "weather-v2-particle-bin-v1",
"magic": "WVECBIN1",
"targetTime": "2026080706",
"width": 513,
"height": 769,
"bounds": {
"west": 112.5,
"south": 0.0,
"east": 157.5,
"north": 55.77657301866769
},
"dtype": "float32 little-endian u plane, v plane, speed plane; uint8 valid plane",
"arrayOrder": "row-major; WeatherV2 tile y increases southward, so rows are north-to-south",
"uVUnit": "m/s",
"validCount": 394497,
"invalidCount": 0,
"bytes": 5128989
},
"output": {
"width": 513,
"height": 769,
"bounds": {
"west": 112.5,
"south": 0.0,
"east": 157.5,
"north": 55.77657301866769
},
"bytes": 3155976,
"sha256": "ef7a8e4b1c47f652f8f7012de16a87c16e3b25556a8f4686629f80514426c451",
"validCount": 394497,
"invalidCount": 0,
"resampled": false
},
"statistics": {
"u": {
"min": -37.18255615234375,
"max": 24.125526428222656,
"mean": 0.9443698525428772
},
"v": {
"min": -28.227636337280273,
"max": 25.05806541442871,
"mean": 1.0168105363845825
},
"speed": {
"min": 0.010480343364179134,
"max": 37.76322555541992,
"mean": 6.7594804763793945
}
},
"sourceToCanonical": {
"lossless": true,
"maxAbsErrorU": 0.0,
"maxAbsErrorV": 0.0,
"samples": [
{
"row": 384,
"column": 256,
"sourceU": -10.2057466506958,
"sourceV": 4.789729118347168,
"canonicalU": -10.2057466506958,
"canonicalV": 4.789729118347168
},
{
"row": 1,
"column": 1,
"sourceU": -0.45672082901000977,
"sourceV": 3.2181167602539062,
"canonicalU": -0.45672082901000977,
"canonicalV": 3.2181167602539062
},
{
"row": 1,
"column": 511,
"sourceU": 2.235017776489258,
"sourceV": -0.7363739013671875,
"canonicalU": 2.235017776489258,
"canonicalV": -0.7363739013671875
},
{
"row": 767,
"column": 1,
"sourceU": -1.2302308082580566,
"sourceV": 1.5909242630004883,
"canonicalU": -1.2302308082580566,
"canonicalV": 1.5909242630004883
},
{
"row": 767,
"column": 511,
"sourceU": -5.034683704376221,
"sourceV": -0.15012121200561523,
"canonicalU": -5.034683704376221,
"canonicalV": -0.15012121200561523
},
{
"row": 0,
"column": 0,
"sourceU": -0.15583133697509766,
"sourceV": 3.617917060852051,
"canonicalU": -0.15583133697509766,
"canonicalV": 3.617917060852051
},
{
"row": 0,
"column": 1,
"sourceU": -0.45672082901000977,
"sourceV": 3.551283836364746,
"canonicalU": -0.45672082901000977,
"canonicalV": 3.551283836364746
},
{
"row": 0,
"column": 2,
"sourceU": -0.6974325180053711,
"sourceV": 3.2181167602539062,
"canonicalU": -0.6974325180053711,
"canonicalV": 3.2181167602539062
}
]
}
}

View File

@@ -0,0 +1,41 @@
{
"schema": "maplibre-particle-field/v1",
"fieldId": "ecmwf-hres-wind-2026080706",
"fieldType": "WIND",
"displayDomain": "GLOBAL",
"generation": 1786082400000,
"validTime": 1786082400000,
"dataVersion": 1786082400000,
"bounds": {
"west": 112.5,
"south": 0.0,
"east": 157.5,
"north": 55.77657301866769
},
"grid": {
"width": 513,
"height": 769
},
"vector": {
"layout": "interleaved-uv",
"encoding": "float32-le",
"noData": -9999.0,
"unit": "m/s",
"origin": "north-west",
"rowOrder": "north-to-south",
"columnOrder": "west-to-east",
"uPositive": "east",
"vPositive": "north"
},
"payload": {
"byteLength": 3155976,
"sha256": "ef7a8e4b1c47f652f8f7012de16a87c16e3b25556a8f4686629f80514426c451"
},
"source": {
"kind": "WeatherV2 ECMWF HRES WVECBIN1",
"referenceTime": "2026080600",
"targetTime": "2026080706",
"sourceBytes": 5128989,
"resampled": false
}
}

View File

@@ -0,0 +1,124 @@
{
"source": {
"format": "weather-v2-particle-bin-v1",
"magic": "WVECBIN1",
"targetTime": "2026080712",
"width": 513,
"height": 769,
"bounds": {
"west": 112.5,
"south": 0.0,
"east": 157.5,
"north": 55.77657301866769
},
"dtype": "float32 little-endian u plane, v plane, speed plane; uint8 valid plane",
"arrayOrder": "row-major; WeatherV2 tile y increases southward, so rows are north-to-south",
"uVUnit": "m/s",
"validCount": 394497,
"invalidCount": 0,
"bytes": 5128989
},
"output": {
"width": 513,
"height": 769,
"bounds": {
"west": 112.5,
"south": 0.0,
"east": 157.5,
"north": 55.77657301866769
},
"bytes": 3155976,
"sha256": "d86a6a51fd5216cf9368756962a5366fd32a627afd40cfde235ba7358c02c5a0",
"validCount": 394497,
"invalidCount": 0,
"resampled": false
},
"statistics": {
"u": {
"min": -37.39310836791992,
"max": 24.769317626953125,
"mean": 0.7698453664779663
},
"v": {
"min": -28.266582489013672,
"max": 27.809932708740234,
"mean": 1.1808899641036987
},
"speed": {
"min": 0.024363698437809944,
"max": 37.42909240722656,
"mean": 6.384326934814453
}
},
"sourceToCanonical": {
"lossless": true,
"maxAbsErrorU": 0.0,
"maxAbsErrorV": 0.0,
"samples": [
{
"row": 384,
"column": 256,
"sourceU": -10.180034637451172,
"sourceV": 4.663511276245117,
"canonicalU": -10.180034637451172,
"canonicalV": 4.663511276245117
},
{
"row": 1,
"column": 1,
"sourceU": -0.5417494773864746,
"sourceV": 0.767613410949707,
"canonicalU": -0.5417494773864746,
"canonicalV": 0.767613410949707
},
{
"row": 1,
"column": 511,
"sourceU": -0.5780353546142578,
"sourceV": 0.26222801208496094,
"canonicalU": -0.5780353546142578,
"canonicalV": 0.26222801208496094
},
{
"row": 767,
"column": 1,
"sourceU": -1.8420743942260742,
"sourceV": 0.3505821228027344,
"canonicalU": -1.8420743942260742,
"canonicalV": 0.3505821228027344
},
{
"row": 767,
"column": 511,
"sourceU": -4.581581115722656,
"sourceV": -1.1498479843139648,
"canonicalU": -4.581581115722656,
"canonicalV": -1.1498479843139648
},
{
"row": 0,
"column": 0,
"sourceU": -0.5939555168151855,
"sourceV": 1.1400480270385742,
"canonicalU": -0.5939555168151855,
"canonicalV": 1.1400480270385742
},
{
"row": 0,
"column": 1,
"sourceU": -0.48954296112060547,
"sourceV": 1.0159034729003906,
"canonicalU": -0.48954296112060547,
"canonicalV": 1.0159034729003906
},
{
"row": 0,
"column": 2,
"sourceU": -0.48954296112060547,
"sourceV": 0.767613410949707,
"canonicalU": -0.48954296112060547,
"canonicalV": 0.767613410949707
}
]
}
}

View File

@@ -0,0 +1,41 @@
{
"schema": "maplibre-particle-field/v1",
"fieldId": "ecmwf-hres-wind-2026080712",
"fieldType": "WIND",
"displayDomain": "GLOBAL",
"generation": 1786104000000,
"validTime": 1786104000000,
"dataVersion": 1786104000000,
"bounds": {
"west": 112.5,
"south": 0.0,
"east": 157.5,
"north": 55.77657301866769
},
"grid": {
"width": 513,
"height": 769
},
"vector": {
"layout": "interleaved-uv",
"encoding": "float32-le",
"noData": -9999.0,
"unit": "m/s",
"origin": "north-west",
"rowOrder": "north-to-south",
"columnOrder": "west-to-east",
"uPositive": "east",
"vPositive": "north"
},
"payload": {
"byteLength": 3155976,
"sha256": "d86a6a51fd5216cf9368756962a5366fd32a627afd40cfde235ba7358c02c5a0"
},
"source": {
"kind": "WeatherV2 ECMWF HRES WVECBIN1",
"referenceTime": "2026080600",
"targetTime": "2026080712",
"sourceBytes": 5128989,
"resampled": false
}
}

View File

@@ -0,0 +1,124 @@
{
"source": {
"format": "weather-v2-particle-bin-v1",
"magic": "WVECBIN1",
"targetTime": "2026080718",
"width": 513,
"height": 769,
"bounds": {
"west": 112.5,
"south": 0.0,
"east": 157.5,
"north": 55.77657301866769
},
"dtype": "float32 little-endian u plane, v plane, speed plane; uint8 valid plane",
"arrayOrder": "row-major; WeatherV2 tile y increases southward, so rows are north-to-south",
"uVUnit": "m/s",
"validCount": 394497,
"invalidCount": 0,
"bytes": 5128989
},
"output": {
"width": 513,
"height": 769,
"bounds": {
"west": 112.5,
"south": 0.0,
"east": 157.5,
"north": 55.77657301866769
},
"bytes": 3155976,
"sha256": "970785b0334a9d1511b8897874a81f681814a32b1e0b90e37cf64b77ca9fa39b",
"validCount": 394497,
"invalidCount": 0,
"resampled": false
},
"statistics": {
"u": {
"min": -31.956016540527344,
"max": 28.557456970214844,
"mean": 1.212062120437622
},
"v": {
"min": -26.13941192626953,
"max": 27.131576538085938,
"mean": 1.1284888982772827
},
"speed": {
"min": 0.0210099034011364,
"max": 33.67807388305664,
"mean": 6.403453826904297
}
},
"sourceToCanonical": {
"lossless": true,
"maxAbsErrorU": 0.0,
"maxAbsErrorV": 0.0,
"samples": [
{
"row": 384,
"column": 256,
"sourceU": -10.21538257598877,
"sourceV": 2.3842287063598633,
"canonicalU": -10.21538257598877,
"canonicalV": 2.3842287063598633
},
{
"row": 1,
"column": 1,
"sourceU": -0.18732452392578125,
"sourceV": 0.2720670700073242,
"canonicalU": -0.18732452392578125,
"canonicalV": 0.2720670700073242
},
{
"row": 1,
"column": 511,
"sourceU": 0.460113525390625,
"sourceV": 0.4729623794555664,
"canonicalU": 0.460113525390625,
"canonicalV": 0.4729623794555664
},
{
"row": 767,
"column": 1,
"sourceU": -1.4576325416564941,
"sourceV": -0.03371381759643555,
"canonicalU": -1.4576325416564941,
"canonicalV": -0.03371381759643555
},
{
"row": 767,
"column": 511,
"sourceU": -2.9916043281555176,
"sourceV": 0.8597030639648438,
"canonicalU": -2.9916043281555176,
"canonicalV": 0.8597030639648438
},
{
"row": 0,
"column": 0,
"sourceU": -1.020096778869629,
"sourceV": 0.9125776290893555,
"canonicalU": -1.020096778869629,
"canonicalV": 0.9125776290893555
},
{
"row": 0,
"column": 1,
"sourceU": -0.49961376190185547,
"sourceV": 0.6990737915039062,
"canonicalU": -0.49961376190185547,
"canonicalV": 0.6990737915039062
},
{
"row": 0,
"column": 2,
"sourceU": -0.08322811126708984,
"sourceV": 0.41440296173095703,
"canonicalU": -0.08322811126708984,
"canonicalV": 0.41440296173095703
}
]
}
}

View File

@@ -0,0 +1,41 @@
{
"schema": "maplibre-particle-field/v1",
"fieldId": "ecmwf-hres-wind-2026080718",
"fieldType": "WIND",
"displayDomain": "GLOBAL",
"generation": 1786125600000,
"validTime": 1786125600000,
"dataVersion": 1786125600000,
"bounds": {
"west": 112.5,
"south": 0.0,
"east": 157.5,
"north": 55.77657301866769
},
"grid": {
"width": 513,
"height": 769
},
"vector": {
"layout": "interleaved-uv",
"encoding": "float32-le",
"noData": -9999.0,
"unit": "m/s",
"origin": "north-west",
"rowOrder": "north-to-south",
"columnOrder": "west-to-east",
"uPositive": "east",
"vPositive": "north"
},
"payload": {
"byteLength": 3155976,
"sha256": "970785b0334a9d1511b8897874a81f681814a32b1e0b90e37cf64b77ca9fa39b"
},
"source": {
"kind": "WeatherV2 ECMWF HRES WVECBIN1",
"referenceTime": "2026080600",
"targetTime": "2026080718",
"sourceBytes": 5128989,
"resampled": false
}
}

View File

@@ -0,0 +1,14 @@
{
"schema": "maplibre-particle-test-dataset/v1",
"datasetId": "ecmwf-hres-wind-20260807",
"source": "WeatherV2 ECMWF HRES WVECBIN1",
"referenceTime": "2026080600",
"validTimes": [
"2026080700",
"2026080706",
"2026080712",
"2026080718"
],
"snapshotLayout": "<validTime>/metadata.json + <validTime>/field.f32",
"notes": "开发期离线 canonical 测试索引;不是 Weather Manifest,也不进入 ParticleFieldSnapshot。"
}

View File

@@ -0,0 +1 @@
{"contractSchemaVersion":1,"profileID":"wind","profileVersion":1,"allowAdditionalPlanes":true,"sharedValidityRequired":true,"timeRelation":"VALID_GTE_REFERENCE","requiredPlanes":[{"semanticID":1,"valueClass":"VELOCITY_COMPONENT","unitID":1,"groupID":1,"component":"U_EAST","requiredFlag":true},{"semanticID":2,"valueClass":"VELOCITY_COMPONENT","unitID":1,"groupID":1,"component":"V_NORTH","requiredFlag":true},{"semanticID":3,"valueClass":"VALIDITY","unitID":0,"groupID":0,"component":"NONE","requiredFlag":true}]}

View File

@@ -9,6 +9,7 @@ import android.os.Bundle
import android.view.Menu
import android.view.MenuItem
import android.view.View
import android.widget.TextView
import androidx.annotation.StringRes
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
@@ -21,6 +22,8 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.maplibre.android.RenderingEngine
import org.maplibre.android.BuildConfig as MapLibreBuildConfig
import org.maplibre.android.testapp.BuildConfig
import org.maplibre.android.testapp.R
import org.maplibre.android.testapp.adapter.FeatureAdapter
import org.maplibre.android.testapp.adapter.FeatureSectionAdapter
@@ -48,6 +51,14 @@ class FeatureOverviewActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_feature_overview)
findViewById<TextView>(R.id.apkVersionView).text = getString(
R.string.apk_version_format,
BuildConfig.VERSION_NAME,
BuildConfig.VERSION_CODE,
BuildConfig.FLAVOR,
BuildConfig.BUILD_TYPE,
MapLibreBuildConfig.GIT_REVISION.take(GIT_REVISION_LENGTH)
)
recyclerView = findViewById<View>(R.id.recyclerView) as RecyclerView
recyclerView.layoutManager = LinearLayoutManager(this)
recyclerView.addOnItemTouchListener(SimpleOnItemTouchListener())
@@ -266,5 +277,6 @@ class FeatureOverviewActivity : AppCompatActivity() {
companion object {
private const val KEY_STATE_FEATURES = "featureList"
private const val GIT_REVISION_LENGTH = 10
}
}

View File

@@ -0,0 +1,76 @@
package org.maplibre.android.testapp.activity.particle
import org.json.JSONArray
import org.json.JSONObject
/** Offline same-source tile fixture for retained target stencil regression. */
internal object Step097StencilFixture {
enum class Topology { RR, RCR, CRR }
const val FIRST_FILL = "step097-shared-fill-a"
const val SECOND_FILL = "step097-shared-fill-b"
const val THIRD_FILL = "step097-shared-fill-c"
private const val FOURTH_FILL = "step097-shared-fill-d"
private const val SOURCE = "step097-shared-polygon"
fun styleJson(): String {
val geometry = JSONObject()
.put("type", "Polygon")
.put("coordinates", JSONArray().put(
JSONArray().put(JSONArray().put(-45).put(-35))
.put(JSONArray().put(45).put(-35))
.put(JSONArray().put(45).put(35))
.put(JSONArray().put(-45).put(35))
.put(JSONArray().put(-45).put(-35))
))
val source = JSONObject().put("type", "geojson").put("data", JSONObject()
.put("type", "Feature")
.put("properties", JSONObject())
.put("geometry", geometry))
val layers = JSONArray()
.put(JSONObject().put("id", "step097-background").put("type", "background")
.put("paint", JSONObject().put("background-color", "#15243a")))
.put(fill(FIRST_FILL, "#f94144"))
.put(fill(SECOND_FILL, "#43aa8b"))
.put(fill(THIRD_FILL, "#577590"))
.put(fill(FOURTH_FILL, "#f9c74f"))
return JSONObject()
.put("version", 8)
.put("name", "STEP-097 same-source stencil fixture")
.put("sources", JSONObject().put(SOURCE, source))
.put("layers", layers)
.toString()
}
fun planJson(topology: Topology): String {
val segments = JSONArray()
fun add(strategy: String, start: String? = null) {
val segment = JSONObject().put("strategy", strategy)
if (start != null) segment.put("startLayerId", start)
segments.put(segment)
}
when (topology) {
Topology.RR -> {
add("RETAINED")
add("RETAINED", SECOND_FILL)
}
Topology.RCR -> {
add("RETAINED")
add("CONTINUOUS", SECOND_FILL)
add("RETAINED", THIRD_FILL)
}
Topology.CRR -> {
add("CONTINUOUS")
add("RETAINED", SECOND_FILL)
add("RETAINED", THIRD_FILL)
}
}
return JSONObject().put("schemaVersion", 1).put("segments", segments).toString()
}
private fun fill(id: String, color: String): JSONObject = JSONObject()
.put("id", id)
.put("type", "fill")
.put("source", SOURCE)
.put("paint", JSONObject().put("fill-color", color).put("fill-opacity", 0.55))
}

View File

@@ -0,0 +1,46 @@
package org.maplibre.android.testapp.activity.particle
import android.content.res.AssetManager
import android.util.Log
import org.maplibre.android.maps.WeatherRuntime
/** STEP-097 A2 test-only ingress. Uses the existing public Android V4 API. */
internal object Step097V4GoldenFixture {
const val KEY = "fixture.wind"
const val T0_MS = 1_789_239_600_000L
const val T1_MS = 1_789_243_200_000L
const val MID_MS = 1_789_241_400_000L
private const val ROOT = "integration/step097-v4/wind/"
private const val TAG = "ParticleValidation"
fun load(assets: AssetManager, runtime: WeatherRuntime) {
val contract = assets.open(ROOT + "contract.json").bufferedReader().use { it.readText() }
val clear = runtime.clearAllEncodedFields()
val color = runtime.setEncodedColorBinding(KEY, 1, 1, 1, 1)
val particle = runtime.setEncodedParticleBinding(KEY, 0, 1, 1, 2, 1, 0, 0, 1)
val t0 = runtime.submitEncodedField(KEY, "fixture-wind", 1L, 1L,
contract, assets.open(ROOT + "T0.qbin").use { it.readBytes() })
val t1 = runtime.submitEncodedField(KEY, "fixture-wind", 2L, 2L,
contract, assets.open(ROOT + "T1.qbin").use { it.readBytes() })
val time = runtime.setEncodedForecastTime(MID_MS)
Log.i(TAG, "STEP097_A2_V4_SUBMIT clear=$clear color=$color particle=$particle t0=$t0 t1=$t1 time=$time")
check(clear == WeatherRuntime.CommandResult.ACCEPTED &&
color == WeatherRuntime.CommandResult.ACCEPTED &&
particle == WeatherRuntime.CommandResult.ACCEPTED &&
t0 == WeatherRuntime.EncodedFieldSubmitResult.ACCEPTED_INSERTED &&
t1 == WeatherRuntime.EncodedFieldSubmitResult.ACCEPTED_INSERTED &&
time == WeatherRuntime.CommandResult.ACCEPTED) { "V4 golden ingress rejected" }
Log.i(TAG, "STEP097_A2_V4_READY fieldKey=$KEY T0=$T0_MS T1=$T1_MS target=$MID_MS")
}
fun command(runtime: WeatherRuntime, action: Int): WeatherRuntime.CommandResult = when (action) {
1 -> runtime.setEncodedForecastPlaybackState(true)
2 -> runtime.setEncodedForecastPlaybackState(false)
3 -> runtime.setEncodedForecastTime(T0_MS + 900_000L)
4 -> runtime.setEncodedForecastTime(T0_MS + 2_700_000L)
5 -> runtime.setEncodedForecastPlaybackSpeed(2.0f)
6 -> runtime.setEncodedForecastPlaybackSpeed(1.0f)
7 -> runtime.clearAllEncodedFields()
else -> WeatherRuntime.CommandResult.REJECTED
}
}

View File

@@ -0,0 +1,149 @@
package org.maplibre.android.testapp.activity.particle
import org.json.JSONArray
import org.json.JSONObject
/** STEP-099/100 offline source readiness and retained segment fixtures. */
internal object Step099ReadinessFixture {
enum class Scenario(val label: String) {
T1_STATIC("T1 STATIC"),
T2_GEOJSON_1HZ("T2 GEOJSON 1HZ"),
T3_PERMANENT_UNREADY("T3 PERMANENT UNREADY"),
T4_RCR("T4 R-C-R")
}
const val PARTICLE_SOURCE = "particle-fixture"
const val STATIC_SOURCE = "step099-static"
const val UPPER_STATIC_SOURCE = "step099-upper-static"
const val DYNAMIC_SOURCE = "step099-dynamic"
const val DYNAMIC_SOURCE_B = "step099-dynamic-b"
const val UNREADY_SOURCE = "step099-unready-raster-array"
const val STATIC_LAYER = "step099-static-fill"
const val WEATHER_LAYER = "step099-weather"
const val PARTICLE_LAYER = "step099-particle"
const val DYNAMIC_LAYER = "step099-dynamic-circle"
const val DYNAMIC_LAYER_B = "step099-dynamic-circle-b"
const val UPPER_STATIC_LAYER = "step099-static-upper-fill"
fun styleJson(scenario: Scenario): String {
val sources = JSONObject()
.put(PARTICLE_SOURCE, JSONObject()
.put("type", "raster-array")
.put("fixture", "uniform-east")
.put("metadata", "asset://integration/particle-fixtures/uniform-east/metadata.json")
.put("field", "asset://integration/particle-fixtures/uniform-east/field.f32"))
.put(STATIC_SOURCE, geoJsonSource(
"""{"type":"Polygon","coordinates":[[[112,0],[158,0],[158,56],[112,56],[112,0]]]}"""
))
if (scenario == Scenario.T2_GEOJSON_1HZ || scenario == Scenario.T4_RCR) {
sources.put(DYNAMIC_SOURCE, geoJsonSource(
"""{"type":"Point","coordinates":[135.0,28.0]}"""
))
}
if (scenario == Scenario.T1_STATIC || scenario == Scenario.T3_PERMANENT_UNREADY) {
sources.put(UPPER_STATIC_SOURCE, geoJsonSource(
"""{"type":"Polygon","coordinates":[[[116,4],[154,4],[154,52],[116,52],[116,4]]]}"""
))
}
if (scenario == Scenario.T2_GEOJSON_1HZ) {
sources.put(DYNAMIC_SOURCE_B, geoJsonSource(
"""{"type":"Point","coordinates":[136.0,29.0]}"""
))
}
if (scenario == Scenario.T3_PERMANENT_UNREADY) {
// A valid host-submission RasterArraySource with no submitted field is intentionally isLoaded=false.
// It has no layer on purpose: STEP-099 verifies whether a globally present but unrelated Source
// can still hold RenderOrchestrator::isLoaded() false.
sources.put(UNREADY_SOURCE, JSONObject()
.put("type", "raster-array")
.put("host-field-submission", true))
}
val layers = JSONArray()
.put(JSONObject()
.put("id", "step099-background")
.put("type", "background")
.put("paint", JSONObject().put("background-color", "#0b2942")))
.put(JSONObject()
.put("id", STATIC_LAYER)
.put("type", "fill")
.put("source", STATIC_SOURCE)
.put("paint", JSONObject()
.put("fill-color", "#176b87")
.put("fill-opacity", 0.58)))
.put(JSONObject()
.put("id", WEATHER_LAYER)
.put("type", "weather-color")
.put("source", PARTICLE_SOURCE)
.put("paint", JSONObject().put("weather-color-opacity", 0.32)))
.put(JSONObject()
.put("id", PARTICLE_LAYER)
.put("type", "raster-particle")
.put("source", PARTICLE_SOURCE)
.put("paint", JSONObject()
.put("raster-particle-active", true)
.put("raster-particle-particle-count", 1024)))
if (scenario == Scenario.T2_GEOJSON_1HZ || scenario == Scenario.T4_RCR) {
layers.put(JSONObject()
.put("id", DYNAMIC_LAYER)
.put("type", "circle")
.put("source", DYNAMIC_SOURCE)
.put("paint", JSONObject()
.put("circle-radius", 12)
.put("circle-color", "#ffcc00")
.put("circle-opacity", 0.95)))
}
if (scenario == Scenario.T2_GEOJSON_1HZ) {
layers.put(JSONObject()
.put("id", DYNAMIC_LAYER_B)
.put("type", "circle")
.put("source", DYNAMIC_SOURCE_B)
.put("paint", JSONObject()
.put("circle-radius", 10)
.put("circle-color", "#ff6600")
.put("circle-opacity", 0.90)))
}
if (scenario == Scenario.T1_STATIC || scenario == Scenario.T3_PERMANENT_UNREADY) {
layers.put(JSONObject()
.put("id", UPPER_STATIC_LAYER)
.put("type", "fill")
.put("source", UPPER_STATIC_SOURCE)
.put("paint", JSONObject().put("fill-color", "#d3a640").put("fill-opacity", 0.38)))
}
return JSONObject()
.put("version", 8)
.put("name", "STEP-099 Source Readiness Diagnostics")
.put("sources", sources)
.put("layers", layers)
.toString()
}
fun planJson(scenario: Scenario): String {
val segments = JSONArray()
fun add(strategy: String, start: String? = null) {
val segment = JSONObject().put("strategy", strategy)
if (start != null) segment.put("startLayerId", start)
segments.put(segment)
}
add("RETAINED")
add("CONTINUOUS", WEATHER_LAYER)
add("RETAINED", if (scenario == Scenario.T2_GEOJSON_1HZ || scenario == Scenario.T4_RCR) DYNAMIC_LAYER else UPPER_STATIC_LAYER)
return JSONObject()
.put("schemaVersion", 1)
.put("segments", segments)
.toString()
}
private fun geoJsonSource(geometryJson: String): JSONObject = JSONObject()
.put("type", "geojson")
.put("data", JSONObject()
.put("type", "Feature")
.put("properties", JSONObject())
.put("geometry", JSONObject(geometryJson)))
}

View File

@@ -0,0 +1,65 @@
package org.maplibre.android.testapp.activity.particle
import org.json.JSONArray
import org.json.JSONObject
/** Offline STEP-102 R-C-R fixture; only the upper retained range changes placement capability. */
internal object Step102PlacementFixture {
enum class Scenario { T1_FREE_SYMBOL, T2_TWO_FREE }
const val STATIC_SOURCE = "step102-static"
const val SYMBOL_SOURCE = "step102-symbol"
private const val PARTICLE_SOURCE = "step102-field"
private const val PARTICLE_LAYER = "step102-particle"
private const val UPPER_LAYER = "step102-upper"
fun styleJson(scenario: Scenario): String {
val sources = JSONObject()
.put(STATIC_SOURCE, geoJsonSource(
"""{"type":"Polygon","coordinates":[[[112,0],[158,0],[158,56],[112,56],[112,0]]]}"""))
.put(PARTICLE_SOURCE, JSONObject()
.put("type", "raster-array")
.put("fixture", "uniform-east")
.put("metadata", "asset://integration/particle-fixtures/uniform-east/metadata.json")
.put("field", "asset://integration/particle-fixtures/uniform-east/field.f32"))
if (scenario == Scenario.T1_FREE_SYMBOL) {
sources.put(SYMBOL_SOURCE, geoJsonSource("""{"type":"Point","coordinates":[135.0,28.0]}"""))
}
val layers = JSONArray()
.put(JSONObject().put("id", "step102-background").put("type", "background")
.put("paint", JSONObject().put("background-color", "#0b2942")))
.put(JSONObject().put("id", "step102-static-fill").put("type", "fill")
.put("source", STATIC_SOURCE)
.put("paint", JSONObject().put("fill-color", "#176b87").put("fill-opacity", 0.58)))
.put(JSONObject().put("id", PARTICLE_LAYER).put("type", "raster-particle")
.put("source", PARTICLE_SOURCE)
.put("paint", JSONObject().put("raster-particle-active", true)
.put("raster-particle-particle-count", 1024)))
if (scenario == Scenario.T1_FREE_SYMBOL) {
layers.put(JSONObject().put("id", UPPER_LAYER).put("type", "symbol")
.put("source", SYMBOL_SOURCE)
.put("layout", JSONObject().put("text-field", "STEP-102")
.put("text-font", JSONArray().put("DIN Offc Pro Regular").put("Arial Unicode MS Regular"))
.put("text-size", 20))
.put("paint", JSONObject().put("text-color", "#ffcc00")))
} else {
layers.put(JSONObject().put("id", UPPER_LAYER).put("type", "fill")
.put("source", STATIC_SOURCE)
.put("paint", JSONObject().put("fill-color", "#d3a640").put("fill-opacity", 0.38)))
}
return JSONObject().put("version", 8).put("name", "STEP-102 Placement Locality")
.put("glyphs", "asset://fonts/{fontstack}/{range}.pbf")
.put("sources", sources).put("layers", layers).toString()
}
fun planJson(): String = JSONObject().put("schemaVersion", 1)
.put("segments", JSONArray()
.put(JSONObject().put("strategy", "RETAINED"))
.put(JSONObject().put("strategy", "CONTINUOUS").put("startLayerId", PARTICLE_LAYER))
.put(JSONObject().put("strategy", "RETAINED").put("startLayerId", UPPER_LAYER)))
.toString()
private fun geoJsonSource(geometryJson: String): JSONObject = JSONObject().put("type", "geojson")
.put("data", JSONObject().put("type", "Feature").put("properties", JSONObject())
.put("geometry", JSONObject(geometryJson)))
}

View File

@@ -5,10 +5,21 @@
android:layout_height="match_parent"
android:orientation="vertical">
<TextView
android:id="@+id/apkVersionView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/primaryDark"
android:padding="12dp"
android:textColor="@android:color/white"
android:textIsSelectable="true"
android:textSize="14sp" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:scrollbars="vertical"/>
</LinearLayout>

View File

@@ -0,0 +1,27 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent">
<org.maplibre.android.maps.MapView android:id="@id/mapView" android:layout_width="match_parent" android:layout_height="match_parent" />
<LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_gravity="bottom" android:background="#DD102030" android:orientation="vertical" android:padding="4dp">
<TextView android:id="@+id/validationStatus" android:layout_width="match_parent" android:layout_height="42dp" android:gravity="center_vertical" android:maxLines="2" android:paddingLeft="4dp" android:paddingRight="4dp" android:textColor="@android:color/white" android:textSize="12sp" />
<TextView android:id="@+id/validationDiagnostics" android:layout_width="match_parent" android:layout_height="92dp" android:fontFamily="monospace" android:gravity="center_vertical" android:maxLines="6" android:paddingLeft="4dp" android:paddingRight="4dp" android:text="STEP-093 diagnostics: open STEP 093" android:textColor="#FFDD88" android:textSize="9sp" />
<LinearLayout android:layout_width="match_parent" android:layout_height="40dp" android:orientation="horizontal">
<Button android:id="@+id/validationMenuTime" style="@style/ParticleValidationButton" android:layout_width="0dp" android:layout_weight="1" android:text="TIME" />
<Button android:id="@+id/validationMenuVisual" style="@style/ParticleValidationButton" android:layout_width="0dp" android:layout_weight="1" android:text="VIS" />
<Button android:id="@+id/validationMenuCount" style="@style/ParticleValidationButton" android:layout_width="0dp" android:layout_weight="1" android:text="COUNT" />
<Button android:id="@+id/validationMenuMap" style="@style/ParticleValidationButton" android:layout_width="0dp" android:layout_weight="1" android:text="MAP" />
<Button android:id="@+id/validationMenuRealWeather" style="@style/ParticleValidationButton" android:layout_width="0dp" android:layout_weight="1" android:text="REAL WEATHER" android:textSize="10sp" />
<Button android:id="@+id/validationMenuPbfMode" style="@style/ParticleValidationButton" android:layout_width="0dp" android:layout_weight="1" android:text="PBF MODE" android:textSize="9sp" />
<Button android:id="@+id/validationMenuNavSeaPbf" style="@style/ParticleValidationButton" android:layout_width="0dp" android:layout_weight="1" android:text="NAVSEA PBF" android:textSize="9sp" />
<Button android:id="@+id/validationMenuRetainedCache" style="@style/ParticleValidationButton" android:layout_width="0dp" android:layout_weight="1" android:text="CACHE ON" android:textSize="9sp" />
<Button android:id="@+id/validationMenuStep093" style="@style/ParticleValidationButton" android:layout_width="0dp" android:layout_weight="1" android:text="STEP 093" android:textSize="9sp" />
<Button android:id="@+id/validationMenuMore" style="@style/ParticleValidationButton" android:layout_width="0dp" android:layout_weight="1" android:text="MORE" />
</LinearLayout>
<!-- Existing action IDs are invisible menu targets; no permanent button grid. -->
<LinearLayout android:visibility="gone" android:layout_width="1dp" android:layout_height="1dp" android:orientation="vertical">
<Button style="@style/ParticleValidationButton" android:id="@+id/validationActive"/><Button style="@style/ParticleValidationButton" android:id="@+id/validationStatic"/><Button style="@style/ParticleValidationButton" android:id="@+id/validationDisable"/><Button style="@style/ParticleValidationButton" android:id="@+id/validationEnable"/><Button style="@style/ParticleValidationButton" android:id="@+id/validationGlobal"/><Button style="@style/ParticleValidationButton" android:id="@+id/validationOceanA"/><Button style="@style/ParticleValidationButton" android:id="@+id/validationOceanB"/>
<Button style="@style/ParticleValidationButton" android:id="@+id/validationUniformEast"/><Button style="@style/ParticleValidationButton" android:id="@+id/validationUniformNorth"/><Button style="@style/ParticleValidationButton" android:id="@+id/validationVortex"/><Button style="@style/ParticleValidationButton" android:id="@+id/validationTimeWindow"/><Button style="@style/ParticleValidationButton" android:id="@+id/validationMix0"/><Button style="@style/ParticleValidationButton" android:id="@+id/validationMix50"/><Button style="@style/ParticleValidationButton" android:id="@+id/validationMix100"/>
<Button style="@style/ParticleValidationButton" android:id="@+id/validationParticle256"/><Button style="@style/ParticleValidationButton" android:id="@+id/validationParticle1024"/><Button style="@style/ParticleValidationButton" android:id="@+id/validationParticle4096"/><Button style="@style/ParticleValidationButton" android:id="@+id/validationParticle10000"/>
<Button style="@style/ParticleValidationButton" android:id="@+id/validationAdvanceWindow"/><Button style="@style/ParticleValidationButton" android:id="@+id/validationDuplicateWindow"/><Button style="@style/ParticleValidationButton" android:id="@+id/validationProductionInvalid"/><Button style="@style/ParticleValidationButton" android:id="@+id/validationHostFieldSubmit"/><Button style="@style/ParticleValidationButton" android:id="@+id/validationRealWeather"/><Button style="@style/ParticleValidationButton" android:id="@+id/validationHostFieldDuplicate"/><Button style="@style/ParticleValidationButton" android:id="@+id/validationHostFieldInvalid"/><Button style="@style/ParticleValidationButton" android:id="@+id/validationVisualDefault"/><Button style="@style/ParticleValidationButton" android:id="@+id/validationVisualSizeDown"/><Button style="@style/ParticleValidationButton" android:id="@+id/validationVisualSizeUp"/><Button style="@style/ParticleValidationButton" android:id="@+id/validationVisualLengthDown"/><Button style="@style/ParticleValidationButton" android:id="@+id/validationVisualLengthUp"/><Button style="@style/ParticleValidationButton" android:id="@+id/validationVisualOpacityDown"/><Button style="@style/ParticleValidationButton" android:id="@+id/validationVisualOpacityUp"/><Button style="@style/ParticleValidationButton" android:id="@+id/validationVisualTrailDown"/><Button style="@style/ParticleValidationButton" android:id="@+id/validationVisualTrailUp"/><Button style="@style/ParticleValidationButton" android:id="@+id/validationVisualFadeDown"/><Button style="@style/ParticleValidationButton" android:id="@+id/validationVisualFadeUp"/><Button style="@style/ParticleValidationButton" android:id="@+id/validationVisualColor"/><Button style="@style/ParticleValidationButton" android:id="@+id/validationDateline"/><Button style="@style/ParticleValidationButton" android:id="@+id/validationWorldWrap"/>
</LinearLayout>
</LinearLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View File

@@ -1,6 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="description_custom_layer">Overlay a custom native layer on the map</string>
<string name="description_particle_core_layer_validation">Validate a source-less Core Style/RenderLayer, frame transform and repaint lifecycle</string>
<string name="description_info_window_adapter">Learn how to create a custom InfoWindow</string>
<string name="description_cameraposition">CameraPosition capabilities</string>
<string name="description_map_fragment">Showcase MapFragment</string>

View File

@@ -8,10 +8,55 @@
<string name="activity_benchmark_world_tour">World Tour Benchmark</string>
<string name="description_world_tour_benchmark">Writes out avg. fps and 1% fps after world tour with .flyTo()</string>
<string name="search">Search</string>
<string name="apk_version_format">APK %1$s (%2$d) · %3$s/%4$s · Git %5$s</string>
<string name="category_data">Data Sources</string>
<string name="rendering_engine_unsupported">%1$s is not supported by this build. Use the multiBackend flavor to switch backends at runtime.</string>
<string name="action_rendering_engine">Rendering Engine</string>
<string name="rendering_engine_dialog_title">Select Rendering Engine</string>
<string name="rendering_engine_current_label">%1$s (current)</string>
<string name="cancel">Cancel</string>
<string name="particle_validation_active">ACTIVE</string>
<string name="particle_validation_static">STATIC</string>
<string name="particle_validation_disable">DISABLE</string>
<string name="particle_validation_enable">ENABLE</string>
<string name="particle_validation_active_status">30-02 ACTIVE: GPU State/Trail ping-pong requests frames. Move, zoom, rotate, pitch and resize.</string>
<string name="particle_validation_static_status">30-02 STATIC: GPU ping-pong stopped requesting frames. Gestures must still render normally.</string>
<string name="particle_validation_disabled_status">30-02 DISABLED: validation layer and its GPU resources were removed. Tap ENABLE to recreate them.</string>
<string name="particle_validation_global">GLOBAL</string>
<string name="particle_validation_ocean_a">OCEAN A</string>
<string name="particle_validation_ocean_b">OCEAN B</string>
<string name="particle_validation_dateline">DATELINE</string>
<string name="particle_validation_world_wrap">WORLD WRAP</string>
<string name="particle_validation_uniform_east">UNIFORM EAST</string>
<string name="particle_validation_uniform_north">UNIFORM NORTH</string>
<string name="particle_validation_vortex">VORTEX</string>
<string name="particle_validation_time_window">T0 EAST / T1 NORTH</string>
<string name="particle_validation_advance_window">ADVANCE WINDOW</string>
<string name="particle_validation_duplicate_window">DUPLICATE T1</string>
<string name="particle_validation_mix_0">MIX 0%</string>
<string name="particle_validation_mix_50">MIX 50%</string>
<string name="particle_validation_mix_100">MIX 100%</string>
<string name="particle_validation_particles_256">256</string>
<string name="particle_validation_particles_1024">1K</string>
<string name="particle_validation_particles_4096">4K</string>
<string name="particle_validation_particles_16384">16K</string>
<string name="particle_validation_particles_65536">64K</string>
<string name="particle_validation_particles_262144">256K</string>
<string name="particle_validation_particles_1048576">1M</string>
<string name="particle_validation_visual_default">VIS DEFAULT</string>
<string name="particle_validation_visual_size_down">SIZE −</string>
<string name="particle_validation_visual_size_up">SIZE +</string>
<string name="particle_validation_visual_length_down">LENGTH −</string>
<string name="particle_validation_visual_length_up">LENGTH +</string>
<string name="particle_validation_visual_opacity_down">OPACITY −</string>
<string name="particle_validation_visual_opacity_up">OPACITY +</string>
<string name="particle_validation_visual_trail_down">TRAIL −</string>
<string name="particle_validation_visual_trail_up">TRAIL +</string>
<string name="particle_validation_visual_fade_down">FADE −</string>
<string name="particle_validation_visual_fade_up">FADE +</string>
<string name="particle_validation_visual_color">COLOR</string>
<string name="particle_validation_global_active_status">40-01 GLOBAL ACTIVE: offline field simulation and current-frame composite.</string>
<string name="particle_validation_global_static_status">40-01 GLOBAL STATIC: zero-step bootstrap and frozen composite.</string>
<string name="particle_validation_ocean_a_status">40-01 OCEAN_ONLY_A: shared coastal-mask consumer.</string>
<string name="particle_validation_ocean_b_status">40-01 OCEAN_ONLY_B: shared coastal-mask consumer.</string>
</resources>

View File

@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="ParticleValidationButton">
<item name="android:layout_width">1dp</item>
<item name="android:layout_height">32dp</item>
<item name="android:minHeight">32dp</item>
<item name="android:minWidth">0dp</item>
<item name="android:padding">0dp</item>
<item name="android:textSize">11sp</item>
</style>
</resources>

View File

@@ -25,6 +25,7 @@
<string name="activity_snapshot">Snapshot Activity</string>
<string name="activity_snapshot_within">Snapshot with within expression</string>
<string name="activity_custom_layer">Custom Layer</string>
<string name="activity_particle_core_layer_validation">Particle Core Layer Validation</string>
<string name="activity_map_padding">Map Padding</string>
<string name="activity_debug_mode">Debug Mode</string>
<string name="activity_offline">Offline Map</string>

View File

@@ -1,9 +1,9 @@
extra["mapLibreArtifactGroupId"] = "org.maplibre.gl"
extra["mapLibreArtifactId"] = "android-sdk"
extra["mapLibreArtifactTitle"] = "MapLibre Android"
extra["mapLibreArtifactDescription"] = "MapLibre Android"
extra["mapLibreDeveloperName"] = "MapLibre"
extra["mapLibreDeveloperId"] = "maplibre"
extra["mapLibreArtifactGroupId"] = "cc.navsea.maplibre"
extra["mapLibreArtifactId"] = "android-sdk-particle"
extra["mapLibreArtifactTitle"] = "NavSea MapLibre Particle Android"
extra["mapLibreArtifactDescription"] = "NavSea MapLibre Particle Android"
extra["mapLibreDeveloperName"] = "NavSea"
extra["mapLibreDeveloperId"] = "navsea"
extra["mapLibreArtifactUrl"] = "https://github.com/maplibre/maplibre-native"
extra["mapLibreArtifactScmUrl"] = "scm:git@github.com:maplibre/maplibre-native.git"
extra["mapLibreArtifactLicenseName"] = "BSD"

View File

@@ -6,6 +6,7 @@ import org.gradle.api.tasks.compile.JavaCompile
import org.gradle.authentication.http.BasicAuthentication
import org.gradle.external.javadoc.StandardJavadocDocletOptions
import org.gradle.kotlin.dsl.get
import org.gradle.plugins.signing.SigningExtension
import java.util.Locale
plugins {
@@ -24,6 +25,13 @@ val publicationRepositoryUsername = providers.gradleProperty("publicationReposit
.orElse(providers.environmentVariable("MAVEN_REPOSITORY_USERNAME"))
val publicationRepositoryPassword = providers.gradleProperty("publicationRepositoryPassword")
.orElse(providers.environmentVariable("MAVEN_REPOSITORY_PASSWORD"))
// Gradle SigningExtension evaluates this Provider when a Sign task executes. Maven Local keeps
// the release coordinate but does not need a PGP signatory; Maven Central publication does.
val signingRequiredForMavenCentral = providers.provider {
gradle.taskGraph.allTasks.any {
it.name.endsWith("PublicationToMavenCentralRepository")
}
}
androidLibrary.publishing {
singleVariant("vulkanRelease")
@@ -67,6 +75,10 @@ afterEvaluate {
signAllPublications()
}
}
// signAllPublications() creates the signatures used by the formal Central path. Override
// only its execution requirement: Local publish Sign tasks are skipped without a signatory,
// while every *PublicationToMavenCentralRepository task remains signing-required.
extensions.getByType<SigningExtension>().setRequired(signingRequiredForMavenCentral)
}
// Configure task dependencies after all tasks are created