/*
* Copyright 2016 The Cartographer Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
using CartographerSharp.Common.Math;
using CartographerSharp.Mapping.D2D;
using CartographerSharp.Transform;
using RobotNet10.Shared.Numbers;
namespace CartographerSharp.Mapping.Internal.D2D;
///
/// Trims submaps from the pose graph based on overlap area.
/// Removes older submaps that overlap significantly with newer ones,
/// keeping only the freshest submaps while ensuring minimum coverage.
///
public class OverlappingSubmapsTrimmer2D : PoseGraphTrimmer
{
private readonly int _freshSubmapsCount;
private readonly double _minCoveredArea;
private readonly int _minAddedSubmapsCount;
// Current finished submap count (matches C++ current_submap_count_)
private int _currentSubmapCount = 0;
public OverlappingSubmapsTrimmer2D(
int freshSubmapsCount,
double minCoveredArea,
int minAddedSubmapsCount)
{
if (freshSubmapsCount < 0)
throw new ArgumentException("freshSubmapsCount must be non-negative", nameof(freshSubmapsCount));
if (minCoveredArea < 0)
throw new ArgumentException("minCoveredArea must be non-negative", nameof(minCoveredArea));
if (minAddedSubmapsCount < 0)
throw new ArgumentException("minAddedSubmapsCount must be non-negative", nameof(minAddedSubmapsCount));
_freshSubmapsCount = freshSubmapsCount;
_minCoveredArea = minCoveredArea;
_minAddedSubmapsCount = minAddedSubmapsCount;
}
public override void Trim(ITrimmable trimmable)
{
var submapData = trimmable.GetOptimizedSubmapData();
// Match C++: if (submap_data.size() - current_submap_count_ <= min_added_submaps_count_)
if (submapData.Count - _currentSubmapCount <= _minAddedSubmapsCount)
{
return;
}
// Get first submap's map limits to initialize coverage grid
if (submapData.Count == 0)
{
return;
}
var firstSubmapData = submapData.First();
if (firstSubmapData.Data.Submap is not Mapping.D2D.Submap2D firstSubmap || firstSubmap.Grid == null)
{
return;
}
var firstSubmapMapLimits = firstSubmap.Grid.Limits;
var coverageGrid = new SubmapCoverageGrid2D(firstSubmapMapLimits);
// Compute submap freshness from intra-submap constraints
var submapFreshness = ComputeSubmapFreshness(
submapData,
trimmable.GetTrajectoryNodes(),
trimmable.GetConstraints());
// Add all submaps to coverage grid
var allSubmapIds = AddSubmapsToSubmapCoverageGrid2D(
submapFreshness,
submapData,
coverageGrid);
// Find submaps to trim
// Match C++: min_covered_area_ / common::Pow2(coverage_grid.resolution())
var minCoveredCellsCount = (int)Math.Round(_minCoveredArea / MathUtils.Pow2(coverageGrid.Resolution));
var submapIdsToRemove = FindSubmapIdsToTrim(
coverageGrid,
allSubmapIds,
_freshSubmapsCount,
minCoveredCellsCount);
// Update current submap count (matches C++: current_submap_count_ = submap_data.size() - submap_ids_to_remove.size())
_currentSubmapCount = submapData.Count - submapIdsToRemove.Count;
// Trim the submaps
foreach (var id in submapIdsToRemove)
{
trimmable.TrimSubmap(id);
}
}
///
/// Tracks which submaps cover which cells in a global coordinate system.
///
private class SubmapCoverageGrid2D(Mapping.D2D.MapLimits mapLimits)
{
// Aliases for documentation only (no type-safety).
public record CellId(long X, long Y);
public record StoredType(SubmapId SubmapId, long Time);
private readonly Vector2 _offset = mapLimits.Max;
private readonly double _resolution = mapLimits.Resolution;
private readonly Dictionary> _cells = [];
public void AddPoint(Vector2 point, SubmapId submapId, long time)
{
var cellId = new CellId(
(long)Math.Round((_offset.X - point.X) / _resolution, MidpointRounding.AwayFromZero),
(long)Math.Round((_offset.Y - point.Y) / _resolution, MidpointRounding.AwayFromZero));
if (!_cells.TryGetValue(cellId, out var storedTypes))
{
storedTypes = [];
_cells[cellId] = storedTypes;
}
storedTypes.Add(new StoredType(submapId, time));
}
public Dictionary> Cells => _cells;
public double Resolution => _resolution;
}
///
/// Uses intra-submap constraints and trajectory node timestamps to identify time
/// of the last range data insertion to the submap.
///
private static Dictionary ComputeSubmapFreshness(
MapById submapData,
MapById trajectoryNodes,
List constraints)
{
var submapFreshness = new Dictionary();
// Find the node with the largest NodeId per SubmapId.
var submapToLatestNode = new Dictionary();
foreach (var constraint in constraints)
{
if (constraint.ConstraintTag != IPoseGraph.Constraint.Tag.IntraSubmap)
{
continue;
}
if (!submapToLatestNode.TryGetValue(constraint.SubmapId, out var existingNodeId))
{
submapToLatestNode[constraint.SubmapId] = constraint.NodeId;
continue;
}
// Keep the maximum NodeId (matches C++: std::max)
if (CompareNodeIds(constraint.NodeId, existingNodeId) > 0)
{
submapToLatestNode[constraint.SubmapId] = constraint.NodeId;
}
}
// Find timestamp of every latest node.
foreach (var (submapId, nodeId) in submapToLatestNode)
{
if (!submapData.Contains(submapId))
{
// Log warning equivalent (C++: LOG(WARNING))
continue;
}
if (!trajectoryNodes.Contains(nodeId))
{
continue;
}
var trajectoryNode = trajectoryNodes[nodeId];
if (trajectoryNode.ConstantData == null)
{
continue;
}
submapFreshness[submapId] = trajectoryNode.ConstantData.Time;
}
return submapFreshness;
}
///
/// Compares two NodeIds. Returns positive if lhs > rhs, negative if lhs < rhs, 0 if equal.
///
private static int CompareNodeIds(NodeId lhs, NodeId rhs)
{
var trajectoryCompare = lhs.TrajectoryId.CompareTo(rhs.TrajectoryId);
if (trajectoryCompare != 0)
{
return trajectoryCompare;
}
return lhs.NodeIndex.CompareTo(rhs.NodeIndex);
}
///
/// Iterates over every cell in a submap, transforms the center of the cell to
/// the global frame and then adds the submap id and the timestamp of the most
/// recent range data insertion into the global grid.
///
private static HashSet AddSubmapsToSubmapCoverageGrid2D(
Dictionary submapFreshness,
MapById submapData,
SubmapCoverageGrid2D coverageGrid)
{
var allSubmapIds = new HashSet();
foreach (var submap in submapData)
{
if (!submapFreshness.TryGetValue(submap.Id, out var freshness))
{
continue;
}
if (submap.Data.Submap is not Mapping.D2D.Submap2D submap2D || !submap2D.InsertionFinished)
{
continue;
}
if (submap2D.Grid == null)
{
continue;
}
allSubmapIds.Add(submap.Id);
var grid = submap2D.Grid;
// Iterate over every cell in a submap.
grid.ComputeCroppedLimits(out var offset, out var cellLimits);
if (cellLimits.NumXCells == 0 || cellLimits.NumYCells == 0)
{
// Log warning equivalent (C++: LOG(WARNING))
continue;
}
var globalFrameFromSubmapFrame = submap.Data.Pose;
var submapFrameFromLocalFrame = submap2D.LocalPose.Inverse();
foreach (var xyIndex in new XYIndexRange(cellLimits))
{
var index = xyIndex + offset;
if (!grid.IsKnown(index))
{
continue;
}
// Match C++: center_of_cell_in_local_frame calculation
// C++: grid.limits().max().x() - grid.limits().resolution() * (index.y() + 0.5)
// C++: grid.limits().max().y() - grid.limits().resolution() * (index.x() + 0.5)
var centerOfCellInLocalFrame = new Rigid3d(
new Vector3(
(grid.Limits.Max.X - grid.Limits.Resolution * (index.Y + 0.5)),
(grid.Limits.Max.Y - grid.Limits.Resolution * (index.X + 0.5)),
0.0),
Quaternion.Identity);
// Match C++: transform::Project2D(global_frame_from_submap_frame * submap_frame_from_local_frame * center_of_cell_in_local_frame)
var centerOfCellInGlobalFrame = TransformOperations.Project2D(
globalFrameFromSubmapFrame *
submapFrameFromLocalFrame *
centerOfCellInLocalFrame);
coverageGrid.AddPoint(
centerOfCellInGlobalFrame.Translation,
submap.Id,
freshness);
}
}
return allSubmapIds;
}
///
/// Returns IDs of submaps that have less than 'min_covered_cells_count' cells
/// not overlapped by at least 'fresh_submaps_count' submaps.
///
private static List FindSubmapIdsToTrim(
SubmapCoverageGrid2D coverageGrid,
HashSet allSubmapIds,
int freshSubmapsCount,
int minCoveredCellsCount)
{
var submapToCoveredCellsCount = new Dictionary();
foreach (var (cellId, storedTypes) in coverageGrid.Cells)
{
var submapsPerCell = new List<(SubmapId SubmapId, long Time)>();
foreach (var storedType in storedTypes)
{
submapsPerCell.Add((storedType.SubmapId, storedType.Time));
}
// In case there are several submaps covering the cell, only the freshest
// submaps are kept.
if (submapsPerCell.Count > freshSubmapsCount)
{
// Sort by time in descending order (matches C++: std::sort with > comparison)
submapsPerCell.Sort((left, right) => right.Time.CompareTo(left.Time));
submapsPerCell = [.. submapsPerCell.Take(freshSubmapsCount)];
}
foreach (var (submapId, _) in submapsPerCell)
{
if (!submapToCoveredCellsCount.TryGetValue(submapId, out var count))
{
count = 0;
}
submapToCoveredCellsCount[submapId] = count + 1;
}
}
var submapIdsToKeep = new List();
foreach (var (submapId, cellsCount) in submapToCoveredCellsCount)
{
if (cellsCount < minCoveredCellsCount)
{
continue;
}
submapIdsToKeep.Add(submapId);
}
// Match C++: std::set_difference(all_submap_ids, submap_ids_to_keep)
submapIdsToKeep.Sort((a, b) =>
{
var trajectoryCompare = a.TrajectoryId.CompareTo(b.TrajectoryId);
if (trajectoryCompare != 0)
{
return trajectoryCompare;
}
return a.SubmapIndex.CompareTo(b.SubmapIndex);
});
var result = new List();
foreach (var submapId in allSubmapIds)
{
if (!submapIdsToKeep.Contains(submapId))
{
result.Add(submapId);
}
}
return result;
}
}