PageRenderTime 62ms CodeModel.GetById 32ms RepoModel.GetById 0ms app.codeStats 0ms

/MantisBT/library/ezc/Graph/src/charts/line.php

https://bitbucket.org/crypticrod/sr_wp_code
PHP | 717 lines | 433 code | 65 blank | 219 comment | 28 complexity | 64bf2c07ad90db5b65a2ceb0c8292db2 MD5 | raw file
Possible License(s): AGPL-1.0, GPL-2.0, LGPL-2.1, GPL-3.0, LGPL-2.0, AGPL-3.0
  1. <?php
  2. /**
  3. * File containing the ezcGraphLineChart class
  4. *
  5. * @package Graph
  6. * @version 1.5
  7. * @copyright Copyright (C) 2005-2009 eZ Systems AS. All rights reserved.
  8. * @license http://ez.no/licenses/new_bsd New BSD License
  9. */
  10. /**
  11. * Class for line charts. Can make use of an unlimited amount of datasets and
  12. * will display them as lines by default.
  13. * X axis:
  14. * - Labeled axis
  15. * - Centered axis label renderer
  16. * Y axis:
  17. * - Numeric axis
  18. * - Exact axis label renderer
  19. *
  20. * <code>
  21. * // Create a new line chart
  22. * $chart = new ezcGraphLineChart();
  23. *
  24. * // Add data to line chart
  25. * $chart->data['sample dataset'] = new ezcGraphArrayDataSet(
  26. * array(
  27. * '100' => 1.2,
  28. * '200' => 43.2,
  29. * '300' => -34.14,
  30. * '350' => 65,
  31. * '400' => 123,
  32. * )
  33. * );
  34. *
  35. * // Render chart with default 2d renderer and default SVG driver
  36. * $chart->render( 500, 200, 'line_chart.svg' );
  37. * </code>
  38. *
  39. * Each chart consists of several chart elements which represents logical
  40. * parts of the chart and can be formatted independently. The line chart
  41. * consists of:
  42. * - title ( {@link ezcGraphChartElementText} )
  43. * - legend ( {@link ezcGraphChartElementLegend} )
  44. * - background ( {@link ezcGraphChartElementBackground} )
  45. * - xAxis ( {@link ezcGraphChartElementLabeledAxis} )
  46. * - yAxis ( {@link ezcGraphChartElementNumericAxis} )
  47. *
  48. * The type of the axis may be changed and all elements can be configured by
  49. * accessing them as properties of the chart:
  50. *
  51. * <code>
  52. * $chart->legend->position = ezcGraph::RIGHT;
  53. * </code>
  54. *
  55. * The chart itself also offers several options to configure the appearance.
  56. * The extended configure options are available in
  57. * {@link ezcGraphLineChartOptions} extending the {@link ezcGraphChartOptions}.
  58. *
  59. * @property ezcGraphLineChartOptions $options
  60. * Chart options class
  61. *
  62. * @version 1.5
  63. * @package Graph
  64. * @mainclass
  65. */
  66. class ezcGraphLineChart extends ezcGraphChart
  67. {
  68. /**
  69. * Array with additional axis for the chart
  70. *
  71. * @var ezcGraphAxisContainer
  72. */
  73. protected $additionalAxis;
  74. /**
  75. * Constructor
  76. *
  77. * @param array $options Default option array
  78. * @return void
  79. * @ignore
  80. */
  81. public function __construct( array $options = array() )
  82. {
  83. $this->additionalAxis = new ezcGraphAxisContainer( $this );
  84. $this->options = new ezcGraphLineChartOptions( $options );
  85. $this->options->highlightFont = $this->options->font;
  86. parent::__construct();
  87. $this->addElement( 'xAxis', new ezcGraphChartElementLabeledAxis() );
  88. $this->elements['xAxis']->position = ezcGraph::LEFT;
  89. $this->addElement( 'yAxis', new ezcGraphChartElementNumericAxis() );
  90. $this->elements['yAxis']->position = ezcGraph::BOTTOM;
  91. }
  92. /**
  93. * __get
  94. *
  95. * @param mixed $propertyName
  96. * @throws ezcBasePropertyNotFoundException
  97. * If a the value for the property options is not an instance of
  98. * @return mixed
  99. * @ignore
  100. */
  101. public function __get( $propertyName )
  102. {
  103. switch ( $propertyName )
  104. {
  105. case 'additionalAxis':
  106. return $this->additionalAxis;
  107. }
  108. return parent::__get( $propertyName );
  109. }
  110. /**
  111. * Options write access
  112. *
  113. * @throws ezcBasePropertyNotFoundException
  114. * If Option could not be found
  115. * @throws ezcBaseValueException
  116. * If value is out of range
  117. * @param mixed $propertyName Option name
  118. * @param mixed $propertyValue Option value;
  119. * @return mixed
  120. * @ignore
  121. */
  122. public function __set( $propertyName, $propertyValue )
  123. {
  124. switch ( $propertyName ) {
  125. case 'xAxis':
  126. if ( $propertyValue instanceof ezcGraphChartElementAxis )
  127. {
  128. $this->addElement( 'xAxis', $propertyValue );
  129. $this->elements['xAxis']->position = ezcGraph::LEFT;
  130. }
  131. else
  132. {
  133. throw new ezcBaseValueException( $propertyName, $propertyValue, 'ezcGraphChartElementAxis' );
  134. }
  135. break;
  136. case 'yAxis':
  137. if ( $propertyValue instanceof ezcGraphChartElementAxis )
  138. {
  139. $this->addElement( 'yAxis', $propertyValue );
  140. $this->elements['yAxis']->position = ezcGraph::BOTTOM;
  141. }
  142. else
  143. {
  144. throw new ezcBaseValueException( $propertyName, $propertyValue, 'ezcGraphChartElementAxis' );
  145. }
  146. break;
  147. default:
  148. parent::__set( $propertyName, $propertyValue );
  149. }
  150. }
  151. /**
  152. * Set colors and border for this element
  153. *
  154. * @param ezcGraphPalette $palette Palette
  155. * @return void
  156. */
  157. public function setFromPalette( ezcGraphPalette $palette )
  158. {
  159. foreach ( $this->additionalAxis as $element )
  160. {
  161. $element->setFromPalette( $palette );
  162. }
  163. parent::setFromPalette( $palette );
  164. }
  165. /**
  166. * Calculate bar chart step width
  167. *
  168. * @return void
  169. */
  170. protected function calculateStepWidth( ezcGraphChartElementAxis $mainAxis, ezcGraphChartElementAxis $secondAxis, $width )
  171. {
  172. $steps = $mainAxis->getSteps();
  173. $stepWidth = null;
  174. foreach ( $steps as $step )
  175. {
  176. if ( $stepWidth === null )
  177. {
  178. $stepWidth = $step->width;
  179. }
  180. elseif ( $step->width !== $stepWidth )
  181. {
  182. throw new ezcGraphUnregularStepsException();
  183. }
  184. }
  185. $step = reset( $steps );
  186. if ( count( $step->childs ) )
  187. {
  188. // Keep this for BC reasons
  189. $barCount = ( $mainAxis->getMajorStepCount() + 1 ) * ( $mainAxis->getMinorStepCount() - 1 );
  190. $stepWidth = 1 / $barCount;
  191. }
  192. $checkedRegularSteps = true;
  193. return $mainAxis->axisLabelRenderer->modifyChartDataPosition(
  194. $secondAxis->axisLabelRenderer->modifyChartDataPosition(
  195. new ezcGraphCoordinate(
  196. $width * $stepWidth,
  197. $width * $stepWidth
  198. )
  199. )
  200. );
  201. }
  202. /**
  203. * Render the assigned data
  204. *
  205. * Will renderer all charts data in the remaining boundings after drawing
  206. * all other chart elements. The data will be rendered depending on the
  207. * settings in the dataset.
  208. *
  209. * @param ezcGraphRenderer $renderer Renderer
  210. * @param ezcGraphBoundings $boundings Remaining boundings
  211. * @return void
  212. */
  213. protected function renderData( ezcGraphRenderer $renderer, ezcGraphBoundings $boundings, ezcGraphBoundings $innerBoundings )
  214. {
  215. // Use inner boundings for drawning chart data
  216. $boundings = $innerBoundings;
  217. $yAxisNullPosition = $this->elements['yAxis']->getCoordinate( false );
  218. // Initialize counters
  219. $nr = array();
  220. $count = array();
  221. foreach ( $this->data as $data )
  222. {
  223. if ( !isset( $nr[$data->displayType->default] ) )
  224. {
  225. $nr[$data->displayType->default] = 0;
  226. $count[$data->displayType->default] = 0;
  227. }
  228. $nr[$data->displayType->default]++;
  229. $count[$data->displayType->default]++;
  230. }
  231. $checkedRegularSteps = false;
  232. // Display data
  233. foreach ( $this->data as $datasetName => $data )
  234. {
  235. --$nr[$data->displayType->default];
  236. // Check which axis should be used
  237. $xAxis = ( $data->xAxis->default ? $data->xAxis->default: $this->elements['xAxis'] );
  238. $yAxis = ( $data->yAxis->default ? $data->yAxis->default: $this->elements['yAxis'] );
  239. // Determine fill color for dataset
  240. if ( $this->options->fillLines !== false )
  241. {
  242. $fillColor = clone $data->color->default;
  243. $fillColor->alpha = (int) round( ( 255 - $fillColor->alpha ) * ( $this->options->fillLines / 255 ) );
  244. }
  245. else
  246. {
  247. $fillColor = null;
  248. }
  249. // Ensure regular steps on axis when used with bar charts and
  250. // precalculate some values use to render bar charts
  251. //
  252. // Called only once and only when bars should be rendered
  253. if ( ( $checkedRegularSteps === false ) &&
  254. ( $data->displayType->default === ezcGraph::BAR ) )
  255. {
  256. $width = $this->calculateStepWidth( $xAxis, $yAxis, $boundings->width )->x;
  257. }
  258. // Draw lines for dataset
  259. $lastPoint = false;
  260. foreach ( $data as $key => $value )
  261. {
  262. // Calculate point in chart
  263. $point = $xAxis->axisLabelRenderer->modifyChartDataPosition(
  264. $yAxis->axisLabelRenderer->modifyChartDataPosition(
  265. new ezcGraphCoordinate(
  266. $xAxis->getCoordinate( $key ),
  267. $yAxis->getCoordinate( $value )
  268. )
  269. )
  270. );
  271. // Render depending on display type of dataset
  272. switch ( true )
  273. {
  274. case $data->displayType->default === ezcGraph::LINE:
  275. $renderer->drawDataLine(
  276. $boundings,
  277. new ezcGraphContext( $datasetName, $key, $data->url[$key] ),
  278. $data->color->default,
  279. ( $lastPoint === false ? $point : $lastPoint ),
  280. $point,
  281. $nr[$data->displayType->default],
  282. $count[$data->displayType->default],
  283. $data->symbol[$key],
  284. $data->color[$key],
  285. $fillColor,
  286. $yAxisNullPosition,
  287. ( $data->lineThickness->default ? $data->lineThickness->default : $this->options->lineThickness )
  288. );
  289. // Render highlight string if requested
  290. if ( $data->highlight[$key] )
  291. {
  292. $renderer->drawDataHighlightText(
  293. $boundings,
  294. new ezcGraphContext( $datasetName, $key, $data->url[$key] ),
  295. $point,
  296. $yAxisNullPosition,
  297. $nr[$data->displayType->default],
  298. $count[$data->displayType->default],
  299. $this->options->highlightFont,
  300. ( $data->highlightValue[$key] ? $data->highlightValue[$key] : $value ),
  301. $this->options->highlightSize + $this->options->highlightFont->padding * 2,
  302. ( $this->options->highlightLines ? $data->color[$key] : null ),
  303. ( $this->options->highlightXOffset ? $this->options->highlightXOffset : 0 ),
  304. ( $this->options->highlightYOffset ? $this->options->highlightYOffset : 0 ),
  305. 0.,
  306. ezcGraph::LINE
  307. );
  308. }
  309. break;
  310. case ( $data->displayType->default === ezcGraph::BAR ) &&
  311. $this->options->stackBars :
  312. // Check if a bar has already been stacked
  313. if ( !isset( $stackedValue[(int) ( $point->x * 10000 )][(int) $value > 0] ) )
  314. {
  315. $start = new ezcGraphCoordinate(
  316. $point->x,
  317. $yAxisNullPosition
  318. );
  319. $stackedValue[(int) ( $point->x * 10000 )][(int) $value > 0] = $value;
  320. }
  321. else
  322. {
  323. $start = $xAxis->axisLabelRenderer->modifyChartDataPosition(
  324. $yAxis->axisLabelRenderer->modifyChartDataPosition(
  325. new ezcGraphCoordinate(
  326. $xAxis->getCoordinate( $key ),
  327. $yAxis->getCoordinate( $stackedValue[(int) ( $point->x * 10000 )][(int) $value > 0] )
  328. )
  329. )
  330. );
  331. $point = $xAxis->axisLabelRenderer->modifyChartDataPosition(
  332. $yAxis->axisLabelRenderer->modifyChartDataPosition(
  333. new ezcGraphCoordinate(
  334. $xAxis->getCoordinate( $key ),
  335. $yAxis->getCoordinate( $stackedValue[(int) ( $point->x * 10000 )][(int) $value > 0] += $value )
  336. )
  337. )
  338. );
  339. }
  340. // Force one symbol for each stacked bar
  341. if ( !isset( $stackedSymbol[(int) ( $point->x * 10000 )] ) )
  342. {
  343. $stackedSymbol[(int) ( $point->x * 10000 )] = $data->symbol[$key];
  344. }
  345. // Store stacked value for next iteration
  346. $side = ( $point->y == 0 ? 1 : $point->y / abs( $point->y ) );
  347. $stacked[(int) ( $point->x * 10000 )][$side] = $point;
  348. $renderer->drawStackedBar(
  349. $boundings,
  350. new ezcGraphContext( $datasetName, $key, $data->url[$key] ),
  351. $data->color->default,
  352. $start,
  353. $point,
  354. $width,
  355. $stackedSymbol[(int) ( $point->x * 10000 )],
  356. $yAxisNullPosition
  357. );
  358. // Render highlight string if requested
  359. if ( $data->highlight[$key] )
  360. {
  361. $renderer->drawDataHighlightText(
  362. $boundings,
  363. new ezcGraphContext( $datasetName, $key, $data->url[$key] ),
  364. $point,
  365. $yAxisNullPosition,
  366. $nr[$data->displayType->default],
  367. $count[$data->displayType->default],
  368. $this->options->highlightFont,
  369. ( $data->highlightValue[$key] ? $data->highlightValue[$key] : $value ),
  370. $this->options->highlightSize + $this->options->highlightFont->padding * 2,
  371. ( $this->options->highlightLines ? $data->color[$key] : null ),
  372. ( $this->options->highlightXOffset ? $this->options->highlightXOffset : 0 ),
  373. ( $this->options->highlightYOffset ? $this->options->highlightYOffset : 0 ),
  374. 0.,
  375. ezcGraph::LINE
  376. );
  377. }
  378. break;
  379. case $data->displayType->default === ezcGraph::BAR:
  380. $renderer->drawBar(
  381. $boundings,
  382. new ezcGraphContext( $datasetName, $key, $data->url[$key] ),
  383. $data->color[$key],
  384. $point,
  385. $width,
  386. $nr[$data->displayType->default],
  387. $count[$data->displayType->default],
  388. $data->symbol[$key],
  389. $yAxisNullPosition
  390. );
  391. // Render highlight string if requested
  392. if ( $data->highlight[$key] )
  393. {
  394. $renderer->drawDataHighlightText(
  395. $boundings,
  396. new ezcGraphContext( $datasetName, $key, $data->url[$key] ),
  397. $point,
  398. $yAxisNullPosition,
  399. $nr[$data->displayType->default],
  400. $count[$data->displayType->default],
  401. $this->options->highlightFont,
  402. ( $data->highlightValue[$key] ? $data->highlightValue[$key] : $value ),
  403. $this->options->highlightSize + $this->options->highlightFont->padding * 2,
  404. ( $this->options->highlightLines ? $data->color[$key] : null ),
  405. ( $this->options->highlightXOffset ? $this->options->highlightXOffset : 0 ),
  406. ( $this->options->highlightYOffset ? $this->options->highlightYOffset : 0 ),
  407. $width,
  408. $data->displayType->default
  409. );
  410. }
  411. break;
  412. default:
  413. throw new ezcGraphInvalidDisplayTypeException( $data->displayType->default );
  414. break;
  415. }
  416. // Store last point, used to connect lines in line chart.
  417. $lastPoint = $point;
  418. }
  419. }
  420. }
  421. /**
  422. * Returns the default display type of the current chart type.
  423. *
  424. * @return int Display type
  425. */
  426. public function getDefaultDisplayType()
  427. {
  428. return ezcGraph::LINE;
  429. }
  430. /**
  431. * Check if renderer supports features requested by some special chart
  432. * options.
  433. *
  434. * @throws ezcBaseValueException
  435. * If some feature is not supported
  436. *
  437. * @return void
  438. */
  439. protected function checkRenderer()
  440. {
  441. // When stacked bars are enabled, check if renderer supports them
  442. if ( $this->options->stackBars )
  443. {
  444. if ( !$this->renderer instanceof ezcGraphStackedBarsRenderer )
  445. {
  446. throw new ezcBaseValueException( 'renderer', $this->renderer, 'ezcGraphStackedBarsRenderer' );
  447. }
  448. }
  449. }
  450. /**
  451. * Aggregate and calculate value boundings on axis.
  452. *
  453. * @return void
  454. */
  455. protected function setAxisValues()
  456. {
  457. // Virtual data set build for agrregated values sums for bar charts
  458. $virtualBarSumDataSet = array( array(), array() );
  459. // Calculate axis scaling and labeling
  460. foreach ( $this->data as $dataset )
  461. {
  462. $nr = 0;
  463. $labels = array();
  464. $values = array();
  465. foreach ( $dataset as $label => $value )
  466. {
  467. $labels[] = $label;
  468. $values[] = $value;
  469. // Build sum of all bars
  470. if ( $this->options->stackBars &&
  471. ( $dataset->displayType->default === ezcGraph::BAR ) )
  472. {
  473. if ( !isset( $virtualBarSumDataSet[(int) $value >= 0][$nr] ) )
  474. {
  475. $virtualBarSumDataSet[(int) $value >= 0][$nr++] = $value;
  476. }
  477. else
  478. {
  479. $virtualBarSumDataSet[(int) $value >= 0][$nr++] += $value;
  480. }
  481. }
  482. }
  483. // Check if data has been associated with another custom axis, use
  484. // default axis otherwise.
  485. if ( $dataset->xAxis->default )
  486. {
  487. $dataset->xAxis->default->addData( $labels );
  488. }
  489. else
  490. {
  491. $this->elements['xAxis']->addData( $labels );
  492. }
  493. if ( $dataset->yAxis->default )
  494. {
  495. $dataset->yAxis->default->addData( $values );
  496. }
  497. else
  498. {
  499. $this->elements['yAxis']->addData( $values );
  500. }
  501. }
  502. // Also use stacked bar values as base for y axis value span
  503. // calculation
  504. if ( $this->options->stackBars )
  505. {
  506. $this->elements['yAxis']->addData( $virtualBarSumDataSet[0] );
  507. $this->elements['yAxis']->addData( $virtualBarSumDataSet[1] );
  508. }
  509. // There should always be something assigned to the main x and y axis.
  510. if ( !$this->elements['xAxis']->initialized ||
  511. !$this->elements['yAxis']->initialized )
  512. {
  513. throw new ezcGraphNoDataException();
  514. }
  515. // Calculate boundings from assigned data
  516. $this->elements['xAxis']->calculateAxisBoundings();
  517. $this->elements['yAxis']->calculateAxisBoundings();
  518. }
  519. /**
  520. * Renders the basic elements of this chart type
  521. *
  522. * @param int $width
  523. * @param int $height
  524. * @return void
  525. */
  526. protected function renderElements( $width, $height )
  527. {
  528. if ( !count( $this->data ) )
  529. {
  530. throw new ezcGraphNoDataException();
  531. }
  532. // Check if renderer supports requested features
  533. $this->checkRenderer();
  534. // Set values form datasets on axis to calculate correct spans
  535. $this->setAxisValues();
  536. // Generate legend
  537. $this->elements['legend']->generateFromDataSets( $this->data );
  538. // Get boundings from parameters
  539. $this->options->width = $width;
  540. $this->options->height = $height;
  541. // Set image properties in driver
  542. $this->driver->options->width = $width;
  543. $this->driver->options->height = $height;
  544. // Render subelements
  545. $boundings = new ezcGraphBoundings();
  546. $boundings->x1 = $this->options->width;
  547. $boundings->y1 = $this->options->height;
  548. $boundings = $this->elements['xAxis']->axisLabelRenderer->modifyChartBoundings(
  549. $this->elements['yAxis']->axisLabelRenderer->modifyChartBoundings(
  550. $boundings, new ezcGraphCoordinate( 1, 0 )
  551. ), new ezcGraphCoordinate( -1, 0 )
  552. );
  553. // Render subelements
  554. foreach ( $this->elements as $name => $element )
  555. {
  556. // Skip element, if it should not get rendered
  557. if ( ( $this->renderElement[$name] === false ) ||
  558. ( $name === 'xAxis' ) ||
  559. ( $name === 'yAxis' ) )
  560. {
  561. continue;
  562. }
  563. $this->driver->options->font = $element->font;
  564. $boundings = $element->render( $this->renderer, $boundings );
  565. }
  566. // Set relative positions of axis in chart depending on the "null"
  567. // value on the other axis.
  568. $this->elements['xAxis']->nullPosition = $this->elements['yAxis']->getCoordinate( false );
  569. $this->elements['yAxis']->nullPosition = $this->elements['xAxis']->getCoordinate( false );
  570. // Calculate inner data boundings of chart
  571. $innerBoundings = new ezcGraphBoundings(
  572. $boundings->x0 + $boundings->width *
  573. $this->elements['yAxis']->axisSpace,
  574. $boundings->y0 + $boundings->height *
  575. ( ( $this->elements['xAxis']->outerAxisSpace === null ) ?
  576. $this->elements['xAxis']->axisSpace :
  577. $this->elements['xAxis']->outerAxisSpace ),
  578. $boundings->x1 - $boundings->width *
  579. ( ( $this->elements['yAxis']->outerAxisSpace === null ) ?
  580. $this->elements['yAxis']->axisSpace :
  581. $this->elements['yAxis']->outerAxisSpace ),
  582. $boundings->y1 - $boundings->height *
  583. $this->elements['xAxis']->axisSpace
  584. );
  585. // Render axis
  586. $this->driver->options->font = $this->elements['yAxis']->font;
  587. $boundings = $this->elements['xAxis']->render( $this->renderer, $boundings, $innerBoundings );
  588. $boundings = $this->elements['yAxis']->render( $this->renderer, $boundings, $innerBoundings );
  589. // Render additional axis
  590. foreach ( $this->additionalAxis as $element )
  591. {
  592. if ( $element->initialized )
  593. {
  594. // Calculate all required step sizes if values has been
  595. // assigned to axis.
  596. $element->calculateAxisBoundings();
  597. }
  598. else
  599. {
  600. // Do not render any axis labels, if no values were assigned
  601. // and no step sizes were defined.
  602. $element->axisLabelRenderer = new ezcGraphAxisNoLabelRenderer();
  603. }
  604. $this->driver->options->font = $element->font;
  605. $element->nullPosition = $element->chartPosition;
  606. $boundings = $element->render( $this->renderer, $boundings, $innerBoundings );
  607. }
  608. // Render graph
  609. $this->renderData( $this->renderer, $boundings, $innerBoundings );
  610. }
  611. /**
  612. * Render the line chart
  613. *
  614. * Renders the chart into a file or stream. The width and height are
  615. * needed to specify the dimensions of the resulting image. For direct
  616. * output use 'php://stdout' as output file.
  617. *
  618. * @param int $width Image width
  619. * @param int $height Image height
  620. * @param string $file Output file
  621. * @apichange
  622. * @return void
  623. */
  624. public function render( $width, $height, $file = null )
  625. {
  626. $this->renderElements( $width, $height );
  627. if ( !empty( $file ) )
  628. {
  629. $this->renderer->render( $file );
  630. }
  631. $this->renderedFile = $file;
  632. }
  633. /**
  634. * Renders this chart to direct output
  635. *
  636. * Does the same as ezcGraphChart::render(), but renders directly to
  637. * output and not into a file.
  638. *
  639. * @param int $width
  640. * @param int $height
  641. * @apichange
  642. * @return void
  643. */
  644. public function renderToOutput( $width, $height )
  645. {
  646. // @TODO: merge this function with render an deprecate ommit of third
  647. // argument in render() when API break is possible
  648. $this->renderElements( $width, $height );
  649. $this->renderer->render( null );
  650. }
  651. }
  652. ?>