You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

679 lines
15 KiB
C++

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

#pragma once
#include "DrawOperator/Xy.h"
#include "DrawOperator/one.h"
#include "Util.h"
class CTextBuilder {
public:
CTextBuilder& FontLocation(double x, double y)
{
m_x = x;
m_y = y;
return *this;
}
CTextBuilder& FontSize(double width, double height)
{
m_width = width;
m_height = height;
return *this;
}
CTextBuilder& Text(const CString& text)
{
m_text = text;
return *this;
}
std::unique_ptr<CText> Build() const
{
std::unique_ptr<CText> pText = std::make_unique<CText>();
const CString formatStr = "%lf,%lf,0.00000000 98\n"
"{\n"
"%s\n"
"}\n"
"%lf %lf 0.000000 0 400 0 0 0 1 0 16 0 0 Times New Roman";
CString content;
content.Format(formatStr, m_x, m_y, m_text, m_width, m_height);
CMemFile memFile((BYTE*)content.GetString(), content.GetLength());
pText->Read(memFile, CUR_VERSION);
return pText;
}
private:
double m_x = 0.0;
double m_y = 0.0;
double m_width = 21.0;
double m_height = 52.0;
CString m_text;
};
/**
* 图例 Item
*/
class LegendItem
{
public:
LegendItem(CXy& xy, CLayer* pLayer)
: m_sourceXy(xy), m_pLayer(pLayer)
{
assert(m_pLayer != nullptr);
}
/**
* 获取图例名称
*
* \return
*/
const CString GetName() const
{
return Path::GetFileNameWithoutExtension(m_pLayer->GetName());
}
/**
* 设置图例占用范围
*
* \param x 左下角 x 坐标
* \param y 左下角 y 坐标
* \param width 宽
* \param height 高
*/
void SetBounds(double x, double y, double width, double height)
{
m_x = x;
m_y = y;
m_width = width;
m_height = height;
}
/**
* 获取图例占用范围
*
* \param x 左下角 x 坐标
* \param y 左下角 y 坐标
* \param width 宽
* \param height 高
*/
void GetBounds(double& x, double& y, double& width, double& height) const
{
x = m_x;
y = m_y;
width = m_width;
height = m_height;
}
/**
* 生成图例
*
* \param targetXy 要生成到的图件 xy
*/
void Build(CXy& targetXy) const
{
BuildCurveRange(targetXy);
BuildElement(targetXy);
BuildName(targetXy);
}
private:
/**
* 生成图例名称
*
* \param targetXy 要生成的目标图件
*/
void BuildName(CXy& targetXy) const
{
CString name = GetName();
auto [x, y] = MeasureNameLocation(name);
std::unique_ptr<CText> pText = CTextBuilder()
.FontLocation(x, y)
.FontSize(10, 26)
.Text(name)
.Build();
CLayer* pLayer = targetXy.FindAddLayer(_T("图例"));
std::unique_ptr<COne> pOne = std::make_unique<COne>();
pOne->SetLayer(pLayer);
pOne->SetValueSafe(pText.release());
pOne->SetColor(RGB(0, 0, 0));
targetXy.AddTailOne(pOne.release());
}
/**
* 获取第一个点或曲线,如果先拿到点就返回点,先拿到曲线就返回曲线
*
* \return 成功返回点或线的 COne 指针,失败返回 nullptr
*/
COne* GetFirstPointOrCurve() const
{
CPositionList select;
m_sourceXy.GetElement(m_pLayer, select);
for (POSITION pos = select.GetHeadPosition(); pos != nullptr; select.GetNext(pos))
{
POSITION pt = select.GetAt(pos);
COne* pOne = m_sourceXy.GetAt(pt);
if (pOne->GetType() == DOUBLEFOX_POINT || pOne->GetType() == DOUBLEFOX_CURVE)
{
return pOne;
}
}
return nullptr;
}
/**
* 生成图例内容
*
* \param targetXy
*/
void BuildElement(CXy& targetXy) const
{
COne* pOne = GetFirstPointOrCurve();
if (pOne == nullptr)
{
return;
}
std::unique_ptr<COne> pNewOne = std::make_unique<COne>();
*pNewOne = *pOne;
CRect8 oldRange;
oldRange.SetRect(1e100, -1e100, -1e100, 1e100);
pOne->GetRange(oldRange);
if (pOne->GetType() == DOUBLEFOX_POINT) // 如果是点,将它放在新区域的中心
{
CPointNameEx* pPoint = pNewOne->GetValueSafe<CPointNameEx>();
assert(pPoint != nullptr);
CenterPointInRange(*pPoint, CanvasRect());
}
else if (pOne->GetType() == DOUBLEFOX_CURVE) // 如果是曲线,将曲线缩小并移到新区域中
{
CCurveEx* pCurve = pNewOne->GetValueSafe<CCurveEx>();
assert(pCurve != nullptr);
CenterCurveInRange(*pCurve, CanvasRect());
}
CLayer* pLayer = targetXy.FindAddLayer(m_pLayer->GetPathName());
CopyLayerHowtoViewTo(m_pLayer, pLayer);
pNewOne->SetLayer(pLayer);
pNewOne->SetColor(RGB(0, 0, 0));
targetXy.AddTailOne(pNewOne.release());
}
/**
* 将点放到目标区域中心
*
* \param point
* \param range
*/
void CenterPointInRange(CPointNameEx& point, const CRect8& range) const
{
point.x0 = range.left + (range.right - range.left) / 2;
point.y0 = range.bottom + (range.top - range.bottom) / 2;
point.SetName(""); // 清空点的名称
}
/**
* 将曲线放到目标区域中心
*
* \param curve
* \param range
*/
void CenterCurveInRange(CCurveEx& curve, const CRect8& range) const
{
CRect8 oldRange;
oldRange.SetRect(1e100, -1e100, -1e100, 1e100);
curve.GetRange(oldRange);
CRect8 newRange = ScaleRect(range, 0.9);
for (int i = 0; i < curve.num; i++)
{
auto [x, y] = ScalePoint(curve.x[i], curve.y[i], oldRange, newRange);
curve.x[i] = x;
curve.y[i] = y;
}
curve.GetLocation();
}
/**
* 绘制图例包围线
*
* \param targetXy
*/
void BuildCurveRange(CXy& targetXy) const
{
std::unique_ptr<CCurveEx> pCurve = std::make_unique<CCurveEx>();
pCurve->Create(5);
// 将最下面的 y 向上提升
double y = m_y + m_height * (1 - m_canvasHeightRatio);
pCurve->x[0] = m_x;
pCurve->y[0] = y;
pCurve->x[1] = m_x + m_width;
pCurve->y[1] = y;
pCurve->x[2] = m_x + m_width;
pCurve->y[2] = m_y + m_height;
pCurve->x[3] = m_x;
pCurve->y[3] = m_y + m_height;
pCurve->x[4] = m_x;
pCurve->y[4] = y;
pCurve->GetLocation();
CLayer* pLayer = targetXy.FindAddLayer(_T("图例"));
std::unique_ptr<COne> pOne = std::make_unique<COne>();
pOne->SetValueSafe(pCurve.release());
pOne->SetLayer(pLayer);
pOne->SetColor(RGB(0, 0, 0));
targetXy.AddTailOne(pOne.release());
}
/**
* 将点从一个矩形范围缩放到另一个矩形范围
*
* \param x x 坐标
* \param y y 坐标
* \param oldRange 旧的范围
* \param newRange 新的范围
* \return 缩放后的 x y 坐标
*/
std::pair<double, double> ScalePoint(double x, double y, const CRect8& oldRange, const CRect8& newRange) const
{
double xMin = oldRange.left;
double xMax = oldRange.right;
double yMin = oldRange.bottom;
double yMax = oldRange.top;
double newXMin = newRange.left;
double newXMax = newRange.right;
double newYMin = newRange.bottom;
double newYMax = newRange.top;
double newX = ((x - xMin) / (xMax - xMin)) * (newXMax - newXMin) + newXMin;
double newY = ((y - yMin) / (yMax - yMin)) * (newYMax - newYMin) + newYMin;
return { newX, newY };
}
/**
* 获取图例绘制区域
*
* \return
*/
CRect8 CanvasRect() const
{
return CRect8(m_x, m_y + m_height, m_x + m_width, m_y + m_height * (1 - m_canvasHeightRatio));
}
/**
* 将矩形向中心缩放
*
* \param rect 要缩放的矩形
* \param scaleFactor 缩放系数
* \return
*/
CRect8 ScaleRect(const CRect8& rect, double scaleFactor) const
{
assert(scaleFactor > 0);
double width = rect.right - rect.left;
double height = rect.top - rect.bottom;
double newWidth = width * scaleFactor;
double newHeight = height * scaleFactor;
double left = rect.left + (width - newWidth) / 2;
double top = rect.top - (height - newHeight) / 2;
double right = rect.right - (width - newWidth) / 2;
double bottom = rect.bottom + (height - newHeight) / 2;
return CRect8(left, top, right, bottom);
}
/**
* 测量名称该显示的 x, y 位置
*
* \param text
* \return
*/
std::pair<double, double> MeasureNameLocation(const CString& name) const
{
double x = m_x + m_width / 2; // 要根据文字的宽度计算 x 的位置,目前把 x 设置到中间位置,貌似文本就显示在中间
double height = m_height * (1 - m_canvasHeightRatio); // 这是名字显示区域高度
double y = m_y + height; // y 抬升,这个位置是我们留给文字显示的区域最高处
y -= height * m_nameOffsetRatio; // 再移动到文字显示位置
return { x, y };
}
// 由于我们大地坐标是笛卡尔坐标系,所以这里的 x y 是左下角位置,这是行业一般惯例
double m_x = 0.0; // 左下 x
double m_y = 0.0; // 左下 y
double m_width = 2.0; // 宽度
double m_height = 1.0; // 高度
double m_nameOffsetRatio = 7.0 / 52.0; // 名称显示位置,相对于名称高度占比
double m_canvasHeightRatio = 65.0 / 117.0; // 绘制区域高度和整个高度的比例
CXy& m_sourceXy; // 原图件xy即要生成符号的图件 xy
CLayer* m_pLayer = nullptr; // 要生成符号的图层
};
/**
* 图例
*
*/
class Legend
{
public:
/**
* 构造函数
*
* \param title 标题
* \param columns 多少列
* \param layers 哪些图层要生成图例
*/
Legend(CXy &xy, double x, double y, double width, const CString& title, int columns, const std::vector<CString>& layers = {})
: m_sourceXy(xy), m_x(x), m_y(y), m_width(width), m_title(title), m_columns(columns), m_layers(layers)
{
for (const CString& layer : layers)
{
CLayer* pLayer = m_sourceXy.FindLayer(layer);
if (pLayer != nullptr)
{
m_boxes.emplace_back(m_sourceXy, pLayer);
}
}
}
/**
* 构造图例
*
* \return 返回构造好的图例指针
*/
std::unique_ptr<CInsertBlock> Build()
{
ComputeLayout();
std::unique_ptr<CXy> pNewXy = std::make_unique<CXy>();
pNewXy->color = 0;
// 生成图例边框
BuildCurveRange(*pNewXy);
// 生成图例标题
BuildTitle(*pNewXy);
// 生成图例 item
for (auto& box : m_boxes)
{
box.Build(*pNewXy);
}
// 创建 Block
std::unique_ptr<CInsertBlock> pBlock = std::make_unique<CInsertBlock>();
pBlock->SetXy(pNewXy.release());
return pBlock;
}
/**
* 计算每个图例占的空间
*
*/
void ComputeLayout()
{
size_t rows = m_boxes.size() / m_columns + (m_boxes.size() % m_columns == 0 ? 0 : 1);
size_t columns = m_columns;
// 计算出列间隙宽度
double horizontalSpacing = CalcHorizontalSpacing(m_width, static_cast<int>(columns), m_boxWidthSpacingRatio);
// 计算图例的宽度
m_boxWidth = horizontalSpacing * m_boxWidthSpacingRatio;
// 计算图例的高度,设计宽高比为 10 比 8
m_boxHeight = m_boxWidth * m_widthHeightRatio;
m_titleHeight = m_boxWidth * m_titleWidthRatio;
// 计算图例高度
m_height = m_boxHeight * rows + m_titleHeight;
for (size_t i = 0; i < m_boxes.size(); i++)
{
size_t row = i / columns;
size_t column = i % columns;
// x 偏移量由前面的列宽加列间隙组成
double xOffset = column * m_boxWidth + (column + 1) * horizontalSpacing;
double yOffset = (row + 1) * m_boxHeight;
// y 则麻烦的多
// 我们先要计算出图例框最高的位置
// 然后减去标题高度
// 再向下减去具体的位置
// 由于我们要计算的 y 是方框左下角的位置,所以还要再减上 boxHeight
yOffset = m_height - yOffset - m_titleHeight;
m_boxes[i].SetBounds(xOffset, yOffset, m_boxWidth, m_boxHeight);
}
OffsetBoxes();
}
double GetWidth() const
{
return m_width;
}
double GetHeight() const
{
return m_height;
}
private:
/**
* 前面的计算是基于 0,0 为左下坐标开始计算的,现在要根本 m_x, m_y 坐标进行偏移
*
*/
void OffsetBoxes()
{
for (auto& box : m_boxes)
{
double x = 0.0;
double y = 0.0;
double width = 0.0;
double height = 0.0;
box.GetBounds(x, y, width, height);
box.SetBounds(m_x + x, m_y + y, width, height);
}
}
// 将笛卡尔坐标系的矩形向中心缩小
std::tuple<double, double, double, double> ScaleRect(double x, double y, double width, double height, double scaleFactor)
{
assert(scaleFactor > 0 && scaleFactor <= 1);
double newWidth = width * scaleFactor;
double newHeight = width * scaleFactor;
double newX = x + ((width - newWidth) / 2);
double newY = y + ((height - newHeight) / 2);
return { newX, newY, newWidth, newHeight };
}
/**
* 绘制最外围的曲线
*
* \param targetXy 要绘制的目标图件
*/
void BuildCurveRange(CXy& targetXy) const
{
std::unique_ptr<CCurveEx> pCurve = std::make_unique<CCurveEx>();
pCurve->Create(5);
pCurve->x[0] = m_x;
pCurve->y[0] = m_y;
pCurve->x[1] = m_x + m_width;
pCurve->y[1] = m_y;
pCurve->x[2] = m_x + m_width;
pCurve->y[2] = m_y + m_height;
pCurve->x[3] = m_x;
pCurve->y[3] = m_y + m_height;
pCurve->x[4] = m_x;
pCurve->y[4] = m_y;
pCurve->GetLocation();
CLayer* pLayer = targetXy.FindAddLayer(_T("图例"));
std::unique_ptr<COne> pOne = std::make_unique<COne>();
pOne->SetValueSafe(pCurve.release());
pOne->SetLayer(pLayer);
pOne->HowToViewCurve = CreateBorderHowToView().release();
targetXy.AddTailOne(pOne.release());
}
/**
* 生成“图例”这个标题
*
* \param targetXy 要绘制的目标图件
*/
void BuildTitle(CXy& targetXy) const
{
double x = m_x + m_width * 0.5;
double y = m_y + m_height - m_titleHeight * m_titleOffsetRatio;
std::unique_ptr<CText> pText = CTextBuilder()
.FontLocation(x, y)
.FontSize(21, 52)
.Text("图 例")
.Build();
CLayer* pLayer = targetXy.FindAddLayer(_T("图例"));
std::unique_ptr<COne> pOne = std::make_unique<COne>();
pOne->SetLayer(pLayer);
pOne->SetValueSafe(pText.release());
pOne->SetColor(RGB(0, 0, 0));
targetXy.AddTailOne(pOne.release());
}
/**
* 添加块到原图件
*
* \param pBlock 组合后的图例块
*/
void AddBlock(std::unique_ptr<CInsertBlock> pBlock)
{
std::unique_ptr<COne> pOne = std::make_unique<COne>();
CLayer *pLayer = m_sourceXy.FindAddLayer(_T("图例"));
pOne->SetValueSafe(pBlock.release());
pOne->SetLayer(pLayer);
m_sourceXy.AddTailOne(pOne.release());
}
/**
* 创建图例最外围边框线的修饰
*
* \return
*/
std::unique_ptr<CHowToViewCurve> CreateBorderHowToView() const
{
std::unique_ptr<CCurvePropertiesEx> prop1 = std::make_unique<CCurvePropertiesEx>();
prop1->m_size = 0;
prop1->color = abs(16777215);
prop1->m_nFlags = 65568;
prop1->m_SmoothStep = 0;
prop1->m_offset = 0;
prop1->m_stuCurveWave.T = 0;
prop1->m_stuCurveWave.T = 0;
prop1->m_stuCurveWave.A = 0;
prop1->m_stuCurveWave.dnum = 0;
std::unique_ptr<CCurvePropertiesEx> prop2 = std::make_unique<CCurvePropertiesEx>();
prop2->m_size = 0;
prop2->color = abs(0);
prop2->m_nFlags = 65536;
prop2->m_SmoothStep = 0;
prop2->m_offset = 0;
prop2->m_stuCurveWave.T = 0;
prop2->m_stuCurveWave.T = 0;
prop2->m_stuCurveWave.A = 0;
prop2->m_stuCurveWave.dnum = 0;
std::unique_ptr<CHowToViewCurve> pHowToViewCurve = std::make_unique<CHowToViewCurve>();
pHowToViewCurve->Add(prop1.release());
pHowToViewCurve->Add(prop2.release());
return pHowToViewCurve;
}
/**
* 根据 总宽度,列数,列宽和间隙比例 计算间隙宽度
*
* \param width 宽度
* \param n 多少列
* \param ratio 列和间隙的宽度比例
* \return
*/
double CalcHorizontalSpacing(double width, int n, double ratio) const
{
// 总共 n 列,间隙为 n + 1 列
// (n + 1) * space + n * boxWidth = width
// boxWidth = ratio * space
// (n + 1) * space + n * (ratio * space) = width
// (n + 1 + n * ratio) * space = width
// space = width / (n + 1 + n * ratio)
return width / (n + 1 + n * ratio);
}
// 由于我们大地坐标是笛卡尔坐标系,所以这里的 x y 是左下角位置,这是行业一般惯例
double m_x = 0.0;
double m_y = 0.0;
double m_width = 512.0;
double m_height = 421.0;
CString m_title;
// 每列间隔和图例 Item 宽度为 46 比 106
double m_boxWidthSpacingRatio = 106.0 / 46.0;
// 图列宽高比
double m_widthHeightRatio = 106.0 / 117.0;
// 标题栏高度与图例宽度的比例
double m_titleWidthRatio = 77.0 / 106.0;
// 标题起始位置相对整个标题框高度占比
double m_titleOffsetRatio = 10.0 / 77.0;
double m_boxWidth = 0.0;
double m_boxHeight = 0.0;
double m_titleHeight = 0.0;
int m_columns = 3;
std::vector<CString> m_layers;
std::vector<LegendItem> m_boxes;
CXy& m_sourceXy;
};