пятница, 21 января 2011 г.

Совместное использование классов GraphingBoard и SimpleGraph

Метод DrawGraph для приложения «Построение графиков производной»

Данный пример покажет возможность непосредственного использования метода drawGraph класса GraphingBoard. Этот метод, с помощью которого вы можете создать массив парных значений (в математических единицах), а затем передать его объекту класса GraphingBoard для непосредственной отрисовки на графике. Т.к. методы класса SimpleGraph позволяют строить лишь несколько предопределенных типов графиков (графики функций в декартовых и полярных координатах, а также параметрических функций), то понимание метода drawGraph класса GraphingBoard даст Вам более гибкие возможности для построения собственных графиков.

Настройка сцены

Настройка сцены заключается в расположении на ней кнопки, слайдера, пяти текстовых полей для ввода текста и одного динамического текстового поля, а также ряда статических полей, поясняющих значение полей ввода (см. рис. 1). Для кнопки назначаем имя btnGraph, для слайдера – slX. Имена пяти текстовых полей: txtFunc, txtXmain, txtXmax, txtYmin, txtYmax. Динамическому текстовому полю, расположенному внизу, задаем имя txtSlope, его ширина должна быть достаточна, чтобы отобразить строку "Slope = -999.99" (уклон касательной).


Добавляем следующий код (здесь я опускаю импорт необходимых классов, смотрите исходники):
public class Main extends Sprite 
{
 // Размеры объекта SimpleGraph 
 private var bSize:Number = 300;
  
 // Длина (в пикселях) касательной
 private var lineLength:Number = 100;
 
 // Переменные для настройки графика 
 private var xticks:Number = 1;
 private var yticks:Number = 1;
 private var xgrids:Number = 1;
 private var ygrids:Number = 1;
 
 // Необходимые текстовые поля
 private var txtFunc:TextField;
 private var txtXmin:TextField;
 private var txtXmax:TextField;
 private var txtYmin:TextField;
 private var txtYmax:TextField;
  
 private var txtSlope:TextField;
 
 private var gderiv:SimpleGraph;
 
 private var slX:HSlider;
 private var btnGraph:PushButton;
 
 // Массивы, хранящие данные о координатах точек 
 // графика функции и ее производной 
 private var arrPx:Array;
 private var arrSlopes:Array;
  
 // Маркеры на графике
 private var shTan:Shape = new Shape();
 private var shPoint:Shape = new Shape(); 
 
Далее, в методе init мы создаем все необходимые текстовые поля, кнопку (этот момент я опускаю) и слайдер, объект SimpleGraph и маркеры, задаем исходное состояние приложения:
gderiv = new SimpleGraph(bSize,bSize);
gderiv.x = 40;
gderiv.y = 50;   
// Чтобы с помощью слайдера можно было отслеживать точку 
// на графике функции, число точек графика должно быть 
// равно диапазону значений слайдера
gderiv.setNumPoints(bSize);
gderiv.board.changeBackColor(0xFFEEEE);
gderiv.board.setCoordsBoxFormat(0xEEEEEE, 0xDDDDDD, 
    0x000000, 12);
gderiv.board.setGrid(xgrids,ygrids);
gderiv.board.setTicks(xticks,yticks,10,10);
addChild(gderiv);
   
slX = new HSlider(this, gderiv.x, gderiv.y + bSize + 10, 
   update);
slX.setSliderParams(0, bSize, 0);
slX.width = bSize;
slX.visible = false;
   
shTan.graphics.lineStyle(1,0x000000);
shTan.graphics.beginFill(0xCC0000,0.8);
shTan.graphics.drawRect(-2,-lineLength/2, 4, lineLength);
shTan.graphics.endFill();
shTan.graphics.beginFill(0xAAAAAA);
shTan.graphics.drawCircle(0,0,4);
shTan.graphics.endFill();
shTan.visible = false;

shPoint.graphics.lineStyle(1,0);
shPoint.graphics.beginFill(0x007700);
shPoint.graphics.drawCircle(0,0,5);
shPoint.graphics.endFill();
shPoint.visible = false;
 
gderiv.addChildToBoard(shTan);
gderiv.addChildToBoard(shPoint);
   
// Исходное состояние графика
txtFunc.text = "sin(x) + cos(x)";
txtXmin.text = "-pi";
txtXmax.text = "pi";
txtYmin.text = "-1.5";
txtYmax.text = "1.5";
drawGraph();
}

Маркер shTan – это вытянутый прямоугольник с точкой регистрации (внутренней точкой отсчета), расположенной в его центре. Чтобы использовать данные об углах наклона кривой, хранящихся в массиве gderiv.getPixData(), для точного позиционирования маркера необходимо, чтобы в исходном состоянии маркер должен быть вертикальным. Итак, маркер shTan будет показывать касательную к графику функции f, введенной пользователем, а маркер shPoint – точку на графике производной функции f.
Для добавления маркеров shTan и shPoint в список отображения объекта gderiv.board мы используем метод addChildtoBoard. Если бы мы использовали метод gderiv.addChild, то к этим маркерам не применилась бы маска объекта gderiv.board, и они оставались бы видимыми даже при выходе за область отрисовки графика:
gderiv.board.addChildToBoard(shTan); 
gderiv.board.addChildToBoard(shPoint); 
 
Функция setup выполняет настройку объекта SimpleGraph (при отсутствии ошибок ввода) и отрисовывает сетку, деления и значения делений.
private function setup():void {
 slX.visible = false;
 shTan.visible = false;
 shPoint.visible = false;

 gderiv.setWindow(txtXmin.text, txtXmax.text, txtYmin.text, 
    txtYmax.text);
 
 if (gderiv.hasError()) return; 

 gderiv.board.drawGrid();
 gderiv.board.drawAxes();
 gderiv.board.drawTicks();
 gderiv.board.addLabels();
} 
 
Функция update является обработчиком события изменения положения слайдера slX, изменяет положения маркеров shPoint и shTan:
private function update(evt:Event):void {
 var i:int = slX.value;
 shTan.x = arrPx[i][0];
 shTan.y = arrPx[i][1];
 shTan.rotation = arrPx[i][2];
  
 shPoint.x = shTan.x;
 shPoint.y = gderiv.board.ytoPix(arrSlopes[i][1]);

 txtSlope.text = "Slope = " + arrSlopes[i][1].toFixed(2);
} 
 
При нажатии на кнопку btnGraph, а также при запуске приложения вызывается функция drawGraph, отвечающая за отрисовку графиков функций:
private function btnGraphClicked(mevt:MouseEvent):void {
 drawGraph();
}
  
private function drawGraph():void {
 var i:int;
 var thisX:Number;
 // Если нет функции для построения графика, 
 // то и делать ничего не нужно
 if (txtFunc.text.length == 0) return;

 setup();
 // при возникновении ошибки график мы не строим
 if (gderiv.hasError())  return;
 // Вычерчиваем график функции, введенной в поле txtFunc 
 gderiv.graphRectangular(txtFunc.text,"x",1,2,0x0000AA);

 if (gderiv.hasError())  return;
   
 gderiv.board.drawGrid();
 gderiv.board.drawAxes();
 gderiv.board.drawTicks();
 gderiv.board.addLabels();
 //  arrPx – это массив, состоящий из массивов по три элемента: 
 // координаты точек и углы наклона касательных в данных точках
 arrPx = gderiv.getPixData(1);
 arrSlopes = new Array(arrPx.length);
 // Заполняем массив точек, по которым будет 
 // строиться график производной функции
 for (i=0; i < arrSlopes.length; i++) {
  thisX = gderiv.board.xfromPix(arrPx[i][0]);
  arrSlopes[i] = [thisX, getSlope(thisX)];
 }
 // Вычерчиваем график производной функции толщиной в 2 
 // пикселя на 2 уровне. 
 // График самой функции вычерчивается на уровне 1 
 gderiv.board.drawGraph(2,2,arrSlopes,0x007700);
   
 shTan.visible = true;
 shPoint.visible = true;
 slX.visible = true;
   
 slX.value = 0;
 // Располагаем маркеры shTan и shPoint 
 // и обновляем отображаемый текст 
 shTan.x = arrPx[0][0];
 shTan.y = arrPx[0][1]; 
 shTan.rotation = arrPx[0][2];
   
 shPoint.x = shTan.x;
 shPoint.y = gderiv.board.ytoPix(arrSlopes[0][1]);

 txtSlope.text = "Slope = " + arrSlopes[0][1].toFixed(2);
}
 
Вспомогательная функция getSlope используется для вычисления приблизительного значения производной в точке xval (используемый здесь прием должен быть знаком Вам из курса школьной алгебры):
private function getSlope(xval:Number):Number {
 var dx:Number = 0.0000001;
 var y1:Number = gderiv.rectangularValueAt(1,xval - dx);
 var y2:Number = gderiv.rectangularValueAt(1,xval + dx);
 return ((y2 - y1)/(2*dx));
} 
 
Использование класса RangeParser для отрисовки графиков в полярных координатах 

В этом примере мы построим простой график функции r = f(θ) в полярных координатах (см. рис. 2). Для этого мы используем метод drawPolar объекта SimpleGraph, но кроме этого нам понадобится кое-что еще: чтобы слайдер мог изменять значения координаты θ в заданных пользователем пределах, мы воспользуемся возможностями класса RangeParser.

Настройка сцены 

На сцене вокруг области отрисовки графиков необходимо расположить множество объектов (текстовые поля для ввода текста, динамические текстовые поля, слайдер). Сама область графиков будет имеет размеры 300х300. Расположение этих объектов не так критично, главное чтобы они не накладывались поверх графика. Пишем код Вы сразу же заметите, что мы импортируем классы из папок parsers и tools, чтобы иметь доступ к классам SimpleGraph, RangeParser и RangeObject. Объекты класса RangeParser будут использоваться для получения из текстовых полей максимальных и минимальных значений угла θ.
import flashandmath.as3.parsers.*; 
import flashandmath.as3.tools.*; 
 
В этом примере мы используем бОльший диапазон значений для слайдера, а, следовательно, и угла θ, что должно сделать график функции более гладким. Т.к. мы снова используем положение слайдера в качестве индекса в массиве отрисовываемых точек, то количество точек графика тоже должно равняться диапазону значений слайдера.
private var bSize:Number = 320;
var arrPx:Array; 
var tmin:Number; 
var tmax:Number;

// Фрагмент метода init
private function init(e:Event = null):void 
{   
 …

 // Добавляем кнопки
 btnGraph = new PushButton(this, 456, 398, "ЧЕРТИТЬ", 
      btnGraphClicked);
 btnGraph.width = 82;
 btnGraph.height = 24;
   
 btnReset = new PushButton(this, 547, 398, "СБРОС", clearGraph);
 btnReset.width = 82;
 btnReset.height = 24;
   
 // Добавляем слайдер
 slT = new HSlider(this, 50, 510, update);
 slT.setSliderParams(0, 2*bSize, 0);
 slT.width = 300;
   
 gpolar = new SimpleGraph(bSize, bSize);
 gpolar.x = 40;
 gpolar.y = 100;
 // Количество точек графика, по которым он строится,
 // должно быть равным диапазону значений слайдера
 gpolar.setNumPoints(slT.maximum);
 addChild(gpolar);
 
 // Создаем маркер точки на графике
 shTrace = new Shape();
 shTrace.graphics.lineStyle(1,0x000000);
 shTrace.graphics.drawCircle(0,0,5);
 shTrace.graphics.moveTo(-5,0);
 shTrace.graphics.lineTo(5,0);
 shTrace.graphics.moveTo(0,-5);
 shTrace.graphics.lineTo(0, 5);
 // Добавляем маркер в список отображения
 gpolar.addChildToBoard(shTrace);
   
 shTrace.visible = false;
 slT.visible = false;
 
 … 
 drawGraph();
} 
 
В функции update мы преобразовываем положение слайдера в значение угла θ, лежащее в пределах между tmin и tmax, и используем его для вычисления значения функции r. Также мы располагаем маркер на графике и используем его координаты для отображения математических координат точки. Объект класса SimpleGraph сам выполняет преобразование полярных координат в прямоугольные для построения массива координат точек arrPix.
private function update(evt:Event):void {
 var i:int = slT.value;
 var curt:Number = tmin + i*(tmax-tmin)/
    (slT.maximum - slT.minimum);
 var curr:Number = gpolar.polarValueAt(1, curt);
   
 shTrace.x = arrPix[i][0];
 shTrace.y = arrPix[i][1];

 var curx:Number = gpolar.board.xfromPix(shTrace.x);
 var cury:Number = gpolar.board.yfromPix(shTrace.y);
    
 txtX.text = curx.toFixed(2);
 txtY.text = cury.toFixed(2);
 txtT.text = curt.toFixed(2);
 txtR.text = curr.toFixed(2);
} 
 
Всю основную работу выполняет фунция drawGraph, вызываемая при щелчке по кнопке btnGraph. Создав данную функцию, мы можем вызвать ее из любого места приложения.
private function btnGraphClicked(mevt:MouseEvent):void {
 drawGraph();
} 
 
Для получения значений границ диапазона изменения координат для отрисовки графика мы используем возможности класса RangeParser. У класса RangeParser есть 2 метода: parseRangeTwo и parseRangeFour, возвращающих объект класса RangeObject . А у объектов класса RangeObject есть 3 свойства: Values (значения), errorStatus (состояние) и errorMes (сообщение об ошибке). Названия последних двух говорят сами за себя. Свойство Values является массивом, содержащим 2 (4) численных значения, (при успешном итоге их анализа).
private function drawGraph():void {
 var stR:String;
 var stMin:String;
 var stMax:String;
 var curx:Number;
 var cury:Number;
 var curt:Number;
 var curr:Number;
 // Создаем объект класса RangeParser для получения 
 // значений границ диапазона изменения координаты θ 
 var rp:RangeParser = new RangeParser();
 var ro:RangeObject;
 // Просто скрываем графические элементы до тех пор, 
 // пока не убедимся в отсутствии ошибок 
 shTrace.visible = false;
 slT.visible = false;
 txtX.text = "";
 txtY.text = "";
 txtT.text = "";
 txtR.text = "";
   
 gpolar.setWindow(txtXmin.text, txtXmax.text, 
    txtYmin.text, txtYmax.text);
   
 if (gpolar.hasError())  return; }
 setupBoard();
   
 stR = txtFunR.text;
 stMin = txtTmin.text;
 stMax = txtTmax.text;
   
 gpolar.graphPolar(stR, "theta", stMin, stMax, 1, 2, 0x0000AA);
   
 if (gpolar.hasError())  return; 
   
 // Используем объект класса RangeParser для получения 
 // численных значенией границ изменения угловой координаты, 
 // введенных в текстовые поля txtTmin и txtTmax.
 ro = rp.parseRangeTwo(stMin, stMax);
 if (ro.errorStatus == 1) return; 
 // Получаем численные значения границ графика функции
 tmin = rp.parseRangeTwo(stMin, stMax).Values[0];
 tmax = rp.parseRangeTwo(stMin, stMax).Values[1];
 // Получаем массив координат точек графика, 
 // который мы можем использовать в дальнейшем 
 arrPix = gpolar.getPixData(1);
 // Если выполнение функции не прекратилось до сих пор, 
 // то ошибок пользовательского ввода не возникло, 
 // и мы можем сделать видимыми скрытые ранее 
 // графические элементы и настроить начальное положение маркера
 
 shTrace.visible = true;
 slT.visible = true;
 slT.value = 0;
 shTrace.x = arrPix[0][0];
 shTrace.y = arrPix[0][1];
   
 curx = gpolar.board.xfromPix(shTrace.x);
 cury = gpolar.board.yfromPix(shTrace.y);
 curt = tmin;
 curr = gpolar.polarValueAt(1, curt);
   
 txtX.text = curx.toFixed(2);
 txtY.text = cury.toFixed(2);
 txtT.text = curt.toFixed(2);
 txtR.text = curr.toFixed(2);
} 
 
Щелчок по кпонке СБРОС должен очищать значения x, y, r и θ, стирать график функции и скрывать слайдер и маркер:
private function clearGraph(e:MouseEvent):void
{
 shTrace.visible = false;
 slT.visible = false;
 txtX.text = "";
 txtY.text = "";
 txtT.text = "";
 txtR.text = "";
 gpolar.removeGraph(1);
 gpolar.setWindow(txtXmin.text, txtXmax.text, 
    txtYmin.text, txtYmax.text);
} 
 
Функция setupBoard нужна для перерисовки делений и осей после вызова функции setWindow:
private function setupBoard():void {
 gpolar.board.changeBackColor(0xFFFFEE);
 gpolar.board.setCoordsBoxFormat(0xEEEEEE, 0xDDDDDD, 
      0x000000, 12);
 gpolar.board.drawAxes(); 
 gpolar.board.setTicks(1,1,10,10);
 gpolar.board.drawTicks();
} 
 
Этот пост – последний из серии переводов 12 главы книги "Flash and Math Applets: Learn by Example". В качестве бонуса кроме исходников приведенных здесь примеров прикладываю дополнительный пример, позволяющий строить графики параметрических функций. Скачать их можно здесь. Если Вас интересуют и другие возможности классов пакета flashandmath и знания английского позволяют, то в качестве дополнительных ресурсов очень рекомендую уроки с сайта www.flashandmath.com разделы Intermediate и Advanced tutorials (откуда, собственно, и были взяты все материалы).

Комментариев нет:

Отправить комментарий