feat: add production-like particle field feed

This commit is contained in:
OpenAI Codex
2026-09-04 14:51:42 +08:00
parent 885e7aebef
commit 7c679f09ac
23 changed files with 545 additions and 26 deletions

View File

@@ -23,6 +23,7 @@
#include <mbgl/style/sources/vector_source.hpp>
#include <mbgl/style/style.hpp>
#include <mbgl/particle/field/field_data_adapter.hpp>
#include <mbgl/particle/field/local_particle_field_feed.hpp>
#include <mbgl/renderer/sources/render_geojson_source.hpp>
#include <mbgl/renderer/sources/render_raster_array_source.hpp>
@@ -62,6 +63,14 @@ std::string fixtureBytes(const std::string& fixtureID, const std::string& file)
return util::read_file("particle-fixtures/" + fixtureID + "/" + file);
}
std::string localMetadata(const int index) {
return "{\"schema\":\"maplibre-particle-field/v1\",\"fieldId\":\"production-t" +
std::to_string(index) + "\",\"fieldType\":\"WIND\",\"displayDomain\":\"GLOBAL\",\"generation\":" +
std::to_string(index + 1) + ",\"validTime\":" + std::to_string(202609040000ULL + index * 100) +
",\"dataVersion\":" + std::to_string(index + 1) +
",\"bounds\":{\"west\":-180,\"south\":-85,\"east\":180,\"north\":85},\"grid\":{\"width\":16,\"height\":16},\"vector\":{\"layout\":\"interleaved-uv\",\"encoding\":\"float32-le\",\"noData\":-9999},\"payload\":{\"byteLength\":2048}}";
}
} // namespace
using namespace mbgl;
@@ -238,6 +247,7 @@ TEST(Source, ParticleFieldDataAdapterAcceptsNeutralDescriptor) {
descriptor.fieldID = "weather-service-wind-20260904T0000Z";
descriptor.fieldType = "WIND";
descriptor.displayDomain = "GLOBAL";
descriptor.generation = 1;
descriptor.width = 1;
descriptor.height = 1;
descriptor.byteLength = 2 * sizeof(float);
@@ -257,6 +267,7 @@ TEST(Source, ParticleFieldDataAdapterReturnsStructuredValidationErrors) {
descriptor.fieldID = "field";
descriptor.fieldType = "WIND";
descriptor.displayDomain = "GLOBAL";
descriptor.generation = 1;
descriptor.width = 2;
descriptor.height = 1;
descriptor.byteLength = 4 * sizeof(float);
@@ -273,6 +284,68 @@ TEST(Source, ParticleFieldDataAdapterReturnsStructuredValidationErrors) {
EXPECT_EQ(result.error, particle::ParticleFieldAdapterError::UnsupportedFormat);
}
TEST(Source, LocalProductionFeedSubmitsSnapshotsWithProducerTime) {
const auto field = fixtureBytes("uniform-east", "field.f32");
const std::vector<char> payload(field.begin(), field.end());
std::vector<particle::ParticleFieldSnapshot> snapshots;
for (int index = 0; index < 4; ++index) {
const auto result = particle::LocalParticleFieldFeed::load(localMetadata(index), payload);
ASSERT_TRUE(result) << result.message;
ASSERT_TRUE(result.snapshot);
EXPECT_EQ(result.snapshot->generation, static_cast<uint64_t>(index + 1));
EXPECT_EQ(result.snapshot->validTime, 202609040000ULL + static_cast<uint64_t>(index) * 100);
snapshots.push_back(std::move(*result.snapshot));
}
for (size_t index = 1; index < snapshots.size(); ++index) {
EXPECT_LT(snapshots[index - 1].validTime, snapshots[index].validTime);
}
SourceTest test;
std::vector<std::string> metadata;
for (int index = 0; index < 4; ++index) metadata.push_back(localMetadata(index));
test.fileSource->sourceResponse = [&](const Resource& resource) -> Response {
Response response;
const auto prefix = std::string("local-metadata-");
if (resource.url.rfind(prefix, 0) == 0) {
const auto index = static_cast<size_t>(std::stoi(resource.url.substr(prefix.size())));
response.data = std::make_unique<std::string>(metadata.at(index));
} else if (resource.url.rfind("local-field-", 0) == 0) {
response.data = std::make_unique<std::string>(field);
} else {
ADD_FAILURE() << "Unexpected local production resource: " << resource.url;
}
return response;
};
std::vector<style::RasterArrayLocalFieldDescriptor> resources;
for (int index = 0; index < 4; ++index) {
resources.push_back({"local-metadata-" + std::to_string(index), "local-field-" + std::to_string(index)});
}
style::RasterArraySource source{"local-production", std::move(resources)};
source.setObserver(&test.styleObserver);
test.styleObserver.sourceChanged = [&](Source&) {
if (source.impl().timeline) test.end();
};
source.loadDescription(*test.fileSource);
test.run();
ASSERT_TRUE(source.impl().timeline);
EXPECT_EQ(source.impl().timeline->frame().state, style::RasterArrayPlaybackState::Ready);
EXPECT_EQ(source.impl().timeline->frame().t0->validTime, snapshots[0].validTime);
EXPECT_EQ(source.impl().timeline->frame().t1->validTime, snapshots[1].validTime);
EXPECT_TRUE(source.controlRuntimeTimeline(0));
}
TEST(Source, LocalProductionFeedRejectsMissingGeneration) {
const auto field = fixtureBytes("uniform-east", "field.f32");
auto metadata = localMetadata(0);
const auto position = metadata.find("\"generation\":1");
ASSERT_NE(position, std::string::npos);
metadata.replace(position, std::string("\"generation\":1").size(), "\"generation\":0");
const std::vector<char> payload(field.begin(), field.end());
const auto result = particle::LocalParticleFieldFeed::load(metadata, payload);
EXPECT_FALSE(result);
EXPECT_EQ(result.error, particle::ParticleFieldAdapterError::InvalidTimestampOrIdentity);
}
TEST(Source, RasterArrayTimelineCandidatesReachReadyThroughFieldAdapter) {
SourceTest test;
const auto eastMetadata = fixtureBytes("uniform-east", "metadata.json");