Highlight steep parts of a route

Signed-off-by: Ilya Shakhat <shakhat@gmail.com>
This commit is contained in:
Ilya Shakhat 2024-09-25 15:32:05 +02:00
parent 5a4e97b818
commit 53a5ba04a9
3 changed files with 72 additions and 20 deletions

View file

@ -112,7 +112,8 @@ void ReflectChartData(vector<double> & chartData)
bool NormalizeChartData(vector<double> const & distanceDataM,
geometry::Altitudes const & altitudeDataM, size_t resultPointCount,
vector<double> & uniformAltitudeDataM)
vector<double> & uniformAltitudeDataM,
vector<double> & uniformSlopeDataM)
{
double constexpr kEpsilon = 1e-6;
@ -134,11 +135,13 @@ bool NormalizeChartData(vector<double> const & distanceDataM,
return true;
}
auto const calculateAltitude = [&](double distFormStartM) {
using TAltitudeSlope = pair<double, double>;
auto const calculateAltitudeSlope = [&](double distFormStartM) {
if (distFormStartM <= distanceDataM.front())
return static_cast<double>(altitudeDataM.front());
return TAltitudeSlope(static_cast<double>(altitudeDataM.front()), 0);
if (distFormStartM >= distanceDataM.back())
return static_cast<double>(altitudeDataM.back());
return TAltitudeSlope(static_cast<double>(altitudeDataM.back()), 0);
auto const lowerIt = lower_bound(distanceDataM.cbegin(), distanceDataM.cend(), distFormStartM);
size_t const nextPointIdx = distance(distanceDataM.cbegin(), lowerIt);
@ -146,20 +149,24 @@ bool NormalizeChartData(vector<double> const & distanceDataM,
size_t const prevPointIdx = nextPointIdx - 1;
if (base::AlmostEqualAbs(distanceDataM[prevPointIdx], distanceDataM[nextPointIdx], kEpsilon))
return static_cast<double>(altitudeDataM[prevPointIdx]);
return TAltitudeSlope(static_cast<double>(altitudeDataM[prevPointIdx]), 0);
double const k = (altitudeDataM[nextPointIdx] - altitudeDataM[prevPointIdx]) /
(distanceDataM[nextPointIdx] - distanceDataM[prevPointIdx]);
return static_cast<double>(altitudeDataM[prevPointIdx]) +
k * (distFormStartM - distanceDataM[prevPointIdx]);
return TAltitudeSlope(static_cast<double>(altitudeDataM[prevPointIdx]) +
k * (distFormStartM - distanceDataM[prevPointIdx]), k);
};
double const routeLenM = distanceDataM.back();
uniformAltitudeDataM.resize(resultPointCount);
uniformSlopeDataM.resize(resultPointCount);
double const stepLenM = resultPointCount <= 1 ? 0.0 : routeLenM / (resultPointCount - 1);
for (size_t i = 0; i < resultPointCount; ++i)
uniformAltitudeDataM[i] = calculateAltitude(static_cast<double>(i) * stepLenM);
for (size_t i = 0; i < resultPointCount; ++i) {
auto altitudeSlope = calculateAltitudeSlope(static_cast<double>(i) * stepLenM);
uniformAltitudeDataM[i] = altitudeSlope.first;
uniformSlopeDataM[i] = altitudeSlope.second;
}
return true;
}
@ -212,6 +219,7 @@ bool GenerateYAxisChartData(uint32_t height, double minMetersPerPxl,
}
bool GenerateChartByPoints(uint32_t width, uint32_t height, vector<m2::PointD> const & geometry,
vector<double> const & slopes,
MapStyle mapStyle, vector<uint8_t> & frameBuffer)
{
frameBuffer.clear();
@ -263,6 +271,39 @@ bool GenerateChartByPoints(uint32_t width, uint32_t height, vector<m2::PointD> c
agg::scanline32_p8 scanline;
agg::render_scanlines_aa_solid(rasterizer, scanline, baseRenderer, kCurveColor);
// Draw slopes
for (size_t i = 1; i < geometry.size(); ++i) {
auto x = geometry[i].x;
auto y = geometry[i].y;
auto slope = abs(slopes[i]);
if (slope < 0.05) {
continue;
}
agg::path_storage segment;
segment.move_to(x, y);
segment.line_to(x, static_cast<double>(height));
agg::conv_stroke<agg::path_storage> strokeSegment(segment);
strokeSegment.width(2.0);
rasterizer.add_path(strokeSegment);
agg::rgba8 color;
// palette is generated by mycolor.space from the base color kCurveColor
if (slope < 0.1)
color = agg::rgba8(0x62, 0x83, 0xDC, 20);
else if (slope < 0.2)
color = agg::rgba8(0x7F, 0x71, 0xC3, 40);
else if (slope < 0.3)
color = agg::rgba8(0x8E, 0x60, 0xA7, 60);
else if (slope < 0.4)
color = agg::rgba8(0x93, 0x51, 0x8B, 80);
else
color = agg::rgba8(0x90, 0x44, 0x6F, 100);
agg::render_scanlines_aa_solid(rasterizer, scanline, baseRenderer, color);
}
// Chart line.
TPath path_adaptor(geometry, false);
TStroke stroke(path_adaptor);
@ -287,7 +328,8 @@ bool GenerateChart(uint32_t width, uint32_t height, vector<double> const & dista
}
vector<double> uniformAltitudeDataM;
if (!NormalizeChartData(distanceDataM, altitudeDataM, width, uniformAltitudeDataM))
vector<double> uniformSlopeDataM;
if (!NormalizeChartData(distanceDataM, altitudeDataM, width, uniformAltitudeDataM, uniformSlopeDataM))
return false;
vector<double> yAxisDataPxl;
@ -305,6 +347,6 @@ bool GenerateChart(uint32_t width, uint32_t height, vector<double> const & dista
geometry[i] = m2::PointD(i * oneSegLenPix, yAxisDataPxl[i]);
}
return GenerateChartByPoints(width, height, geometry, mapStyle, frameBuffer);
return GenerateChartByPoints(width, height, geometry, uniformSlopeDataM, mapStyle, frameBuffer);
}
} // namespace maps

View file

@ -21,7 +21,8 @@ void ReflectChartData(std::vector<double> & chartData);
/// This method is used to generalize and evenly distribute points of the chart.
bool NormalizeChartData(std::vector<double> const & distanceDataM,
geometry::Altitudes const & altitudeDataM, size_t resultPointCount,
std::vector<double> & uniformAltitudeDataM);
std::vector<double> & uniformAltitudeDataM,
std::vector<double> & uniformSlopeDataM);
/// \brief fills |yAxisDataPxl|. |yAxisDataPxl| is formed to pevent displaying
/// big waves on the chart in case of small deviation in absolute values in |yAxisData|.
@ -42,7 +43,9 @@ bool GenerateYAxisChartData(uint32_t height, double minMetersPerPxl,
/// \param frameBuffer is a vector for a result image. It's resized in this method.
/// It's filled with RGBA(8888) image date.
bool GenerateChartByPoints(uint32_t width, uint32_t height,
std::vector<m2::PointD> const & geometry, MapStyle mapStyle,
std::vector<m2::PointD> const & geometry,
std::vector<double> const & slopes,
MapStyle mapStyle,
std::vector<uint8_t> & frameBuffer);
bool GenerateChart(uint32_t width, uint32_t height, std::vector<double> const & distanceDataM,

View file

@ -81,7 +81,8 @@ UNIT_TEST(NormalizeChartData_SmokeTest)
geometry::Altitudes const altitudeDataM = {0, 0, 0};
vector<double> uniformAltitudeDataM;
TEST(maps::NormalizeChartData(distanceDataM, altitudeDataM, 2 /* resultPointCount */, uniformAltitudeDataM),
vector<double> uniformSlopeDataM;
TEST(maps::NormalizeChartData(distanceDataM, altitudeDataM, 2 /* resultPointCount */, uniformAltitudeDataM, uniformSlopeDataM),
());
vector<double> const expectedUniformAltitudeDataM = {0.0, 0.0};
@ -94,7 +95,8 @@ UNIT_TEST(NormalizeChartData_NoResultPointTest)
geometry::Altitudes const altitudeDataM = {0, 0, 0};
vector<double> uniformAltitudeDataM;
TEST(maps::NormalizeChartData(distanceDataM, altitudeDataM, 0 /* resultPointCount */, uniformAltitudeDataM),
vector<double> uniformSlopeDataM;
TEST(maps::NormalizeChartData(distanceDataM, altitudeDataM, 0 /* resultPointCount */, uniformAltitudeDataM, uniformSlopeDataM),
());
TEST(uniformAltitudeDataM.empty(), ());
@ -106,7 +108,8 @@ UNIT_TEST(NormalizeChartData_NoPointTest)
geometry::Altitudes const altitudeDataM = {};
vector<double> uniformAltitudeDataM;
TEST(maps::NormalizeChartData(distanceDataM, altitudeDataM, 2 /* resultPointCount */, uniformAltitudeDataM),
vector<double> uniformSlopeDataM;
TEST(maps::NormalizeChartData(distanceDataM, altitudeDataM, 2 /* resultPointCount */, uniformAltitudeDataM, uniformSlopeDataM),
());
TEST(uniformAltitudeDataM.empty(), ());
@ -118,7 +121,8 @@ UNIT_TEST(NormalizeChartData_Test)
geometry::Altitudes const altitudeDataM = {-9, 0, 9, 18};
vector<double> uniformAltitudeDataM;
TEST(maps::NormalizeChartData(distanceDataM, altitudeDataM, 10 /* resultPointCount */, uniformAltitudeDataM),
vector<double> uniformSlopeDataM;
TEST(maps::NormalizeChartData(distanceDataM, altitudeDataM, 10 /* resultPointCount */, uniformAltitudeDataM, uniformSlopeDataM),
());
vector<double> const expectedUniformAltitudeDataM = {-9.0, -6.0, -3.0, 0.0, 3.0,
@ -158,11 +162,12 @@ UNIT_TEST(GenerateYAxisChartData_Test)
UNIT_TEST(GenerateChartByPoints_NoGeometryTest)
{
vector<m2::PointD> const geometry = {};
vector<double> const slopes = {};
size_t constexpr width = 100;
size_t constexpr height = 40;
vector<uint8_t> frameBuffer;
TEST(maps::GenerateChartByPoints(width, height, geometry, MapStyleDefaultLight /* mapStyle */, frameBuffer), ());
TEST(maps::GenerateChartByPoints(width, height, geometry, slopes, MapStyleDefaultLight /* mapStyle */, frameBuffer), ());
TestAngleColors(width, height, frameBuffer, 255 /* expectedR */, 255 /* expectedG */,
255 /* expectedB */, 0 /* expectedA */);
}
@ -170,11 +175,12 @@ UNIT_TEST(GenerateChartByPoints_NoGeometryTest)
UNIT_TEST(GenerateChartByPoints_OnePointTest)
{
vector<m2::PointD> const geometry = {{20.0, 20.0}};
vector<double> const slopes = {0.0};
size_t constexpr width = 40;
size_t constexpr height = 40;
vector<uint8_t> frameBuffer;
TEST(maps::GenerateChartByPoints(width, height, geometry, MapStyleDefaultLight /* mapStyle */, frameBuffer), ());
TEST(maps::GenerateChartByPoints(width, height, geometry, slopes, MapStyleDefaultLight /* mapStyle */, frameBuffer), ());
TestAngleColors(width, height, frameBuffer, 255 /* expectedR */, 255 /* expectedG */,
255 /* expectedB */, 0 /* expectedA */);
}
@ -182,12 +188,13 @@ UNIT_TEST(GenerateChartByPoints_OnePointTest)
UNIT_TEST(GenerateChartByPoints_Test)
{
vector<m2::PointD> const geometry = {{0.0, 0.0}, {10.0, 10.0}};
vector<double> const slopes = {1.0, 0.0};
size_t constexpr width = 40;
size_t constexpr height = 40;
vector<uint8_t> frameBuffer;
TEST(maps::GenerateChartByPoints(width, height, geometry, MapStyleDefaultLight /* mapStyle */, frameBuffer), ());
TEST(maps::GenerateChartByPoints(width, height, geometry, slopes, MapStyleDefaultLight /* mapStyle */, frameBuffer), ());
TEST_EQUAL(frameBuffer.size(), width * height * kAltitudeChartBPP, ());
TEST(IsColor(frameBuffer, 0 /* startColorIdx */, 30 /* expectedR */, 150 /* expectedG */,