core: add scalar vector field snapshots
This commit is contained in:
@@ -58,6 +58,7 @@
|
||||
|
||||
#include <optional>
|
||||
#include <cstring>
|
||||
#include <limits>
|
||||
|
||||
namespace {
|
||||
|
||||
@@ -73,6 +74,26 @@ std::string localMetadata(const int index) {
|
||||
",\"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}}";
|
||||
}
|
||||
|
||||
std::vector<char> floatBytes(std::initializer_list<float> values) {
|
||||
std::vector<float> copy(values);
|
||||
std::vector<char> bytes(copy.size() * sizeof(float));
|
||||
std::memcpy(bytes.data(), copy.data(), bytes.size());
|
||||
return bytes;
|
||||
}
|
||||
|
||||
std::string v2Metadata(bool scalar, bool vector, const char* semantics = "VELOCITY") {
|
||||
std::string result = "{\"schema\":\"maplibre-particle-field/v2\",\"fieldId\":\"v2-field\",\"generation\":1,\"validTime\":1,\"grid\":{\"width\":2,\"height\":1},\"bounds\":{\"west\":0,\"south\":0,\"east\":1,\"north\":1},\"validity\":{\"encoding\":\"uint8\",\"byteLength\":2}";
|
||||
if (scalar) result += ",\"scalar\":{\"encoding\":\"float32-le\",\"byteLength\":8,\"noData\":-9999}";
|
||||
if (vector) result += std::string(",\"vector\":{\"layout\":\"interleaved-uv\",\"encoding\":\"float32-le\",\"byteLength\":16,\"noData\":-9999,\"semantics\":\"") + semantics + "\"}";
|
||||
return result + "}";
|
||||
}
|
||||
|
||||
particle::ParticleFieldPayloadBundle v2Payload(std::optional<std::vector<char>> scalar,
|
||||
std::optional<std::vector<char>> vector,
|
||||
std::vector<char> validity) {
|
||||
return {.scalar = std::move(scalar), .vector = std::move(vector), .validity = std::move(validity)};
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
using namespace mbgl;
|
||||
@@ -256,6 +277,7 @@ TEST(Source, ParticleFieldDataAdapterAcceptsNeutralDescriptor) {
|
||||
descriptor.fieldType = "WIND";
|
||||
descriptor.displayDomain = "GLOBAL";
|
||||
descriptor.generation = 1;
|
||||
descriptor.validTime = 1;
|
||||
descriptor.width = 1;
|
||||
descriptor.height = 1;
|
||||
descriptor.byteLength = 2 * sizeof(float);
|
||||
@@ -276,6 +298,7 @@ TEST(Source, ParticleFieldDataAdapterReturnsStructuredValidationErrors) {
|
||||
descriptor.fieldType = "WIND";
|
||||
descriptor.displayDomain = "GLOBAL";
|
||||
descriptor.generation = 1;
|
||||
descriptor.validTime = 1;
|
||||
descriptor.width = 2;
|
||||
descriptor.height = 1;
|
||||
descriptor.byteLength = 4 * sizeof(float);
|
||||
@@ -292,6 +315,74 @@ TEST(Source, ParticleFieldDataAdapterReturnsStructuredValidationErrors) {
|
||||
EXPECT_EQ(result.error, particle::ParticleFieldAdapterError::UnsupportedFormat);
|
||||
}
|
||||
|
||||
TEST(Source, ParticleFieldV3V2DecodesAllSupportedChannelCombinations) {
|
||||
const std::vector<char> valid{1, 0};
|
||||
const auto vectorOnly = particle::LocalParticleFieldFeed::load(
|
||||
v2Metadata(false, true), v2Payload(std::nullopt, floatBytes({1, 2, -9999, -9999}), valid));
|
||||
ASSERT_TRUE(vectorOnly) << vectorOnly.message;
|
||||
EXPECT_TRUE(vectorOnly.snapshot->hasVector());
|
||||
EXPECT_FALSE(vectorOnly.snapshot->hasScalar());
|
||||
EXPECT_EQ(vectorOnly.snapshot->vectorSemantics, particle::VectorSemantics::Velocity);
|
||||
|
||||
const auto scalarOnly = particle::LocalParticleFieldFeed::load(
|
||||
v2Metadata(true, false), v2Payload(floatBytes({3, -9999}), std::nullopt, valid));
|
||||
ASSERT_TRUE(scalarOnly) << scalarOnly.message;
|
||||
EXPECT_TRUE(scalarOnly.snapshot->hasScalar());
|
||||
EXPECT_FALSE(scalarOnly.snapshot->hasVector());
|
||||
|
||||
const auto combined = particle::LocalParticleFieldFeed::load(
|
||||
v2Metadata(true, true), v2Payload(floatBytes({3, -9999}), floatBytes({1, 2, -9999, -9999}), valid));
|
||||
ASSERT_TRUE(combined) << combined.message;
|
||||
EXPECT_TRUE(combined.snapshot->hasScalar());
|
||||
EXPECT_TRUE(combined.snapshot->hasVector());
|
||||
}
|
||||
|
||||
TEST(Source, ParticleFieldV3V2RejectsInvalidPlaneAndSemanticsContracts) {
|
||||
const std::vector<char> valid{1, 1};
|
||||
auto neither = particle::LocalParticleFieldFeed::load(v2Metadata(false, false), v2Payload(std::nullopt, std::nullopt, valid));
|
||||
EXPECT_FALSE(neither);
|
||||
auto scalarLength = particle::LocalParticleFieldFeed::load(v2Metadata(true, false), v2Payload(floatBytes({1}), std::nullopt, valid));
|
||||
EXPECT_FALSE(scalarLength);
|
||||
auto vectorLength = particle::LocalParticleFieldFeed::load(v2Metadata(false, true), v2Payload(std::nullopt, floatBytes({1, 2}), valid));
|
||||
EXPECT_FALSE(vectorLength);
|
||||
auto validityLength = particle::LocalParticleFieldFeed::load(v2Metadata(true, false), v2Payload(floatBytes({1, 2}), std::nullopt, std::vector<char>{1}));
|
||||
EXPECT_FALSE(validityLength);
|
||||
auto missingSemantics = v2Metadata(false, true);
|
||||
const auto position = missingSemantics.find(",\"semantics\":\"VELOCITY\"");
|
||||
ASSERT_NE(position, std::string::npos);
|
||||
missingSemantics.erase(position, std::string(",\"semantics\":\"VELOCITY\"").size());
|
||||
EXPECT_FALSE(particle::LocalParticleFieldFeed::load(missingSemantics, v2Payload(std::nullopt, floatBytes({1, 2, 3, 4}), valid)));
|
||||
auto directionWithoutVector = v2Metadata(true, false);
|
||||
directionWithoutVector.pop_back();
|
||||
directionWithoutVector += ",\"vector\":{\"semantics\":\"DIRECTION\"}}";
|
||||
EXPECT_FALSE(particle::LocalParticleFieldFeed::load(directionWithoutVector, v2Payload(floatBytes({1, 2}), std::nullopt, valid)));
|
||||
}
|
||||
|
||||
TEST(Source, ParticleFieldV3V2RejectsNonFiniteValidDataAndAcceptsInvalidFiniteNoData) {
|
||||
const auto nonFinite = particle::LocalParticleFieldFeed::load(
|
||||
v2Metadata(true, false), v2Payload(floatBytes({std::numeric_limits<float>::infinity(), 2}), std::nullopt, std::vector<char>{1, 1}));
|
||||
EXPECT_FALSE(nonFinite);
|
||||
const auto placeholder = particle::LocalParticleFieldFeed::load(
|
||||
v2Metadata(true, false), v2Payload(floatBytes({1, -9999}), std::nullopt, std::vector<char>{1, 0}));
|
||||
EXPECT_TRUE(placeholder) << placeholder.message;
|
||||
}
|
||||
|
||||
TEST(Source, ParticleFieldV3CompatibilityAndContentIdentityAreChannelAware) {
|
||||
const std::vector<char> valid{1, 1};
|
||||
auto scalar = *particle::LocalParticleFieldFeed::load(v2Metadata(true, false), v2Payload(floatBytes({1, 2}), std::nullopt, valid)).snapshot;
|
||||
auto scalarChanged = scalar; scalarChanged.scalar[0] = 3;
|
||||
auto validityChanged = scalar; validityChanged.valid[0] = 0;
|
||||
EXPECT_TRUE(scalar.isTimeInterpolationCompatibleWith(scalarChanged));
|
||||
EXPECT_FALSE(particle::sameFieldGpuContent(scalar, scalarChanged));
|
||||
EXPECT_FALSE(particle::sameFieldGpuContent(scalar, validityChanged));
|
||||
auto vector = *particle::LocalParticleFieldFeed::load(v2Metadata(false, true), v2Payload(std::nullopt, floatBytes({1, 2, 3, 4}), valid)).snapshot;
|
||||
auto direction = *particle::LocalParticleFieldFeed::load(v2Metadata(false, true, "DIRECTION"), v2Payload(std::nullopt, floatBytes({1, 2, 3, 4}), valid)).snapshot;
|
||||
auto vectorChanged = vector; vectorChanged.vector[0] = 9;
|
||||
EXPECT_FALSE(scalar.isTimeInterpolationCompatibleWith(vector));
|
||||
EXPECT_FALSE(vector.isTimeInterpolationCompatibleWith(direction));
|
||||
EXPECT_FALSE(particle::sameFieldGpuContent(vector, vectorChanged));
|
||||
}
|
||||
|
||||
TEST(Source, LocalProductionFeedSubmitsSnapshotsWithProducerTime) {
|
||||
const auto field = fixtureBytes("uniform-east", "field.f32");
|
||||
const std::vector<char> payload(field.begin(), field.end());
|
||||
@@ -369,17 +460,17 @@ TEST(Source, CanonicalFieldUsesEastwardUAndNorthwardV) {
|
||||
ASSERT_TRUE(vortexResult);
|
||||
ASSERT_TRUE(eastResult.snapshot);
|
||||
ASSERT_TRUE(northResult.snapshot);
|
||||
ASSERT_GE(eastResult.snapshot->velocity.size(), 2u);
|
||||
ASSERT_GE(northResult.snapshot->velocity.size(), 2u);
|
||||
EXPECT_FLOAT_EQ(eastResult.snapshot->velocity[0], 1.0f);
|
||||
EXPECT_FLOAT_EQ(eastResult.snapshot->velocity[1], 0.0f);
|
||||
EXPECT_FLOAT_EQ(northResult.snapshot->velocity[0], 0.0f);
|
||||
EXPECT_FLOAT_EQ(northResult.snapshot->velocity[1], 1.0f);
|
||||
ASSERT_GE(eastResult.snapshot->vector.size(), 2u);
|
||||
ASSERT_GE(northResult.snapshot->vector.size(), 2u);
|
||||
EXPECT_FLOAT_EQ(eastResult.snapshot->vector[0], 1.0f);
|
||||
EXPECT_FLOAT_EQ(eastResult.snapshot->vector[1], 0.0f);
|
||||
EXPECT_FLOAT_EQ(northResult.snapshot->vector[0], 0.0f);
|
||||
EXPECT_FLOAT_EQ(northResult.snapshot->vector[1], 1.0f);
|
||||
ASSERT_TRUE(vortexResult.snapshot);
|
||||
EXPECT_FLOAT_EQ(vortexResult.snapshot->velocity[0], 0.9375f);
|
||||
EXPECT_FLOAT_EQ(vortexResult.snapshot->velocity[1], -0.9375f);
|
||||
EXPECT_FLOAT_EQ(vortexResult.snapshot->velocity[vortexResult.snapshot->velocity.size() - 2], -0.9375f);
|
||||
EXPECT_FLOAT_EQ(vortexResult.snapshot->velocity.back(), 0.9375f);
|
||||
EXPECT_FLOAT_EQ(vortexResult.snapshot->vector[0], 0.9375f);
|
||||
EXPECT_FLOAT_EQ(vortexResult.snapshot->vector[1], -0.9375f);
|
||||
EXPECT_FLOAT_EQ(vortexResult.snapshot->vector[vortexResult.snapshot->vector.size() - 2], -0.9375f);
|
||||
EXPECT_FLOAT_EQ(vortexResult.snapshot->vector.back(), 0.9375f);
|
||||
}
|
||||
|
||||
TEST(Source, HostFieldSubmissionBuildsOneCoreOwnedTimeline) {
|
||||
@@ -707,7 +798,7 @@ TEST(Source, RasterArrayRuntimeFieldWindowAdvancesWithoutReplacingTheSource) {
|
||||
|
||||
EXPECT_EQ(source.updateRuntimeFieldWindow(vortex), style::RasterArrayRuntimeFieldUpdateResult::Noop);
|
||||
auto invalid = nodata;
|
||||
invalid.velocity.clear();
|
||||
invalid.vector.clear();
|
||||
EXPECT_EQ(source.updateRuntimeFieldWindow(invalid),
|
||||
style::RasterArrayRuntimeFieldUpdateResult::RejectedInvalidSnapshot);
|
||||
EXPECT_EQ(source.impl().field->fieldID, "uniform-north");
|
||||
|
||||
Reference in New Issue
Block a user