After some others help and Google, I come up with this solution (written in C#).
AngleRange
public class AngleRange
{
private readonly double _from, _to;
private readonly bool _full;
public AngleRange (double from, double to)
{
_from = DoubleUtils.NormalizeAngle(from);
_to = DoubleUtils.NormalizeAngle(to);
_full = false;
}
public AngleRange (bool full)
{
_from = 0;
_to = 0;
_full = full;
}
public bool Inside(double target)
{
if (_full)
return true;
if (_from < _to)
return _from <= target && target <= _to;
return _from <= target || target <= _to;
}
public bool TryMerge(AngleRange newAngleRange, out AngleRange resultAngleRange)
{
if (newAngleRange._full)
{
resultAngleRange = newAngleRange;
return true;
}
if (_full || Equals(newAngleRange))
{
resultAngleRange = this;
return true;
}
var aStart = _from;
var aEnd = aStart > _to ? _to + 360 : _to;
var bStart = newAngleRange._from;
var bEnd = bStart > newAngleRange._to
? newAngleRange._to + 360 : newAngleRange._to;
var diffA = (aEnd - aStart)/2;
var diffB = (bEnd - bStart)/2;
var avgA = (aStart + aEnd)/2;
var avgB = (bStart + bEnd)/2;
var cosDiffA = Math.Cos(diffA.ToRadians());
var cosDiffB = Math.Cos(diffB.ToRadians());
var resultFlag = MergeFlag.None;
if (Math.Cos((avgA - bStart).ToRadians()).AboutGreaterThanOrEqual(cosDiffA))
resultFlag |= MergeFlag.BStartInsideA;
if (Math.Cos((avgA - bEnd).ToRadians()).AboutGreaterThanOrEqual(cosDiffA))
resultFlag |= MergeFlag.BEndInsideA;
if (Math.Cos((avgB - aStart).ToRadians()).AboutGreaterThanOrEqual(cosDiffB))
resultFlag |= MergeFlag.AStartInsideB;
if (Math.Cos((avgB - aEnd).ToRadians()).AboutGreaterThanOrEqual(cosDiffB))
resultFlag |= MergeFlag.AEndInsideB;
if (NotHasFlags(resultFlag, MergeFlag.BStartInsideA, MergeFlag.BEndInsideA,
MergeFlag.AEndInsideB, MergeFlag.AStartInsideB))
{
resultAngleRange = null;
return false;
}
if (HasFlags(resultFlag, MergeFlag.BStartInsideA, MergeFlag.BEndInsideA,
MergeFlag.AEndInsideB, MergeFlag.AStartInsideB))
{
resultAngleRange = new Shadow(true);
return true;
}
if (HasFlags(resultFlag, MergeFlag.BStartInsideA, MergeFlag.BEndInsideA))
{
resultAngleRange = this;
return true;
}
if (HasFlags(resultFlag, MergeFlag.AEndInsideB, MergeFlag.AStartInsideB))
{
resultAngleRange = newAngleRange;
return true;
}
if (HasFlags(resultFlag, MergeFlag.AEndInsideB, MergeFlag.BStartInsideA))
{
resultAngleRange = new Shadow(aStart, bEnd);
return true;
}
if (!HasFlags(resultFlag, MergeFlag.AStartInsideB, MergeFlag.BEndInsideA))
{
resultAngleRange = new Shadow(bStart, aEnd);
return true;
}
throw new InvalidOperationException("This should never happen.");
}
private static bool HasFlags(MergeFlag resultFlag, params MergeFlag[] flags)
{
return flags.All(flag => (resultFlag & flag) == flag);
}
private static bool NotHasFlags(MergeFlag resultFlag, params MergeFlag[] flags)
{
return flags.All(flag => (resultFlag & flag) != flag);
}
}
MergeFlag
[Flags]
private enum MergeFlag : short
{
None = 0,
BStartInsideA = 1 << 0,
BEndInsideA = 1 << 1,
AStartInsideB = 1 << 2,
AEndInsideB = 1 << 3
}
Using AboutGreaterThanOrEqual() instead of >= because of rounding error on edge cases. It is a method combine thin AboutEqual() from here and >.
_full represents AngleRange is a full circle. ToRadians() is a method that coverts degree into Radians.