При обработке данных в предметных областях, связанных с научной деятельностью, часто возникает необходимость в построении и визуализации функции двух независимых переменных. Типичным примером является необходимость визуального представления результатов решения двумерных дифференциальных уравнений в частных производных, получаемых в виде так называемых сеточных функций.
Предлагается простой класс для построения линий уровня (изолиний) функции: Z=F(X,Y) в виде линий на плоскости X-Y, удовлетворяющих уравнениям Z=const (где const — набор заданных значений).
Предполагается, что функция Z задана в виде массива z[J,K] на произвольной сетке с четырехугольными ячейками. Сетка задается двумя массивами x[J,K], y[J,K], где J и K размеры сетки.
Значения функции определены в углах четырехугольной ячейки. В каждой ячейке проверяется прохождение рассчитываемой линии уровня через ее грани и, при условии, что линия проходит через ячейку, вычисляются координаты пересечения линии уровня с гранями. Внутри ячейки линия проводится прямолинейным отрезком.
Исходный текст снабжен подробными комментариями.
Файл LinesLevels.cs:
using System.Collections.Generic;
using System.Linq;
using System.Windows;
namespace WpfLinesLevels
{
public class LinesOfLevels
{
private int J, K;
private double[,] X;
private double[,] Y;
private double[,] Z;
// Список изолиний
public List<LineLevel> Lines { get; set; }
/// <summary>
/// Подготовка
/// </summary>
/// <param name="_levels">Массив уровней</param>
/// <param name="_x">Координаты X области</param>
/// <param name="_y">Координаты Y области</param>
/// <param name="_z">Сеточная функция</param>
public LinesOfLevels(double[] _levels, double[,] _x, double[,] _y, double[,] _z)
{
Lines = new List<LineLevel>(_levels.Count());
foreach (double l in _levels)
{
Lines.Add(new LineLevel(l));
}
X = _x;
Y = _y;
Z = _z;
J = X.GetLength(0);
K = X.GetLength(1);
}
/// <summary>
/// Расчет изолиний.
/// </summary>
public void Calculate()
{
for (int j = 0; j < J - 1; j++)
for (int k = 0; k < K - 1; k++)
{
Ceil ir = new Ceil(j, k, X, Y, Z);
for (int l = 0; l < Lines.Count(); l++)
ir.AddIntoLineLevel(Lines[l]);
}
}
}
/// <summary>
/// Одна изолиния
/// </summary>
public class LineLevel
{
// Список точек изолинии в виде пар точек
// принадлежащих одной четырехугольной ячеейке
public List<PairOfPoints> Pairs { get; set; }
// Уровень изолинии
public double Level { get; set; }
public LineLevel(double _level)
{
Level = _level;
Pairs = new List<PairOfPoints>();
}
}
/// <summary>
/// Пара точек изолинии, принадлежащая одной ячейке
/// </summary>
public class PairOfPoints
{
public List<Point> Points { get; set; }
public PairOfPoints() { Points = new List<Point>(); }
}
/// <summary>
/// Угол ячейки.
/// Индексы для определения одного угла четырехугольной ячейки
/// </summary>
internal struct Dot
{
internal int j { get; set; }
internal int k { get; set; }
internal Dot(int _j, int _k) { j = _j; k = _k; }
}
/// <summary>
/// Четырехугольная ячейка сетки. Определяет текущую ячейку.
/// Рассчитывает отрезки изолиний в ячейке
/// </summary>
internal class Ceil
{
// Углы ячейки
private Dot[] d = new Dot[4];
// Координатные точки углов
private Point[] r = new Point[4];
// Массивы координат всей области
private double[,] X;
private double[,] Y;
// Массив сеточной функции
private double[,] Z;
/// <summary>
/// Определение ячейки
/// Определяется левым нижним углом. Циклы перебора индексов должны быть на 1 меньше размерностей J,K массивов
/// </summary>
/// <param name="_j">j - индекс левого нижнего угла</param>
/// <param name="_k">k - индекс левого нижнего угла</param>
/// <param name="_x">Массив X[J,K]</param>
/// <param name="_y">Массив Y[J,K]</param>
/// <param name="_z">Массив сеточной функции Z[J,K]</param>
internal Ceil(int _j, int _k, double[,] _x, double[,] _y, double[,] _z)
{
d[0] = new Dot(_j, _k);
d[1] = new Dot(_j + 1, _k);
d[2] = new Dot(_j + 1, _k + 1);
d[3] = new Dot(_j, _k + 1);
X = _x;
Y = _y;
Z = _z;
r[0] = dotPoint(d[0]);
r[1] = dotPoint(d[1]);
r[2] = dotPoint(d[2]);
r[3] = dotPoint(d[3]);
}
/// <summary>
/// Определение координатной точки Point угла
/// </summary>
/// <param name="_d">Угол, заданный стуктурой Dot</param>
/// <returns></returns>
private Point dotPoint(Dot _d) { return new Point(X[_d.j, _d.k], Y[_d.j, _d.k]); }
/// <summary>
/// Определение функции в заданном углу
/// </summary>
/// <param name="_d">Угол, заданный стуктурой Dot</param>
/// <returns></returns>
private double dotZ(Dot _d) { return Z[_d.j, _d.k]; }
/// <summary>
/// Определение пары точек, через которые проходит линия уровня
/// Точки на границах ячейки определяются линейной интераоляцией.
/// </summary>
/// <param name="_l">Значение уровня функции</param>
/// <returns></returns>
private PairOfPoints ByLevel(double _l)
{
PairOfPoints p = new PairOfPoints();
// Ребро 0
if ((dotZ(d[0]) >= _l && dotZ(d[1]) < _l) || (dotZ(d[1]) > _l && dotZ(d[0]) <= _l))
{
double t = (_l - dotZ(d[1])) / (dotZ(d[0]) - dotZ(d[1]));
double x = r[0].X * t + r[1].X * (1 - t);
double y = r[0].Y * t + r[1].Y * (1 - t);
p.Points.Add(new Point(x, y));
}
// Ребро 1
if ((dotZ(d[1]) >= _l && dotZ(d[2]) < _l) || (dotZ(d[2]) > _l && dotZ(d[1]) <= _l))
{
double t = (_l - dotZ(d[2])) / (dotZ(d[1]) - dotZ(d[2]));
double x = r[1].X * t + r[2].X * (1 - t);
double y = r[1].Y * t + r[2].Y * (1 - t);
p.Points.Add(new Point(x, y));
if (p.Points.Count == 2) return p;
}
// Ребро 2
if ((dotZ(d[2]) >= _l && dotZ(d[3]) < _l) || (dotZ(d[3]) > _l && dotZ(d[2]) <= _l))
{
double t = (_l - dotZ(d[3])) / (dotZ(d[2]) - dotZ(d[3]));
double x = r[2].X * t + r[3].X * (1 - t);
double y = r[2].Y * t + r[3].Y * (1 - t);
p.Points.Add(new Point(x, y));
if (p.Points.Count == 2) return p;
}
// Ребро 3
if ((dotZ(d[3]) >= _l && dotZ(d[0]) < _l) || (dotZ(d[0]) > _l && dotZ(d[3]) <= _l))
{
double t = (_l - dotZ(d[0])) / (dotZ(d[3]) - dotZ(d[0]));
double x = r[3].X * t + r[0].X * (1 - t);
double y = r[3].Y * t + r[0].Y * (1 - t);
p.Points.Add(new Point(x, y));
}
return p;
}
/// <summary>
/// Добавление пары точек в линию уравня
/// </summary>
/// <param name="_lL">Линия уровня</param>
internal void AddIntoLineLevel(LineLevel _lL)
{
PairOfPoints lp = ByLevel(_lL.Level);
if (lp.Points.Count > 0) _lL.Pairs.Add(lp);
}
}
}
Для демонстрации работы класса предлагается небольшое тестовое приложение WPF, которое строит линии уровня для функции вида: z = x^2 + y^2 на сетке 10 на 10.
Файл MainWindow.xaml:
<Window x:Class="WpfLinesLevels.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:WpfLinesLevels"
mc:Ignorable="d"
Title="Изолинии Z = X^2+Y^2" Height="590" Width="525">
<Grid Name="grid1">
</Grid>
</Window>
И файл кода MainWindow.xaml.cs:
using System.Linq;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using System.Windows.Shapes;
namespace WpfLinesLevels
{
/// <summary>
/// Логика взаимодействия для MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
private double Xmax;
private double Xmin;
private double Ymax;
private double Ymin;
private double xSt;
private double ySt;
public MainWindow()
{
InitializeComponent();
// Определение уровней, которые будут отображаться
double[] levels = { 5, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100 };
double[,] X = new double[10, 10];
double[,] Y = new double[10, 10];
double[,] Z = new double[10, 10];
// Переменные для пересчета физических координат в экранные
Xmax = 10;
Xmin = 0;
Ymax = 10;
Ymin = 0;
xSt = 525 / (Xmax - Xmin);
ySt = 525 / (Ymax - Ymin);
// Определение массивов координат и функции
for (int k = 0; k < 10; k++)
for (int j = 0; j < 10; j++)
{
X[j, k] = j;
Y[j, k] = k;
Z[j, k] = j * j + k * k;
}
// Создание изолиний
LinesOfLevels lol = new LinesOfLevels(levels, X, Y, Z);
// Их расчет
lol.Calculate();
// Построение
DrowLevelLine(lol, X, Y);
}
/// <summary>
/// Метод построения изолиний
/// </summary>
/// <param name="lL">Расчитанный объект с изолиниями</param>
/// <param name="x">массив X координат</param>
/// <param name="y">массив Y координат</param>
private void DrowLevelLine(LinesOfLevels lL, double[,] x, double[,] y)
{
Canvas can = new Canvas();
foreach (LineLevel l in lL.Lines)
{
foreach (PairOfPoints pp in l.Pairs)
{
if (pp.Points.Count() == 2)
{
Line pl = new Line();
pl.Stroke = new SolidColorBrush(Colors.BlueViolet);
pl.X1 = xCalc(pp.Points[0].X);
pl.X2 = xCalc(pp.Points[1].X);
pl.Y1 = yCalc(pp.Points[0].Y);
pl.Y2 = yCalc(pp.Points[1].Y);
can.Children.Add(pl);
}
}
}
can.Margin = new Thickness(10, 10, 10, 10);
can.VerticalAlignment = VerticalAlignment.Stretch;
can.HorizontalAlignment = HorizontalAlignment.Stretch;
grid1.Children.Add(can);
}
/// <summary>
/// Пересчет физической координаты X в экранную
/// </summary>
/// <param name="_x">Физическая кордината X</param>
/// <returns>Экранная координата X</returns>
private double xCalc(double _x)
{
return xSt * (_x - Xmin);
}
/// <summary>
/// Пересчет физической координаты Y в экранную
/// </summary>
/// <param name="_x">Физическая кордината Y</param>
/// <returns>Экранная координата Y</returns>
private double yCalc(double _y)
{
return ySt * (Ymax - _y);
}
}
}
Результат работы тестового примера представлен на рисунке:
Спасибо за внимание!
Автор: Shef1954