Files
MapLibreParticleLab/src/mbgl/particle/field/field_data_adapter.cpp
2026-09-10 22:15:44 +08:00

115 lines
8.3 KiB
C++

#include <mbgl/particle/field/field_data_adapter.hpp>
#include <algorithm>
#include <cmath>
#include <cstring>
#include <utility>
namespace mbgl::particle {
namespace {
ParticleFieldAdapterResult reject(ParticleFieldAdapterError error, std::string message) {
return {.snapshot = std::nullopt, .error = error, .message = std::move(message)};
}
bool validCommonDescriptor(const ParticleFieldDescriptor& d) {
return !d.fieldID.empty() && d.generation && d.validTime && d.width && d.height && d.west < d.east && d.south < d.north;
}
bool finitePlane(const std::vector<float>& values) {
return std::all_of(values.begin(), values.end(), [](float value) { return std::isfinite(value); });
}
bool isNoData(const std::optional<float>& noData, float value) { return noData && value == *noData; }
} // namespace
const char* particleFieldAdapterErrorName(ParticleFieldAdapterError error) {
switch (error) {
case ParticleFieldAdapterError::None: return "NONE";
case ParticleFieldAdapterError::InvalidDescriptor: return "INVALID_DESCRIPTOR";
case ParticleFieldAdapterError::UnsupportedFormat: return "UNSUPPORTED_FORMAT";
case ParticleFieldAdapterError::DimensionMismatch: return "DIMENSION_MISMATCH";
case ParticleFieldAdapterError::PayloadSizeMismatch: return "PAYLOAD_SIZE_MISMATCH";
case ParticleFieldAdapterError::InvalidTimestampOrIdentity: return "INVALID_TIMESTAMP_OR_IDENTITY";
case ParticleFieldAdapterError::NonFiniteData: return "NON_FINITE_DATA";
case ParticleFieldAdapterError::DecodeValidationFailure: return "DECODE_VALIDATION_FAILURE";
}
return "UNKNOWN";
}
bool ParticleFieldSnapshot::isValid() const {
const auto cells = static_cast<size_t>(width) * height;
if (fieldID.empty() || !generation || !validTime || !width || !height || !(west < east) || !(south < north) ||
valid.size() != cells || (!hasScalar() && !hasVector())) return false;
if (hasScalar() && scalar.size() != cells) return false;
if (hasVector() && (vector.size() != cells * 2 || !vectorSemantics)) return false;
return (!hasVector() ? !vectorSemantics : true) && finitePlane(scalar) && finitePlane(vector);
}
bool ParticleFieldSnapshot::isTimeInterpolationCompatibleWith(const ParticleFieldSnapshot& other) const {
return width == other.width && height == other.height && west == other.west && south == other.south && east == other.east &&
north == other.north && hasScalar() == other.hasScalar() && hasVector() == other.hasVector() &&
vectorSemantics == other.vectorSemantics;
}
ParticleFieldAdapterResult ParticleFieldDataAdapter::decode(const ParticleFieldDescriptor& d,
const ParticleFieldPayloadBundle& payloads) {
if (!validCommonDescriptor(d)) return reject(ParticleFieldAdapterError::InvalidTimestampOrIdentity, "field identity, time or grid is invalid");
const bool scalar = d.scalar.has_value(), vector = d.vector.has_value();
if ((!scalar && !vector) || scalar != payloads.scalar.has_value() || vector != payloads.vector.has_value() || !d.validity || !payloads.validity)
return reject(ParticleFieldAdapterError::InvalidDescriptor, "v2 channel declaration and payload presence differ");
if (vector != d.vectorSemantics.has_value())
return reject(ParticleFieldAdapterError::InvalidDescriptor, "vector semantics must be present exactly with vector");
const uint64_t cells = static_cast<uint64_t>(d.width) * d.height;
const auto planeMatches = [](const ParticleFieldPlaneDescriptor& p, const std::optional<std::vector<char>>& bytes,
uint64_t expected, const char* encoding, const char* layout) {
return p.byteLength == expected && bytes && bytes->size() == expected && p.encoding == encoding &&
(!layout[0] || p.layout == layout) && (!p.noData || std::isfinite(*p.noData));
};
if (scalar && !planeMatches(*d.scalar, payloads.scalar, cells * sizeof(float), "float32-le", ""))
return reject(ParticleFieldAdapterError::PayloadSizeMismatch, "scalar plane length or encoding is invalid");
if (vector && !planeMatches(*d.vector, payloads.vector, cells * 2 * sizeof(float), "float32-le", "interleaved-uv"))
return reject(ParticleFieldAdapterError::PayloadSizeMismatch, "vector plane length or encoding is invalid");
if (d.validity->byteLength != cells || payloads.validity->size() != cells || d.validity->encoding != "uint8")
return reject(ParticleFieldAdapterError::PayloadSizeMismatch, "validity plane length or encoding is invalid");
ParticleFieldSnapshot field;
field.fieldID = d.fieldID; field.fieldType = d.fieldType; field.displayDomain = d.displayDomain; field.validTime = d.validTime;
field.width = d.width; field.height = d.height; field.west = d.west; field.south = d.south; field.east = d.east; field.north = d.north;
field.dataVersion = d.dataVersion; field.generation = d.generation; field.fieldSha256 = d.fieldSha256; field.vectorSemantics = d.vectorSemantics;
field.valid.assign(payloads.validity->begin(), payloads.validity->end());
if (scalar) { field.scalar.resize(cells); std::memcpy(field.scalar.data(), payloads.scalar->data(), payloads.scalar->size()); }
if (vector) { field.vector.resize(cells * 2); std::memcpy(field.vector.data(), payloads.vector->data(), payloads.vector->size()); }
if (!finitePlane(field.scalar) || !finitePlane(field.vector)) return reject(ParticleFieldAdapterError::NonFiniteData, "field contains non-finite data");
for (size_t cell = 0; cell < field.valid.size(); ++cell) {
if (field.valid[cell] > 1) return reject(ParticleFieldAdapterError::DecodeValidationFailure, "validity values must be 0 or 1");
if (field.valid[cell]) {
if (scalar && isNoData(d.scalar->noData, field.scalar[cell])) return reject(ParticleFieldAdapterError::DecodeValidationFailure, "valid scalar cell contains noData");
if (vector && (isNoData(d.vector->noData, field.vector[cell * 2]) || isNoData(d.vector->noData, field.vector[cell * 2 + 1])))
return reject(ParticleFieldAdapterError::DecodeValidationFailure, "valid vector cell contains noData");
}
}
if (!field.isValid()) return reject(ParticleFieldAdapterError::DecodeValidationFailure, "decoded field is invalid");
return {.snapshot = std::move(field), .error = ParticleFieldAdapterError::None, .message = {}};
}
ParticleFieldAdapterResult ParticleFieldDataAdapter::decode(const ParticleFieldDescriptor& d, const std::vector<char>& bytes, uint64_t generation) {
if (d.layout != "interleaved-uv" || d.encoding != "float32-le")
return reject(ParticleFieldAdapterError::UnsupportedFormat, "legacy v1 only supports interleaved-uv float32-le");
if (!d.generation || !generation || d.generation != generation || (d.stride && d.stride != 2 * sizeof(float)) || !std::isfinite(d.noData))
return reject(ParticleFieldAdapterError::InvalidDescriptor, "legacy v1 descriptor is invalid");
ParticleFieldDescriptor normalized = d;
normalized.vector = ParticleFieldPlaneDescriptor{.byteLength = d.byteLength, .encoding = d.encoding, .layout = d.layout, .noData = d.noData};
normalized.vectorSemantics = VectorSemantics::Velocity;
normalized.validity = ParticleFieldPlaneDescriptor{.byteLength = static_cast<uint64_t>(d.width) * d.height,
.encoding = "uint8", .layout = {}, .noData = std::nullopt};
std::vector<uint8_t> valid(static_cast<size_t>(normalized.validity->byteLength));
if (bytes.size() == static_cast<size_t>(d.width) * d.height * 2 * sizeof(float)) {
for (size_t cell = 0; cell < valid.size(); ++cell) {
float pair[2]; std::memcpy(pair, bytes.data() + cell * sizeof(pair), sizeof(pair));
if (!std::isfinite(pair[0]) || !std::isfinite(pair[1])) return reject(ParticleFieldAdapterError::NonFiniteData, "legacy v1 field contains non-finite data");
valid[cell] = (pair[0] == d.noData || pair[1] == d.noData) ? 0 : 1;
}
}
return decode(normalized, ParticleFieldPayloadBundle{.scalar = std::nullopt,
.vector = bytes,
.validity = std::vector<char>(valid.begin(), valid.end())});
}
} // namespace mbgl::particle