diff --git a/libs/gridsource/include/gridsource/gridsource.h b/libs/gridsource/include/gridsource/gridsource.h index b4bf2660..d5f5b6e6 100644 --- a/libs/gridsource/include/gridsource/gridsource.h +++ b/libs/gridsource/include/gridsource/gridsource.h @@ -6,6 +6,7 @@ #include #include #include +#include #include #include #include @@ -49,6 +50,54 @@ struct AttributeLayerConfig; struct RelationConfig; struct GeometryConfig; +/** + * Attribute tree profile for procedural attribute generation + */ +enum class AttributeTreeProfile { + None, // No profile attributes (default, backward compatible) + Minimal, // ~5 leaf nodes, 1 layer, flat scalars + Moderate, // ~40 leaf nodes, 3 layers, shallow nesting + Realistic, // ~150 leaf nodes, 6 layers, nested objects with validity + Stress // ~1000+ leaf nodes, 12 layers, deep nesting +}; + +/** + * Parameters for attribute tree generation (overridable per-profile) + */ +struct AttributeTreeParams { + std::optional numLayers; + std::optional attrsPerLayer; + std::optional fieldsPerAttr; + std::optional maxNestingDepth; + std::optional maxArraySize; + std::optional nestingProbability; + std::optional directionalValidityProb; + std::optional rangeValidityProb; + std::optional topLevelExtraFields; + + static AttributeTreeParams fromYAML(const YAML::Node& node); +}; + +/** + * Resolved (fully concrete) tree params after merging profile + overrides + */ +struct ResolvedTreeParams { + int numLayers; + int attrsPerLayer; + int fieldsPerAttr; + int maxNestingDepth; + int maxArraySize; + double nestingProbability; + double directionalValidityProb; + double rangeValidityProb; + int topLevelExtraFields; + + static ResolvedTreeParams resolve( + AttributeTreeProfile profile, + const AttributeTreeParams& globalOverrides, + const AttributeTreeParams& layerOverrides); +}; + /** * Geometry type for generated features */ @@ -202,6 +251,10 @@ struct LayerConfig { std::vector layeredAttributes; std::vector relations; + // Per-layer attribute tree profile override (nullopt = use global) + std::optional attributeTreeProfile; + AttributeTreeParams attributeTreeParams; + static LayerConfig fromYAML(const YAML::Node& node); }; @@ -212,9 +265,19 @@ struct Config { std::string mapId = "GridDataSource"; bool spatialCoherence = true; double collisionGridSize = 10.0; + uint32_t sourceDownloadDelayMs = 0; // Sleep-wait (simulates IO: downloading from server) + uint32_t sourceUnpackDelayMs = 0; // Busy-wait (simulates CPU: decompression/parsing) + uint32_t sourceTransformDelayMs = 0; // Busy-wait (simulates CPU: conversion to features) + + // Attribute tree profile (global default) + AttributeTreeProfile attributeTreeProfile = AttributeTreeProfile::None; + AttributeTreeParams attributeTreeParams; + std::vector layers; static Config fromYAML(const YAML::Node& node); + nlohmann::json toJson() const; + static Config fromJson(const nlohmann::json& j); }; /** @@ -312,14 +375,30 @@ class GridDataSource : public mapget::DataSource } std::vector locate(mapget::LocateRequest const& req) override; + // Live config mutation (for dev UI) + void setConfig(gridsource::Config newConfig); + gridsource::Config getConfig() const; + void clearContextCache(); + + // Static instance registry (for dev UI REST API) + static void registerInstance(std::shared_ptr instance); + static std::vector> getInstances(); + private: - gridsource::Config config_; + std::shared_ptr config_; + mutable std::mutex configMutex_; mutable std::mutex contextMutex_; mutable std::unordered_map> contextCache_; static constexpr size_t MAX_CACHED_CONTEXTS = 1000; + // Static registry + static std::mutex registryMutex_; + static std::vector> registry_; + // Get or create spatial context for a tile - std::shared_ptr getOrCreateContext(mapget::TileId tileId) const; + std::shared_ptr getOrCreateContext( + mapget::TileId tileId, + std::shared_ptr const& cfg) const; // Layer generation methods void generateRoadGrid(gridsource::TileSpatialContext& ctx, @@ -327,15 +406,18 @@ class GridDataSource : public mapget::DataSource mapget::TileFeatureLayer::Ptr const& tile); void generateBuildings(gridsource::TileSpatialContext& ctx, - const gridsource::LayerConfig& config, + const gridsource::LayerConfig& layerCfg, + const gridsource::Config& cfg, mapget::TileFeatureLayer::Ptr const& tile); void generateRoads(gridsource::TileSpatialContext& ctx, - const gridsource::LayerConfig& config, + const gridsource::LayerConfig& layerCfg, + const gridsource::Config& cfg, mapget::TileFeatureLayer::Ptr const& tile); void generateIntersections(gridsource::TileSpatialContext& ctx, - const gridsource::LayerConfig& config, + const gridsource::LayerConfig& layerCfg, + const gridsource::Config& cfg, mapget::TileFeatureLayer::Ptr const& tile); // Attribute generation @@ -349,6 +431,13 @@ class GridDataSource : public mapget::DataSource std::mt19937& gen, uint32_t featureId); + // Profile-based attribute tree generation + void generateProfileAttributes(mapget::model_ptr feature, + mapget::TileFeatureLayer::Ptr const& tile, + const gridsource::ResolvedTreeParams& params, + std::mt19937& gen, + uint32_t featureId); + // Relation generation void generateRelations(mapget::model_ptr feature, const gridsource::TileSpatialContext& ctx, diff --git a/libs/gridsource/src/gridsource.cpp b/libs/gridsource/src/gridsource.cpp index a2de04fc..70207b0e 100644 --- a/libs/gridsource/src/gridsource.cpp +++ b/libs/gridsource/src/gridsource.cpp @@ -4,8 +4,10 @@ #include #include +#include #include #include +#include #include "mapget/log.h" #include "fmt/format.h" @@ -33,6 +35,84 @@ std::string replaceAll(std::string str, const std::string& from, const std::stri return str; } +// ============================================================================ +// Attribute Tree Profile: Name & Value Pools +// ============================================================================ + +static constexpr const char* kProfileLayerNames[] = { + "SpeedRestrictions", "AccessControl", "RoadCharacteristics", + "TrafficSigns", "LaneModel", "RoadGeometry", + "RoutingAttributes", "EnvironmentalZones", "TollStructures", + "ParkingFacilities", "ElectricVehicle", "NavigationAttributes" +}; +static constexpr size_t kNumProfileLayerNames = 12; + +static constexpr const char* kProfileAttrNames[] = { + "speedLimit", "overtakingRestriction", "accessPermission", + "vehicleClassRestriction", "surfaceType", "pavementCondition", + "numberOfLanes", "laneWidth", "roadWidth", + "functionalRoadClass", "formOfWay", "gradientPercent", + "curvatureRadius", "trafficFlowDirection", "dividerType", + "environmentalZone", "tollCategory", "heightRestriction", + "weightRestriction", "hazmatRestriction" +}; +static constexpr size_t kNumProfileAttrNames = 20; + +static constexpr const char* kProfileFieldNames[] = { + "value", "unit", "condition", "timeOfDay", + "vehicleType", "source", "confidence", "validFrom", + "validTo", "applicableDays", "weatherCondition", "seasonality", + "priority", "overrideLevel", "verificationDate", "dataQuality", + "measurementMethod", "referenceStandard", "toleranceRange", "exceptionalCase", + "alternateValue", "displayUnit", "rawValue", "encodingVersion" +}; +static constexpr size_t kNumProfileFieldNames = 24; + +static constexpr const char* kProfileStringValues[] = { + "concrete", "asphalt", "gravel", "cobblestone", + "motorway", "primary", "secondary", "residential", + "permitted", "forbidden", "restricted", "conditional" +}; +static constexpr size_t kNumProfileStringValues = 12; + +static constexpr int kProfileIntValues[] = { + 0, 10, 20, 30, 50, 60, 80, 100, 120, 130, 150, 200 +}; +static constexpr size_t kNumProfileIntValues = 12; + +static constexpr double kProfileFloatValues[] = { + 0.0, 0.5, 1.0, 1.5, 2.0, 2.5, 3.0, 3.5, 4.0, 5.0, 7.5, 10.0 +}; +static constexpr size_t kNumProfileFloatValues = 12; + +// Deterministic hash for pool selection +inline uint32_t profileHash(uint32_t seed, uint32_t a, uint32_t b, uint32_t c) { + uint32_t h = seed; + h ^= a * 2654435761u; + h ^= b * 2246822519u; + h ^= c * 3266489917u; + h ^= h >> 16; + h *= 0x45d9f3b; + h ^= h >> 16; + return h; +} + +// Profile string-to-enum mapping +static const std::map kProfileMap = { + {"none", AttributeTreeProfile::None}, + {"minimal", AttributeTreeProfile::Minimal}, + {"moderate", AttributeTreeProfile::Moderate}, + {"realistic", AttributeTreeProfile::Realistic}, + {"stress", AttributeTreeProfile::Stress} +}; + +std::string profileToString(AttributeTreeProfile p) { + for (auto& [k, v] : kProfileMap) { + if (v == p) return k; + } + return "none"; +} + } // anonymous namespace // ============================================================================ @@ -224,9 +304,72 @@ LayerConfig LayerConfig::fromYAML(const YAML::Node& node) { } } + // Per-layer attribute tree profile override + if (node["attributeTreeProfile"]) { + auto profileStr = node["attributeTreeProfile"].as("none"); + cfg.attributeTreeProfile = parseEnum(profileStr, kProfileMap, AttributeTreeProfile::None); + } + if (node["attributeTreeParams"]) { + cfg.attributeTreeParams = AttributeTreeParams::fromYAML(node["attributeTreeParams"]); + } + return cfg; } +AttributeTreeParams AttributeTreeParams::fromYAML(const YAML::Node& node) { + AttributeTreeParams p; + if (!node) return p; + if (node["numLayers"]) p.numLayers = node["numLayers"].as(); + if (node["attrsPerLayer"]) p.attrsPerLayer = node["attrsPerLayer"].as(); + if (node["fieldsPerAttr"]) p.fieldsPerAttr = node["fieldsPerAttr"].as(); + if (node["maxNestingDepth"]) p.maxNestingDepth = node["maxNestingDepth"].as(); + if (node["maxArraySize"]) p.maxArraySize = node["maxArraySize"].as(); + if (node["nestingProbability"]) p.nestingProbability = node["nestingProbability"].as(); + if (node["directionalValidityProb"]) p.directionalValidityProb = node["directionalValidityProb"].as(); + if (node["rangeValidityProb"]) p.rangeValidityProb = node["rangeValidityProb"].as(); + if (node["topLevelExtraFields"]) p.topLevelExtraFields = node["topLevelExtraFields"].as(); + return p; +} + +ResolvedTreeParams ResolvedTreeParams::resolve( + AttributeTreeProfile profile, + const AttributeTreeParams& global, + const AttributeTreeParams& layer) +{ + // Profile base values + // minimal moderate realistic stress + struct Preset { int nL; int aPL; int fPA; int mND; int mAS; double nP; double dVP; double rVP; int tLEF; }; + static const std::map presets = { + {AttributeTreeProfile::Minimal, { 1, 2, 2, 0, 0, 0.0, 0.0, 0.0, 1}}, + {AttributeTreeProfile::Moderate, { 3, 4, 3, 1, 0, 0.15, 0.2, 0.0, 3}}, + {AttributeTreeProfile::Realistic, { 6, 5, 4, 2, 3, 0.25, 0.5, 0.3, 5}}, + {AttributeTreeProfile::Stress, {12, 10, 8, 4, 6, 0.4, 0.8, 0.5, 10}}, + }; + + auto it = presets.find(profile); + Preset base = (it != presets.end()) ? it->second : Preset{1, 2, 2, 0, 0, 0.0, 0.0, 0.0, 1}; + + // Apply overrides: layer > global > preset + auto pick = [](std::optional layerV, std::optional globalV, int preset) { + return layerV.value_or(globalV.value_or(preset)); + }; + auto pickD = [](std::optional layerV, std::optional globalV, double preset) { + return layerV.value_or(globalV.value_or(preset)); + }; + + ResolvedTreeParams r; + r.numLayers = pick(layer.numLayers, global.numLayers, base.nL); + r.attrsPerLayer = pick(layer.attrsPerLayer, global.attrsPerLayer, base.aPL); + r.fieldsPerAttr = pick(layer.fieldsPerAttr, global.fieldsPerAttr, base.fPA); + r.maxNestingDepth = pick(layer.maxNestingDepth, global.maxNestingDepth, base.mND); + r.maxArraySize = pick(layer.maxArraySize, global.maxArraySize, base.mAS); + r.nestingProbability = pickD(layer.nestingProbability, global.nestingProbability, base.nP); + r.directionalValidityProb = pickD(layer.directionalValidityProb, global.directionalValidityProb, base.dVP); + r.rangeValidityProb = pickD(layer.rangeValidityProb, global.rangeValidityProb, base.rVP); + r.topLevelExtraFields = pick(layer.topLevelExtraFields, global.topLevelExtraFields, base.tLEF); + return r; +} + Config Config::fromYAML(const YAML::Node& node) { Config cfg; if (!node) return cfg; @@ -235,6 +378,33 @@ Config Config::fromYAML(const YAML::Node& node) { cfg.spatialCoherence = node["spatialCoherence"].as(true); cfg.collisionGridSize = node["collisionGridSize"].as(10.0); + // New delay fields + cfg.sourceDownloadDelayMs = node["sourceDownloadDelayMs"].as(0); + cfg.sourceUnpackDelayMs = node["sourceUnpackDelayMs"].as(0); + cfg.sourceTransformDelayMs = node["sourceTransformDelayMs"].as(0); + + // Legacy fallback: map old delayMs/delayMode to new fields + if (node["delayMs"] && !node["sourceDownloadDelayMs"] + && !node["sourceUnpackDelayMs"] && !node["sourceTransformDelayMs"]) + { + auto legacyDelay = node["delayMs"].as(0); + std::string delayMode = node["delayMode"].as("sleep"); + if (delayMode == "busyWait") + cfg.sourceTransformDelayMs = legacyDelay; + else + cfg.sourceDownloadDelayMs = legacyDelay; + } + + // Attribute tree profile + if (node["attributeTreeProfile"]) { + cfg.attributeTreeProfile = parseEnum( + node["attributeTreeProfile"].as("none"), + kProfileMap, AttributeTreeProfile::None); + } + if (node["attributeTreeParams"]) { + cfg.attributeTreeParams = AttributeTreeParams::fromYAML(node["attributeTreeParams"]); + } + if (node["layers"]) { for (const auto& layer : node["layers"]) { auto layerCfg = LayerConfig::fromYAML(layer); @@ -247,6 +417,110 @@ Config Config::fromYAML(const YAML::Node& node) { return cfg; } +nlohmann::json Config::toJson() const { + nlohmann::json j; + j["mapId"] = mapId; + j["spatialCoherence"] = spatialCoherence; + j["collisionGridSize"] = collisionGridSize; + j["sourceDownloadDelayMs"] = sourceDownloadDelayMs; + j["sourceUnpackDelayMs"] = sourceUnpackDelayMs; + j["sourceTransformDelayMs"] = sourceTransformDelayMs; + j["attributeTreeProfile"] = profileToString(attributeTreeProfile); + + nlohmann::json atp; + if (attributeTreeParams.numLayers) atp["numLayers"] = *attributeTreeParams.numLayers; + if (attributeTreeParams.attrsPerLayer) atp["attrsPerLayer"] = *attributeTreeParams.attrsPerLayer; + if (attributeTreeParams.fieldsPerAttr) atp["fieldsPerAttr"] = *attributeTreeParams.fieldsPerAttr; + if (attributeTreeParams.maxNestingDepth) atp["maxNestingDepth"] = *attributeTreeParams.maxNestingDepth; + if (attributeTreeParams.maxArraySize) atp["maxArraySize"] = *attributeTreeParams.maxArraySize; + if (attributeTreeParams.nestingProbability) atp["nestingProbability"] = *attributeTreeParams.nestingProbability; + if (attributeTreeParams.directionalValidityProb) atp["directionalValidityProb"] = *attributeTreeParams.directionalValidityProb; + if (attributeTreeParams.rangeValidityProb) atp["rangeValidityProb"] = *attributeTreeParams.rangeValidityProb; + if (attributeTreeParams.topLevelExtraFields) atp["topLevelExtraFields"] = *attributeTreeParams.topLevelExtraFields; + j["attributeTreeParams"] = atp; + + j["layers"] = nlohmann::json::array(); + for (const auto& layer : layers) { + nlohmann::json lj; + lj["name"] = layer.name; + lj["enabled"] = layer.enabled; + lj["featureType"] = layer.featureType; + lj["geometry"] = nlohmann::json{ + {"type", layer.geometry.type == GeometryType::Point ? "point" : + layer.geometry.type == GeometryType::Line ? "line" : + layer.geometry.type == GeometryType::Polygon ? "polygon" : "mesh"}, + {"density", layer.geometry.density}, + {"complexity", layer.geometry.complexity}, + {"curvature", layer.geometry.curvature}, + {"sizeRange", {layer.geometry.sizeRange[0], layer.geometry.sizeRange[1]}}, + {"aspectRatio", {layer.geometry.aspectRatio[0], layer.geometry.aspectRatio[1]}}, + {"avoidBuildings", layer.geometry.avoidBuildings}, + {"minBuildingDistance", layer.geometry.minBuildingDistance} + }; + if (layer.attributeTreeProfile) { + lj["attributeTreeProfile"] = profileToString(*layer.attributeTreeProfile); + } + j["layers"].push_back(lj); + } + return j; +} + +Config Config::fromJson(const nlohmann::json& j) { + Config cfg; + if (j.contains("mapId")) cfg.mapId = j["mapId"].get(); + if (j.contains("spatialCoherence")) cfg.spatialCoherence = j["spatialCoherence"].get(); + if (j.contains("collisionGridSize")) cfg.collisionGridSize = j["collisionGridSize"].get(); + if (j.contains("sourceDownloadDelayMs")) cfg.sourceDownloadDelayMs = j["sourceDownloadDelayMs"].get(); + if (j.contains("sourceUnpackDelayMs")) cfg.sourceUnpackDelayMs = j["sourceUnpackDelayMs"].get(); + if (j.contains("sourceTransformDelayMs")) cfg.sourceTransformDelayMs = j["sourceTransformDelayMs"].get(); + if (j.contains("attributeTreeProfile")) { + auto profileStr = j["attributeTreeProfile"].get(); + auto it = kProfileMap.find(profileStr); + cfg.attributeTreeProfile = (it != kProfileMap.end()) ? it->second : AttributeTreeProfile::None; + } + if (j.contains("attributeTreeParams")) { + auto& atp = j["attributeTreeParams"]; + if (atp.contains("numLayers")) cfg.attributeTreeParams.numLayers = atp["numLayers"].get(); + if (atp.contains("attrsPerLayer")) cfg.attributeTreeParams.attrsPerLayer = atp["attrsPerLayer"].get(); + if (atp.contains("fieldsPerAttr")) cfg.attributeTreeParams.fieldsPerAttr = atp["fieldsPerAttr"].get(); + if (atp.contains("maxNestingDepth")) cfg.attributeTreeParams.maxNestingDepth = atp["maxNestingDepth"].get(); + if (atp.contains("maxArraySize")) cfg.attributeTreeParams.maxArraySize = atp["maxArraySize"].get(); + if (atp.contains("nestingProbability")) cfg.attributeTreeParams.nestingProbability = atp["nestingProbability"].get(); + if (atp.contains("directionalValidityProb")) cfg.attributeTreeParams.directionalValidityProb = atp["directionalValidityProb"].get(); + if (atp.contains("rangeValidityProb")) cfg.attributeTreeParams.rangeValidityProb = atp["rangeValidityProb"].get(); + if (atp.contains("topLevelExtraFields")) cfg.attributeTreeParams.topLevelExtraFields = atp["topLevelExtraFields"].get(); + } + if (j.contains("layers")) { + for (const auto& lj : j["layers"]) { + LayerConfig layer; + if (lj.contains("name")) layer.name = lj["name"].get(); + if (lj.contains("enabled")) layer.enabled = lj["enabled"].get(); + if (lj.contains("featureType")) layer.featureType = lj["featureType"].get(); + if (lj.contains("geometry")) { + auto& gj = lj["geometry"]; + if (gj.contains("type")) { + static const std::map gmap = { + {"point", GeometryType::Point}, {"line", GeometryType::Line}, + {"polygon", GeometryType::Polygon}, {"mesh", GeometryType::Mesh} + }; + auto it = gmap.find(gj["type"].get()); + if (it != gmap.end()) layer.geometry.type = it->second; + } + if (gj.contains("density")) layer.geometry.density = gj["density"].get(); + if (gj.contains("complexity")) layer.geometry.complexity = gj["complexity"].get(); + if (gj.contains("curvature")) layer.geometry.curvature = gj["curvature"].get(); + } + if (lj.contains("attributeTreeProfile")) { + auto profileStr = lj["attributeTreeProfile"].get(); + auto it = kProfileMap.find(profileStr); + layer.attributeTreeProfile = (it != kProfileMap.end()) ? it->second : AttributeTreeProfile::None; + } + cfg.layers.push_back(layer); + } + } + return cfg; +} + // ============================================================================ // TileSpatialContext Implementation // ============================================================================ @@ -413,14 +687,62 @@ uint32_t TileSpatialContext::findRoadAtPoint(Point p, double tolerance) const { // GridDataSource Implementation // ============================================================================ +// Static registry members +std::mutex GridDataSource::registryMutex_; +std::vector> GridDataSource::registry_; + +void GridDataSource::registerInstance(std::shared_ptr instance) { + std::lock_guard lock(registryMutex_); + // Prune expired entries + registry_.erase( + std::remove_if(registry_.begin(), registry_.end(), + [](const auto& wp) { return wp.expired(); }), + registry_.end()); + registry_.push_back(instance); +} + +std::vector> GridDataSource::getInstances() { + std::lock_guard lock(registryMutex_); + std::vector> result; + auto it = registry_.begin(); + while (it != registry_.end()) { + if (auto sp = it->lock()) { + result.push_back(sp); + ++it; + } else { + it = registry_.erase(it); + } + } + return result; +} + +void GridDataSource::setConfig(gridsource::Config newConfig) { + { + std::lock_guard lock(configMutex_); + config_ = std::make_shared(std::move(newConfig)); + } + clearContextCache(); +} + +gridsource::Config GridDataSource::getConfig() const { + std::lock_guard lock(configMutex_); + return *config_; +} + +void GridDataSource::clearContextCache() { + std::lock_guard lock(contextMutex_); + contextCache_.clear(); +} + GridDataSource::GridDataSource(const YAML::Node& config) { + auto cfg = std::make_shared(); if (config && config.IsMap()) { - config_ = Config::fromYAML(config); + *cfg = Config::fromYAML(config); } else { // Default configuration with roads and buildings - config_.mapId = "GridDataSource"; - config_.spatialCoherence = true; - config_.collisionGridSize = 10.0; + cfg->mapId = "GridDataSource"; + cfg->spatialCoherence = true; + cfg->collisionGridSize = 10.0; // Default building layer LayerConfig buildingLayer; @@ -432,7 +754,7 @@ GridDataSource::GridDataSource(const YAML::Node& config) { buildingLayer.geometry.complexity = 4; buildingLayer.geometry.sizeRange = {15.0, 50.0}; buildingLayer.geometry.aspectRatio = {1.2, 3.0}; - config_.layers.push_back(buildingLayer); + cfg->layers.push_back(buildingLayer); // Default road layer LayerConfig roadLayer; @@ -445,7 +767,7 @@ GridDataSource::GridDataSource(const YAML::Node& config) { roadLayer.geometry.curvature = 0.08; roadLayer.geometry.avoidBuildings = true; roadLayer.geometry.minBuildingDistance = 2.0; - config_.layers.push_back(roadLayer); + cfg->layers.push_back(roadLayer); // Default intersection layer LayerConfig intersectionLayer; @@ -453,25 +775,31 @@ GridDataSource::GridDataSource(const YAML::Node& config) { intersectionLayer.enabled = true; intersectionLayer.featureType = "DevSrc-Intersection"; intersectionLayer.geometry.type = GeometryType::Point; - config_.layers.push_back(intersectionLayer); + cfg->layers.push_back(intersectionLayer); } + config_ = std::move(cfg); } DataSourceInfo GridDataSource::info() { + auto cfg = [&] { + std::lock_guard lock(configMutex_); + return config_; + }(); + nlohmann::json info; - info["mapId"] = config_.mapId; + info["mapId"] = cfg->mapId; info["layers"] = nlohmann::json::object(); - mapget::log().info("GridDataSource registering {} layers", config_.layers.size()); + mapget::log().info("GridDataSource registering {} layers", cfg->layers.size()); // Collect all unique feature types across all layers std::set allFeatureTypes; - for (const auto& layer : config_.layers) { + for (const auto& layer : cfg->layers) { allFeatureTypes.insert(layer.featureType); } // Register each layer with ALL feature types (for cross-layer relations) - for (const auto& layer : config_.layers) { + for (const auto& layer : cfg->layers) { nlohmann::json layerInfo; layerInfo["featureTypes"] = nlohmann::json::array(); @@ -505,7 +833,10 @@ DataSourceInfo GridDataSource::info() { return DataSourceInfo::fromJson(info); } -std::shared_ptr GridDataSource::getOrCreateContext(TileId tileId) const { +std::shared_ptr GridDataSource::getOrCreateContext( + TileId tileId, + std::shared_ptr const& cfg) const +{ try { std::lock_guard lock(contextMutex_); @@ -515,51 +846,89 @@ std::shared_ptr GridDataSource::getOrCreateContext(TileId ti } // Create new context - auto ctx = std::make_shared(tileId, config_.collisionGridSize); + auto ctx = std::make_shared(tileId, cfg->collisionGridSize); // LRU eviction if cache is full if (contextCache_.size() >= MAX_CACHED_CONTEXTS) { - // Simple FIFO for now (could be improved with proper LRU) contextCache_.erase(contextCache_.begin()); } contextCache_[tileId] = ctx; return ctx; } catch (const std::system_error&) { - // Handle mutex errors during shutdown - return empty context - return std::make_shared(tileId, config_.collisionGridSize); + return std::make_shared(tileId, cfg->collisionGridSize); } } void GridDataSource::fill(TileFeatureLayer::Ptr const& tile) { + // Snapshot config at entry for consistent use throughout fill() + auto cfg = [&] { + std::lock_guard lock(configMutex_); + return config_; + }(); + std::string layerName = tile->layerInfo()->layerId_; mapget::log().info("GridDataSource::fill() called for layer '{}' tile {}", layerName, tile->tileId().value_); + // Phase 1: Simulated source download (sleep-wait, IO) + if (cfg->sourceDownloadDelayMs > 0) { + auto t0 = std::chrono::steady_clock::now(); + std::this_thread::sleep_for(std::chrono::milliseconds(cfg->sourceDownloadDelayMs)); + auto elapsed = std::chrono::duration_cast( + std::chrono::steady_clock::now() - t0).count(); + tile->setInfo("source-download-ms", elapsed); + } + + // Phase 2: Simulated source unpack (busy-wait, CPU) + if (cfg->sourceUnpackDelayMs > 0) { + auto t0 = std::chrono::steady_clock::now(); + auto deadline = t0 + std::chrono::milliseconds(cfg->sourceUnpackDelayMs); + while (std::chrono::steady_clock::now() < deadline) {} + auto elapsed = std::chrono::duration_cast( + std::chrono::steady_clock::now() - t0).count(); + tile->setInfo("source-unpack-ms", elapsed); + } + // Get or create spatial context for this tile - auto ctx = getOrCreateContext(tile->tileId()); + auto ctx = getOrCreateContext(tile->tileId(), cfg); // Set ID prefix tile->setIdPrefix({{"tileId", static_cast(tile->tileId().value_)}}); - // Find matching layer configuration - for (const auto& layerCfg : config_.layers) { + // Phase 3: Feature generation (actual work) + auto genStart = std::chrono::steady_clock::now(); + for (const auto& layerCfg : cfg->layers) { if (layerCfg.name == layerName) { mapget::log().info(" Found matching layer config, geometry type: {}", static_cast(layerCfg.geometry.type)); - // Generate features based on geometry type if (layerCfg.geometry.type == GeometryType::Polygon || layerCfg.geometry.type == GeometryType::Mesh) { mapget::log().info(" Generating buildings..."); - generateBuildings(*ctx, layerCfg, tile); + generateBuildings(*ctx, layerCfg, *cfg, tile); mapget::log().info(" Generated {} buildings", ctx->buildings.size()); } else if (layerCfg.geometry.type == GeometryType::Line) { mapget::log().info(" Generating roads..."); - generateRoads(*ctx, layerCfg, tile); + generateRoads(*ctx, layerCfg, *cfg, tile); mapget::log().info(" Generated {} roads", ctx->roads.size()); } else if (layerCfg.geometry.type == GeometryType::Point) { mapget::log().info(" Generating intersections..."); - generateIntersections(*ctx, layerCfg, tile); + generateIntersections(*ctx, layerCfg, *cfg, tile); mapget::log().info(" Generated {} intersections", ctx->intersections.size()); } + + auto genElapsed = std::chrono::duration_cast( + std::chrono::steady_clock::now() - genStart).count(); + tile->setInfo("feature-gen-ms", genElapsed); + + // Phase 4: Simulated source transform (busy-wait, CPU) + if (cfg->sourceTransformDelayMs > 0) { + auto t0 = std::chrono::steady_clock::now(); + auto deadline = t0 + std::chrono::milliseconds(cfg->sourceTransformDelayMs); + while (std::chrono::steady_clock::now() < deadline) {} + auto elapsed = std::chrono::duration_cast( + std::chrono::steady_clock::now() - t0).count(); + tile->setInfo("source-transform-ms", elapsed); + } + return; } } @@ -567,6 +936,11 @@ void GridDataSource::fill(TileFeatureLayer::Ptr const& tile) { } std::vector GridDataSource::locate(const LocateRequest& req) { + auto cfg = [&] { + std::lock_guard lock(configMutex_); + return config_; + }(); + // Extract tileId from the feature ID parts std::optional tileId = req.getIntIdPart("tileId"); if (!tileId) { @@ -576,7 +950,7 @@ std::vector GridDataSource::locate(const LocateRequest& req) { // Find the layer that contains this feature type std::string layerId; - for (const auto& layer : config_.layers) { + for (const auto& layer : cfg->layers) { if (layer.featureType == req.typeId_) { layerId = layer.name; break; @@ -606,17 +980,18 @@ std::vector GridDataSource::locate(const LocateRequest& req) { } void GridDataSource::generateBuildings(TileSpatialContext& ctx, - const LayerConfig& config, + const LayerConfig& layerCfg, + const gridsource::Config& cfg, TileFeatureLayer::Ptr const& tile) { // Lazily generate road grid first (ensures roads are always generated before buildings) - generateRoadGrid(ctx, config, tile); + generateRoadGrid(ctx, layerCfg, tile); // Only generate buildings once for this tile if (!ctx.buildings.empty()) { // Buildings already generated, just recreate features for (const auto& building : ctx.buildings) { - auto feature = tile->newFeature(config.featureType, - {{config.featureType + "Id", building.id}}); + auto feature = tile->newFeature(layerCfg.featureType, + {{layerCfg.featureType + "Id", building.id}}); // Create axis-aligned rectangle as mesh (two triangles) feature->addMesh({ @@ -632,8 +1007,15 @@ void GridDataSource::generateBuildings(TileSpatialContext& ctx, // Generate attributes std::mt19937 gen(ctx.seed + building.id); - generateAttributes(feature, config.topAttributes, gen, building.id); - generateLayeredAttributes(feature, config.layeredAttributes, gen, building.id); + generateAttributes(feature, layerCfg.topAttributes, gen, building.id); + generateLayeredAttributes(feature, layerCfg.layeredAttributes, gen, building.id); + + // Profile-based attribute tree generation + auto profile = layerCfg.attributeTreeProfile.value_or(cfg.attributeTreeProfile); + if (profile != AttributeTreeProfile::None) { + auto params = ResolvedTreeParams::resolve(profile, cfg.attributeTreeParams, layerCfg.attributeTreeParams); + generateProfileAttributes(feature, tile, params, gen, building.id); + } } return; } @@ -657,8 +1039,8 @@ void GridDataSource::generateBuildings(TileSpatialContext& ctx, const double gap = gapMeters / metersPerDegree; std::mt19937 gen(ctx.seed + 1000); - std::uniform_real_distribution<> sizeDist(config.geometry.sizeRange[0], config.geometry.sizeRange[1]); - std::uniform_real_distribution<> aspectDist(config.geometry.aspectRatio[0], config.geometry.aspectRatio[1]); + std::uniform_real_distribution<> sizeDist(layerCfg.geometry.sizeRange[0], layerCfg.geometry.sizeRange[1]); + std::uniform_real_distribution<> aspectDist(layerCfg.geometry.aspectRatio[0], layerCfg.geometry.aspectRatio[1]); uint32_t buildingId = 100; int totalBuildings = 0; @@ -709,8 +1091,8 @@ void GridDataSource::generateBuildings(TileSpatialContext& ctx, totalBuildings++; // Create feature - auto feature = tile->newFeature(config.featureType, - {{config.featureType + "Id", building.id}}); + auto feature = tile->newFeature(layerCfg.featureType, + {{layerCfg.featureType + "Id", building.id}}); feature->addMesh({ Point(building.minX, building.minY, 0.0), @@ -725,13 +1107,20 @@ void GridDataSource::generateBuildings(TileSpatialContext& ctx, // Generate attributes std::mt19937 attrGen(ctx.seed + building.id); - generateAttributes(feature, config.topAttributes, attrGen, building.id); - generateLayeredAttributes(feature, config.layeredAttributes, attrGen, building.id); + generateAttributes(feature, layerCfg.topAttributes, attrGen, building.id); + generateLayeredAttributes(feature, layerCfg.layeredAttributes, attrGen, building.id); + + // Profile-based attribute tree generation + auto profile = layerCfg.attributeTreeProfile.value_or(cfg.attributeTreeProfile); + if (profile != AttributeTreeProfile::None) { + auto params = ResolvedTreeParams::resolve(profile, cfg.attributeTreeParams, layerCfg.attributeTreeParams); + generateProfileAttributes(feature, tile, params, attrGen, building.id); + } // Generate relations Point buildingCenter((building.minX + building.maxX) / 2.0, (building.minY + building.maxY) / 2.0, 0.0); - generateRelations(feature, ctx, config.relations, buildingCenter); + generateRelations(feature, ctx, layerCfg.relations, buildingCenter); // Move to next column col_x += buildingWidth + gap; @@ -875,10 +1264,11 @@ void GridDataSource::generateRoadGrid(TileSpatialContext& ctx, } void GridDataSource::generateRoads(TileSpatialContext& ctx, - const LayerConfig& config, + const LayerConfig& layerCfg, + const gridsource::Config& cfg, TileFeatureLayer::Ptr const& tile) { // Lazily generate road grid structure first - generateRoadGrid(ctx, config, tile); + generateRoadGrid(ctx, layerCfg, tile); // Create road features (only if not already done) if (ctx.roads.empty()) { @@ -886,12 +1276,12 @@ void GridDataSource::generateRoads(TileSpatialContext& ctx, return; } - mapget::log().info(" Creating {} road features with type '{}'", ctx.roads.size(), config.featureType); + mapget::log().info(" Creating {} road features with type '{}'", ctx.roads.size(), layerCfg.featureType); // Recreate features for all roads for (const auto& road : ctx.roads) { - auto feature = tile->newFeature(config.featureType, - {{config.featureType + "Id", road.id}}); + auto feature = tile->newFeature(layerCfg.featureType, + {{layerCfg.featureType + "Id", road.id}}); // Create straight line (no jitter for grid roads) auto line = feature->geom()->newGeometry(GeomType::Line, 2); @@ -919,21 +1309,29 @@ void GridDataSource::generateRoads(TileSpatialContext& ctx, // Generate attributes std::mt19937 gen(ctx.seed + road.id); - generateAttributes(feature, config.topAttributes, gen, road.id); - generateLayeredAttributes(feature, config.layeredAttributes, gen, road.id); + generateAttributes(feature, layerCfg.topAttributes, gen, road.id); + generateLayeredAttributes(feature, layerCfg.layeredAttributes, gen, road.id); + + // Profile-based attribute tree generation + auto profile = layerCfg.attributeTreeProfile.value_or(cfg.attributeTreeProfile); + if (profile != AttributeTreeProfile::None) { + auto params = ResolvedTreeParams::resolve(profile, cfg.attributeTreeParams, layerCfg.attributeTreeParams); + generateProfileAttributes(feature, tile, params, gen, road.id); + } // Generate relations Point roadMidpoint((road.start.x + road.end.x) / 2.0, (road.start.y + road.end.y) / 2.0, 0.0); - generateRelations(feature, ctx, config.relations, roadMidpoint); + generateRelations(feature, ctx, layerCfg.relations, roadMidpoint); } } void GridDataSource::generateIntersections(TileSpatialContext& ctx, - const LayerConfig& config, + const LayerConfig& layerCfg, + const gridsource::Config& cfg, TileFeatureLayer::Ptr const& tile) { // Lazily generate road grid first (which creates intersections) - generateRoadGrid(ctx, config, tile); + generateRoadGrid(ctx, layerCfg, tile); if (ctx.intersections.empty()) { mapget::log().warn(" No intersections to generate"); @@ -942,8 +1340,8 @@ void GridDataSource::generateIntersections(TileSpatialContext& ctx, // Create intersection features for (const auto& intersection : ctx.intersections) { - auto feature = tile->newFeature(config.featureType, - {{config.featureType + "Id", intersection.id}}); + auto feature = tile->newFeature(layerCfg.featureType, + {{layerCfg.featureType + "Id", intersection.id}}); // Create point geometry (Points type with single point) auto points = feature->geom()->newGeometry(GeomType::Points, 1); @@ -959,7 +1357,14 @@ void GridDataSource::generateIntersections(TileSpatialContext& ctx, // Generate attributes std::mt19937 gen(ctx.seed + intersection.id); - generateAttributes(feature, config.topAttributes, gen, intersection.id); + generateAttributes(feature, layerCfg.topAttributes, gen, intersection.id); + + // Profile-based attribute tree generation + auto profile = layerCfg.attributeTreeProfile.value_or(cfg.attributeTreeProfile); + if (profile != AttributeTreeProfile::None) { + auto params = ResolvedTreeParams::resolve(profile, cfg.attributeTreeParams, layerCfg.attributeTreeParams); + generateProfileAttributes(feature, tile, params, gen, intersection.id); + } } mapget::log().info(" Created {} intersection features", ctx.intersections.size()); @@ -1138,3 +1543,130 @@ std::string GridDataSource::generateAttributeValue(const AttributeConfig& attr, return "0"; } } + +// ============================================================================ +// Profile-based Attribute Tree Generation +// ============================================================================ + +namespace { + +// Recursive helper to generate a nested value for attribute tree profiles +simfil::ModelNode::Ptr generateNestedValue( + TileFeatureLayer::Ptr const& tile, + const ResolvedTreeParams& params, + uint32_t seed, + int depth, + int fieldIdx) +{ + uint32_t h = profileHash(seed, static_cast(depth), static_cast(fieldIdx), 0); + + // Check if we should nest deeper + bool shouldNest = (depth < params.maxNestingDepth) && + ((h % 1000) < static_cast(params.nestingProbability * 1000)); + + if (shouldNest) { + // Check if we should create an array instead of object + bool shouldArray = (params.maxArraySize > 0) && ((h % 3) == 0); + if (shouldArray) { + int arraySize = 1 + static_cast(h % static_cast(params.maxArraySize)); + auto arr = tile->newArray(static_cast(arraySize)); + for (int i = 0; i < arraySize; ++i) { + // Array elements are scalars + uint32_t elemH = profileHash(seed, static_cast(depth), static_cast(fieldIdx), static_cast(i)); + int typeRotation = static_cast(elemH % 4); + switch (typeRotation) { + case 0: arr->append(tile->newValue(std::string(kProfileStringValues[elemH % kNumProfileStringValues]))); break; + case 1: arr->append(tile->newValue(static_cast(kProfileIntValues[elemH % kNumProfileIntValues]))); break; + case 2: arr->append(tile->newValue(kProfileFloatValues[elemH % kNumProfileFloatValues])); break; + case 3: arr->append(tile->newSmallValue((elemH & 1) != 0)); break; + } + } + return arr; + } else { + // Nested object + int numSubFields = std::min(params.fieldsPerAttr, 4); + auto obj = tile->newObject(static_cast(numSubFields)); + for (int sf = 0; sf < numSubFields; ++sf) { + uint32_t subH = profileHash(seed, static_cast(depth + 1), static_cast(sf), 0); + auto fieldName = kProfileFieldNames[subH % kNumProfileFieldNames]; + auto val = generateNestedValue(tile, params, seed ^ static_cast(sf * 7919), depth + 1, sf); + if (val) + obj->addField(fieldName, val); + } + return obj; + } + } + + // Scalar value: rotate type based on hash + int typeRotation = static_cast(h % 4); + switch (typeRotation) { + case 0: return tile->newValue(std::string(kProfileStringValues[h % kNumProfileStringValues])); + case 1: return tile->newValue(static_cast(kProfileIntValues[h % kNumProfileIntValues])); + case 2: return tile->newValue(kProfileFloatValues[h % kNumProfileFloatValues]); + case 3: return tile->newSmallValue((h & 1) != 0); + } + return tile->newValue(static_cast(0)); +} + +} // anonymous namespace + +void GridDataSource::generateProfileAttributes( + model_ptr feature, + TileFeatureLayer::Ptr const& tile, + const ResolvedTreeParams& params, + std::mt19937& gen, + uint32_t featureId) +{ + uint32_t baseSeed = static_cast(gen()); + + // Step 1: Add top-level extra scalar fields to feature->attributes() + for (int i = 0; i < params.topLevelExtraFields; ++i) { + uint32_t h = profileHash(baseSeed, 0, static_cast(i), 0); + auto fieldName = kProfileFieldNames[h % kNumProfileFieldNames]; + int typeRotation = static_cast(h % 4); + switch (typeRotation) { + case 0: feature->attributes()->addField(fieldName, std::string(kProfileStringValues[h % kNumProfileStringValues])); break; + case 1: feature->attributes()->addField(fieldName, static_cast(kProfileIntValues[h % kNumProfileIntValues])); break; + case 2: feature->attributes()->addField(fieldName, kProfileFloatValues[h % kNumProfileFloatValues]); break; + case 3: feature->attributes()->addField(fieldName, static_cast((h & 1) ? 1 : 0)); break; + } + } + + // Step 2: Create attribute layers with semantic names + for (int layerIdx = 0; layerIdx < params.numLayers; ++layerIdx) { + auto layerName = kProfileLayerNames[layerIdx % kNumProfileLayerNames]; + auto attrLayer = feature->attributeLayers()->newLayer(layerName); + + // Step 3: Create attributes per layer + for (int attrIdx = 0; attrIdx < params.attrsPerLayer; ++attrIdx) { + uint32_t attrSeed = profileHash(baseSeed, static_cast(layerIdx), static_cast(attrIdx), featureId); + auto attrName = kProfileAttrNames[attrSeed % kNumProfileAttrNames]; + auto attr = attrLayer->newAttribute(attrName); + + // Step 4: Roll validity + uint32_t validH = profileHash(attrSeed, 999, 0, 0); + double validRoll = (validH % 1000) / 1000.0; + if (validRoll < params.directionalValidityProb) { + auto direction = ((validH >> 10) & 1) ? Validity::Positive : Validity::Negative; + attr->validity()->newDirection(direction); + } else if (validRoll < params.directionalValidityProb + params.rangeValidityProb) { + double startFrac = (validH % 100) / 100.0; + double endFrac = startFrac + ((validH >> 8) % 50) / 100.0; + if (endFrac > 1.0) endFrac = 1.0; + attr->validity()->newRange( + Validity::RelativeLengthOffset, + startFrac, endFrac, {}, + Validity::Both); + } + + // Step 5: Generate fields per attribute + for (int fieldIdx = 0; fieldIdx < params.fieldsPerAttr; ++fieldIdx) { + uint32_t fieldSeed = profileHash(attrSeed, static_cast(fieldIdx), featureId, 0); + auto fieldName = kProfileFieldNames[fieldSeed % kNumProfileFieldNames]; + auto val = generateNestedValue(tile, params, fieldSeed, 0, fieldIdx); + if (val) + attr->addField(fieldName, val); + } + } + } +} diff --git a/libs/http-service/CMakeLists.txt b/libs/http-service/CMakeLists.txt index ce3d733e..81c3ec9b 100644 --- a/libs/http-service/CMakeLists.txt +++ b/libs/http-service/CMakeLists.txt @@ -7,7 +7,9 @@ add_library(mapget-http-service STATIC src/http-service.cpp src/http-client.cpp - src/cli.cpp) + src/cli.cpp + src/devui.h + src/devui.cpp) target_include_directories(mapget-http-service PUBLIC diff --git a/libs/http-service/include/mapget/http-service/cli.h b/libs/http-service/include/mapget/http-service/cli.h index a028ed6e..ee9f5ff6 100644 --- a/libs/http-service/include/mapget/http-service/cli.h +++ b/libs/http-service/include/mapget/http-service/cli.h @@ -16,4 +16,7 @@ namespace mapget const std::string &getPathToSchemaPatch(); void setPathToSchema(const std::string &path); + + bool isDevModeEnabled(); + void setDevModeEnabled(bool enabled); } diff --git a/libs/http-service/src/cli.cpp b/libs/http-service/src/cli.cpp index 7e51c540..8a675972 100644 --- a/libs/http-service/src/cli.cpp +++ b/libs/http-service/src/cli.cpp @@ -76,6 +76,13 @@ nlohmann::json gridDataSourceSchema() {"mapId", {{"type", "string"}, {"title", "Map ID"}}}, {"spatialCoherence", {{"type", "boolean"}}}, {"collisionGridSize", {{"type", "number"}}}, + {"sourceDownloadDelayMs", {{"type", "integer"}, {"title", "Source Download Delay (ms)"}, {"description", "Sleep-wait delay simulating IO-bound source download (0 = disabled)."}}}, + {"sourceUnpackDelayMs", {{"type", "integer"}, {"title", "Source Unpack Delay (ms)"}, {"description", "Busy-wait delay simulating CPU-bound decompression/parsing (0 = disabled)."}}}, + {"sourceTransformDelayMs", {{"type", "integer"}, {"title", "Source Transform Delay (ms)"}, {"description", "Busy-wait delay simulating CPU-bound feature conversion (0 = disabled)."}}}, + {"attributeTreeProfile", {{"type", "string"}, {"title", "Attribute Tree Profile"}, {"enum", {"none", "minimal", "moderate", "realistic", "stress"}}, {"description", "Procedural attribute tree complexity profile."}}}, + {"attributeTreeParams", {{"type", "object"}, {"title", "Attribute Tree Params"}, {"description", "Optional overrides for attribute tree generation parameters."}}}, + {"delayMs", {{"type", "integer"}, {"title", "Simulated Delay (ms)"}, {"description", "DEPRECATED: Use sourceDownloadDelayMs/sourceTransformDelayMs instead."}}}, + {"delayMode", {{"type", "string"}, {"title", "Delay Mode"}, {"enum", {"sleep", "busyWait"}}, {"description", "DEPRECATED: Use sourceDownloadDelayMs/sourceTransformDelayMs instead."}}}, {"layers", {{"type", "array"}}} }}, {"additionalProperties", true} @@ -226,7 +233,9 @@ void registerDefaultDatasourceTypes() { if (config["enabled"].IsDefined() && !config["enabled"].as()) { return nullptr; // Skip this datasource } - return std::make_shared(config); + auto ds = std::make_shared(config); + gridsource::GridDataSource::registerInstance(ds); + return ds; }, gridDataSourceSchema()); service.registerDataSourceType( @@ -261,6 +270,7 @@ void loadConfigSchemaPatch(const std::string& schemaPath) bool isPostConfigEndpointEnabled_ = false; bool isGetConfigEndpointEnabled_ = true; +bool isDevModeEnabled_ = false; } struct ServeCommand @@ -344,6 +354,10 @@ struct ServeCommand "(0=disabled, 1=after every request, N=after every N JSON requests). " "Only effective on platforms supporting allocator trimming (e.g., Linux).") ->default_val(memoryTrimIntervalJson_); + serveCmd->add_flag( + "--dev-mode", + isDevModeEnabled_, + "Enable Developer UI at /dev/ for live GridDataSource configuration."); serveCmd->callback([this]() { serve(); }); } @@ -604,4 +618,14 @@ void setPathToSchema(const std::string& path) pathToSchema = path; } +bool isDevModeEnabled() +{ + return isDevModeEnabled_; +} + +void setDevModeEnabled(bool enabled) +{ + isDevModeEnabled_ = enabled; +} + } // namespace mapget diff --git a/libs/http-service/src/devui.cpp b/libs/http-service/src/devui.cpp new file mode 100644 index 00000000..2bebc87b --- /dev/null +++ b/libs/http-service/src/devui.cpp @@ -0,0 +1,452 @@ +// Copyright (c) Navigation Data Standard e.V. - See "LICENSE" file. + +#include "devui.h" +#include "gridsource/gridsource.h" +#include "mapget/log.h" +#include "nlohmann/json.hpp" + +namespace mapget +{ + +namespace +{ + +// Embedded Developer UI HTML page +static const char* kDevUIHtml = R"html( + + + + +Developer UI + + + +

Developer UI

+ +
+
GridSource: Attribute Tree Profile
+
+
+
+ +
+
GridSource: Simulated Latency
+
+
+ +
+
GridSource: Layers
+
+
+ +
+
Cache
+
+ + +
+
+ +
+ + +
+ + + +)html"; + +} // anonymous namespace + +void setupDevUI(httplib::Server& server, Cache::Ptr cache) +{ + // GET /dev/ - Serve the Developer UI HTML page + server.Get("/dev/", [](const httplib::Request&, httplib::Response& res) { + res.set_content(kDevUIHtml, "text/html"); + }); + // Also handle /dev without trailing slash + server.Get("/dev", [](const httplib::Request&, httplib::Response& res) { + res.set_redirect("/dev/"); + }); + + // GET /dev/gridsource/config - Return current config as JSON + server.Get("/dev/gridsource/config", + [](const httplib::Request&, httplib::Response& res) { + auto instances = gridsource::GridDataSource::getInstances(); + nlohmann::json result; + result["instances"] = nlohmann::json::array(); + for (auto& inst : instances) { + auto cfg = inst->getConfig(); + nlohmann::json entry; + entry["mapId"] = cfg.mapId; + entry["config"] = cfg.toJson(); + // Stats + nlohmann::json stats; + // Context cache size is not directly accessible, but we can signal it + entry["stats"] = stats; + result["instances"].push_back(entry); + } + res.set_content(result.dump(), "application/json"); + }); + + // POST /dev/gridsource/config - Partial config update + server.Post("/dev/gridsource/config", + [cache](const httplib::Request& req, httplib::Response& res) { + try { + auto j = nlohmann::json::parse(req.body); + + // Special: context cache clear only + if (j.contains("_clearContextCache") && j["_clearContextCache"].get()) { + for (auto& inst : gridsource::GridDataSource::getInstances()) { + inst->clearContextCache(); + } + res.set_content(R"({"status":"ok"})", "application/json"); + return; + } + + auto instances = gridsource::GridDataSource::getInstances(); + if (instances.empty()) { + res.status = 404; + res.set_content(R"({"error":"No GridDataSource instances found"})", "application/json"); + return; + } + + // Apply to the first instance (most common case) + auto& inst = instances[0]; + auto currentCfg = inst->getConfig(); + + // Merge provided fields into current config + if (j.contains("sourceDownloadDelayMs")) + currentCfg.sourceDownloadDelayMs = j["sourceDownloadDelayMs"].get(); + if (j.contains("sourceUnpackDelayMs")) + currentCfg.sourceUnpackDelayMs = j["sourceUnpackDelayMs"].get(); + if (j.contains("sourceTransformDelayMs")) + currentCfg.sourceTransformDelayMs = j["sourceTransformDelayMs"].get(); + if (j.contains("attributeTreeProfile")) { + static const std::map pmap = { + {"none", gridsource::AttributeTreeProfile::None}, + {"minimal", gridsource::AttributeTreeProfile::Minimal}, + {"moderate", gridsource::AttributeTreeProfile::Moderate}, + {"realistic", gridsource::AttributeTreeProfile::Realistic}, + {"stress", gridsource::AttributeTreeProfile::Stress} + }; + auto profileStr = j["attributeTreeProfile"].get(); + auto it = pmap.find(profileStr); + currentCfg.attributeTreeProfile = (it != pmap.end()) ? it->second : gridsource::AttributeTreeProfile::None; + } + if (j.contains("attributeTreeParams")) { + auto& atp = j["attributeTreeParams"]; + if (atp.contains("numLayers")) currentCfg.attributeTreeParams.numLayers = atp["numLayers"].get(); + if (atp.contains("attrsPerLayer")) currentCfg.attributeTreeParams.attrsPerLayer = atp["attrsPerLayer"].get(); + if (atp.contains("fieldsPerAttr")) currentCfg.attributeTreeParams.fieldsPerAttr = atp["fieldsPerAttr"].get(); + if (atp.contains("maxNestingDepth")) currentCfg.attributeTreeParams.maxNestingDepth = atp["maxNestingDepth"].get(); + if (atp.contains("maxArraySize")) currentCfg.attributeTreeParams.maxArraySize = atp["maxArraySize"].get(); + if (atp.contains("nestingProbability")) currentCfg.attributeTreeParams.nestingProbability = atp["nestingProbability"].get(); + if (atp.contains("directionalValidityProb")) currentCfg.attributeTreeParams.directionalValidityProb = atp["directionalValidityProb"].get(); + if (atp.contains("rangeValidityProb")) currentCfg.attributeTreeParams.rangeValidityProb = atp["rangeValidityProb"].get(); + if (atp.contains("topLevelExtraFields")) currentCfg.attributeTreeParams.topLevelExtraFields = atp["topLevelExtraFields"].get(); + } + if (j.contains("layers") && j["layers"].is_array()) { + // Match by index for now + for (size_t i = 0; i < j["layers"].size() && i < currentCfg.layers.size(); ++i) { + auto& lj = j["layers"][i]; + auto& layer = currentCfg.layers[i]; + if (lj.contains("enabled")) layer.enabled = lj["enabled"].get(); + if (lj.contains("geometry") && lj["geometry"].contains("density")) { + layer.geometry.density = lj["geometry"]["density"].get(); + } + if (lj.contains("attributeTreeProfile")) { + static const std::map pmap = { + {"none", gridsource::AttributeTreeProfile::None}, + {"minimal", gridsource::AttributeTreeProfile::Minimal}, + {"moderate", gridsource::AttributeTreeProfile::Moderate}, + {"realistic", gridsource::AttributeTreeProfile::Realistic}, + {"stress", gridsource::AttributeTreeProfile::Stress} + }; + auto profileStr = lj["attributeTreeProfile"].get(); + auto it = pmap.find(profileStr); + layer.attributeTreeProfile = (it != pmap.end()) ? it->second : gridsource::AttributeTreeProfile::None; + } + } + } + + // Apply the updated config + inst->setConfig(std::move(currentCfg)); + + // Also clear the tile cache so next requests regenerate + if (cache) cache->clear(); + + log().info("Dev UI: GridDataSource config updated"); + res.set_content(R"({"status":"ok"})", "application/json"); + } catch (const std::exception& e) { + res.status = 400; + nlohmann::json err; + err["error"] = e.what(); + res.set_content(err.dump(), "application/json"); + } + }); + + // POST /dev/cache/clear - Clear tile cache + server.Post("/dev/cache/clear", + [cache](const httplib::Request&, httplib::Response& res) { + if (cache) { + cache->clear(); + log().info("Dev UI: Tile cache cleared"); + } + // Also clear context caches on all GridDataSource instances + for (auto& inst : gridsource::GridDataSource::getInstances()) { + inst->clearContextCache(); + } + res.set_content(R"({"status":"ok"})", "application/json"); + }); + + log().info("Developer UI enabled at /dev/"); +} + +} // namespace mapget diff --git a/libs/http-service/src/devui.h b/libs/http-service/src/devui.h new file mode 100644 index 00000000..4293a747 --- /dev/null +++ b/libs/http-service/src/devui.h @@ -0,0 +1,21 @@ +// Copyright (c) Navigation Data Standard e.V. - See "LICENSE" file. + +#pragma once + +#include "httplib.h" +#include "mapget/service/cache.h" + +namespace mapget +{ + +/** + * Set up the Developer UI routes on the given HTTP server. + * Routes: + * GET /dev/ - Serves the Developer UI HTML page + * GET /dev/gridsource/config - Returns current GridDataSource config as JSON + * POST /dev/gridsource/config - Partial config update, clears caches + * POST /dev/cache/clear - Clears the tile cache + */ +void setupDevUI(httplib::Server& server, Cache::Ptr cache); + +} // namespace mapget diff --git a/libs/http-service/src/http-service.cpp b/libs/http-service/src/http-service.cpp index 0137a1b7..7a37e8eb 100644 --- a/libs/http-service/src/http-service.cpp +++ b/libs/http-service/src/http-service.cpp @@ -1,4 +1,5 @@ #include "http-service.h" +#include "devui.h" #include "mapget/log.h" #include "mapget/service/config.h" @@ -663,6 +664,11 @@ void HttpService::setup(httplib::Server& server) "/config", [this](const httplib::Request& req, httplib::Response& res) { impl_->handlePostConfigRequest(req, res); }); + + // Conditionally enable Developer UI + if (isDevModeEnabled()) { + setupDevUI(server, cache()); + } } } // namespace mapget diff --git a/libs/service/include/mapget/service/cache.h b/libs/service/include/mapget/service/cache.h index bee02b1d..281c8841 100644 --- a/libs/service/include/mapget/service/cache.h +++ b/libs/service/include/mapget/service/cache.h @@ -58,6 +58,12 @@ class Cache : public TileLayerStream::StringPoolCache, public std::enable_shared /** Abstract: Upsert (update or insert) a string-pool blob. */ virtual void putStringPoolBlob(std::string_view const& sourceNodeId, std::string const& v) = 0; + /** + * Clear all cached data. Default is a no-op (e.g. for NullCache). + * Override in cache implementations that hold data. + */ + virtual void clear() {} + // Override this method if your cache implementation has special stats. /** diff --git a/libs/service/include/mapget/service/memcache.h b/libs/service/include/mapget/service/memcache.h index 283c0e11..94ac944c 100644 --- a/libs/service/include/mapget/service/memcache.h +++ b/libs/service/include/mapget/service/memcache.h @@ -35,6 +35,9 @@ class MemCache : public Cache /** Upsert a string-pool blob. -> No-Op */ void putStringPoolBlob(std::string_view const& sourceNodeId, std::string const& v) override {} + /** Clear all cached tiles. */ + void clear() override; + /** Enriches the statistics with info about the number of cached tiles. */ nlohmann::json getStatistics() const override; diff --git a/libs/service/src/memcache.cpp b/libs/service/src/memcache.cpp index d9a49dc8..dc775a91 100644 --- a/libs/service/src/memcache.cpp +++ b/libs/service/src/memcache.cpp @@ -31,6 +31,12 @@ void MemCache::putTileLayerBlob(const MapTileKey& k, const std::string& v) } } +void MemCache::clear() { + std::unique_lock cacheLock(cacheMutex_); + cachedTiles_.clear(); + fifo_.clear(); +} + nlohmann::json MemCache::getStatistics() const { auto result = Cache::getStatistics(); result["memcache-map-size"] = (int64_t)cachedTiles_.size(); diff --git a/libs/service/src/service.cpp b/libs/service/src/service.cpp index c8449d8c..09d2ba57 100644 --- a/libs/service/src/service.cpp +++ b/libs/service/src/service.cpp @@ -116,6 +116,8 @@ struct Service::Controller MapTileKey tileKey; LayerTilesRequest::Ptr request; std::optional cacheExpiredAt; + std::chrono::steady_clock::time_point createdAt; + bool cacheHit = false; }; std::set jobsInProgress_; // Set of jobs currently in progress @@ -157,7 +159,8 @@ struct Service::Controller // Create result wrapper object. auto tileId = request->tiles_[request->nextTileIndex_++]; - result = Job{MapTileKey(), request, std::nullopt}; + result = Job{MapTileKey(), request, std::nullopt, + std::chrono::steady_clock::now(), false}; result->tileKey.layer_ = layerIt->second->type_; result->tileKey.mapId_ = request->mapId_; result->tileKey.layerId_ = request->layerId_; @@ -260,10 +263,17 @@ struct Service::Worker dataSource_->onCacheExpired(job.tileKey, *job.cacheExpiredAt); } + auto workStart = std::chrono::steady_clock::now(); auto layer = dataSource_->get(job.tileKey, controller_.cache_, info_); if (!layer) raise("DataSource::get() returned null."); + // Record server-side timing metadata + auto queueWaitMs = std::chrono::duration_cast( + workStart - job.createdAt).count(); + layer->setInfo("queue-wait-ms", queueWaitMs); + layer->setInfo("cache-hit", job.cacheHit ? 1 : 0); + // Special FeatureLayer handling if (layer->layerInfo()->type_ == LayerType::Features) { controller_.loadAddOnTiles(std::static_pointer_cast(layer), *dataSource_);