feat: add production-like particle field feed
This commit is contained in:
@@ -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");
|
||||
|
||||
Reference in New Issue
Block a user