1 /*
2  * Copyright (C) 2015 Emweb bv, Herent, Belgium.
3  *
4  * See the LICENSE file for terms of use.
5  */
6 
7 #include "Wt/Chart/WAxisSliderWidget.h"
8 
9 #include "Wt/WApplication.h"
10 #include "Wt/WAny.h"
11 #include "Wt/WBrush.h"
12 #include "Wt/WColor.h"
13 #include "Wt/WJavaScript.h"
14 #include "Wt/WLogger.h"
15 #include "Wt/WPainter.h"
16 #include "Wt/WStringStream.h"
17 #include "Wt/WTransform.h"
18 
19 #include "Wt/Chart/WCartesianChart.h"
20 
21 #ifndef WT_DEBUG_JS
22 #include "js/WAxisSliderWidget.min.js"
23 #endif
24 
25 #include <algorithm>
26 
27 namespace Wt {
28 
29 LOGGER("Chart.WAxisSliderWidget");
30 
31   namespace Chart {
32 
WAxisSliderWidget()33 WAxisSliderWidget::WAxisSliderWidget()
34   : series_(nullptr),
35     selectedSeriesPen_(&seriesPen_),
36     handleBrush_(WColor(0,0,200)),
37     background_(WColor(230, 230, 230)),
38     selectedAreaBrush_(WColor(255, 255, 255)),
39     autoPadding_(false),
40     labelsEnabled_(true),
41     yAxisZoomEnabled_(true)
42 {
43   init();
44 }
45 
WAxisSliderWidget(WDataSeries * series)46 WAxisSliderWidget::WAxisSliderWidget(WDataSeries *series)
47   : series_(series),
48     selectedSeriesPen_(&seriesPen_),
49     handleBrush_(WColor(0,0,200)),
50     background_(WColor(230, 230, 230)),
51     selectedAreaBrush_(WColor(255, 255, 255)),
52     autoPadding_(false),
53     labelsEnabled_(true),
54     yAxisZoomEnabled_(true)
55 {
56   init();
57 }
58 
init()59 void WAxisSliderWidget::init()
60 {
61   transform_ = createJSTransform();
62 
63   mouseWentDown().connect("function(o, e){var o=" + this->sObjJsRef() + ";if(o){o.mouseDown(o, e);}}");
64   mouseWentUp().connect("function(o, e){var o=" + this->sObjJsRef() + ";if(o){o.mouseUp(o, e);}}");
65   mouseDragged().connect("function(o, e){var o=" + this->sObjJsRef() + ";if(o){o.mouseDrag(o, e);}}");
66   mouseMoved().connect("function(o, e){var o=" + this->sObjJsRef() + ";if(o){o.mouseMoved(o, e);}}");
67   touchStarted().connect("function(o, e){var o=" + this->sObjJsRef() + ";if(o){o.touchStarted(o, e);}}");
68   touchEnded().connect("function(o, e){var o=" + this->sObjJsRef() + ";if(o){o.touchEnded(o, e);}}");
69   touchMoved().connect("function(o, e){var o=" + this->sObjJsRef() + ";if(o){o.touchMoved(o, e);}}");
70 
71   setSelectionAreaPadding(0, Side::Top);
72   setSelectionAreaPadding(20, WFlags<Side>(Side::Left) | Side::Right);
73   setSelectionAreaPadding(30, Side::Bottom);
74 
75   if (chart()) {
76     chart()->addAxisSliderWidget(this);
77   }
78 }
79 
~WAxisSliderWidget()80 WAxisSliderWidget::~WAxisSliderWidget()
81 {
82   if (chart()) {
83     chart()->removeAxisSliderWidget(this);
84   }
85   if (selectedSeriesPen_ != &seriesPen_) {
86     delete selectedSeriesPen_;
87   }
88 }
89 
setSeries(WDataSeries * series)90 void WAxisSliderWidget::setSeries(WDataSeries *series)
91 {
92   if (series_ != series) {
93     if (series_)
94       chart()->removeAxisSliderWidget(this);
95     series_ = series;
96     if (series_)
97       chart()->addAxisSliderWidget(this);
98     update();
99   }
100 }
101 
setSeriesPen(const WPen & pen)102 void WAxisSliderWidget::setSeriesPen(const WPen& pen)
103 {
104   if (pen != seriesPen_) {
105     seriesPen_ = pen;
106     update();
107   }
108 }
109 
setSelectedSeriesPen(const WPen & pen)110 void WAxisSliderWidget::setSelectedSeriesPen(const WPen& pen)
111 {
112   if (selectedSeriesPen_ != &seriesPen_) {
113     delete selectedSeriesPen_;
114   }
115   selectedSeriesPen_ = new WPen(pen);
116   update();
117 }
118 
setHandleBrush(const WBrush & brush)119 void WAxisSliderWidget::setHandleBrush(const WBrush& brush)
120 {
121   if (brush != handleBrush_) {
122     handleBrush_ = brush;
123     update();
124   }
125 }
126 
setBackground(const WBrush & brush)127 void WAxisSliderWidget::setBackground(const WBrush& brush)
128 {
129   if (brush != background_) {
130     background_ = brush;
131     update();
132   }
133 }
134 
setSelectedAreaBrush(const WBrush & brush)135 void WAxisSliderWidget::setSelectedAreaBrush(const WBrush& brush)
136 {
137   if (brush != selectedAreaBrush_) {
138     selectedAreaBrush_ = brush;
139     update();
140   }
141 }
142 
render(WFlags<RenderFlag> flags)143 void WAxisSliderWidget::render(WFlags<RenderFlag> flags)
144 {
145   WPaintedWidget::render(flags);
146 
147   WApplication *app = WApplication::instance();
148 
149   LOAD_JAVASCRIPT(app, "js/WAxisSliderWidget.js", "WAxisSliderWidget", wtjs1);
150 }
151 
setSelectionAreaPadding(int padding,WFlags<Side> sides)152 void WAxisSliderWidget::setSelectionAreaPadding(int padding, WFlags<Side> sides)
153 {
154   if (sides.test(Side::Top))
155     padding_[0] = padding;
156   if (sides.test(Side::Right))
157     padding_[1] = padding;
158   if (sides.test(Side::Bottom))
159     padding_[2] = padding;
160   if (sides.test(Side::Left))
161     padding_[3] = padding;
162 }
163 
selectionAreaPadding(Side side)164 int WAxisSliderWidget::selectionAreaPadding(Side side) const
165 {
166   switch (side) {
167   case Side::Top:
168     return padding_[0];
169   case Side::Right:
170     return padding_[1];
171   case Side::Bottom:
172     return padding_[2];
173   case Side::Left:
174     return padding_[3];
175   default:
176     LOG_ERROR("selectionAreaPadding(): improper side.");
177     return 0;
178   }
179 }
180 
setAutoLayoutEnabled(bool enabled)181 void WAxisSliderWidget::setAutoLayoutEnabled(bool enabled)
182 {
183   autoPadding_ = enabled;
184 }
185 
setLabelsEnabled(bool enabled)186 void WAxisSliderWidget::setLabelsEnabled(bool enabled)
187 {
188   if (enabled != labelsEnabled_) {
189     labelsEnabled_ = enabled;
190     update();
191   }
192 }
193 
setYAxisZoomEnabled(bool enabled)194 void WAxisSliderWidget::setYAxisZoomEnabled(bool enabled)
195 {
196   if (enabled != yAxisZoomEnabled_) {
197     yAxisZoomEnabled_ = enabled;
198     update();
199   }
200 }
201 
hv(const WRectF & rect)202 WRectF WAxisSliderWidget::hv(const WRectF& rect) const
203 {
204   bool horizontal = chart()->orientation() == Orientation::Vertical; // yes, vertical chart means horizontal X axis slider
205 
206   if (horizontal) {
207     return rect;
208   } else {
209     return WRectF(width().value() - rect.y() - rect.height(), rect.x(), rect.height(), rect.width());
210   }
211 }
212 
hv(const WTransform & t)213 WTransform WAxisSliderWidget::hv(const WTransform& t) const
214 {
215   bool horizontal = chart()->orientation() == Orientation::Vertical; // yes, vertical chart means horizontal X axis slider
216 
217   if (horizontal) {
218     return t;
219   } else {
220     return WTransform(0,1,1,0,0,0) * t * WTransform(0,1,1,0,0,0);
221   }
222 }
223 
paintEvent(WPaintDevice * paintDevice)224 void WAxisSliderWidget::paintEvent(WPaintDevice *paintDevice)
225 {
226   if (!chart()) {
227     LOG_ERROR("Attempted to draw a slider widget not associated with a chart.");
228     return;
229   }
230   // Don't paint anything, unless we're associated to a chart,
231   // and the chart has been painted.
232   if (!chart()->cObjCreated_ || chart()->needRerender())
233     return;
234 
235   if (series_->type() != SeriesType::Line &&
236       series_->type() != SeriesType::Curve) {
237     if (getMethod() == RenderMethod::HtmlCanvas) {
238       WStringStream ss;
239       ss << "\ndelete " << jsRef() << ".wtSObj;";
240       ss << "\nif (" << objJsRef() << ") {"
241 	   << objJsRef() << ".canvas.style.cursor = 'auto';"
242 	   << "setTimeout(" << objJsRef() << ".repaint,0);"
243 	    "}\n";
244       doJavaScript(ss.str());
245     }
246     LOG_ERROR("WAxisSliderWidget is not associated with a line or curve series.");
247     return;
248   }
249 
250   WAxis &xAxis = chart()->xAxis(series_->xAxis());
251   WCartesianChart::AxisStruct &xAxisStruct = chart()->xAxes_[series_->xAxis()];
252 
253   WPainter painter(paintDevice);
254 
255   bool horizontal = chart()->orientation() == Orientation::Vertical; // yes, vertical chart means horizontal X axis slider
256 
257   double w = horizontal ? width().value() : height().value(),
258 	 h = horizontal ? height().value() : width().value();
259 
260   bool autoPadding = autoPadding_;
261   if (autoPadding && !(paintDevice->features() &
262 		       PaintDeviceFeatureFlag::FontMetrics) &&
263       labelsEnabled_) {
264     LOG_ERROR("setAutoLayout(): device does not have font metrics "
265 	"(not even server-side font metrics).");
266     autoPadding = false;
267   }
268 
269   if (autoPadding) {
270     if (horizontal) {
271       if (labelsEnabled_) {
272         setSelectionAreaPadding(0, Side::Top);
273 	setSelectionAreaPadding(
274             static_cast<int>(xAxis.calcMaxTickLabelSize(
275 	      paintDevice,
276               Orientation::Vertical
277             ) + 10), Side::Bottom);
278 	setSelectionAreaPadding(
279             static_cast<int>(std::max(xAxis.calcMaxTickLabelSize(
280 	      paintDevice,
281               Orientation::Horizontal
282             ) / 2, 10.0)), WFlags<Side>(Side::Left) | Side::Right);
283       } else {
284 	setSelectionAreaPadding(0, Side::Top);
285 	setSelectionAreaPadding(5, WFlags<Side>(Side::Left) | Side::Right | Side::Bottom);
286       }
287     } else {
288       if (labelsEnabled_) {
289         setSelectionAreaPadding(0, Side::Right);
290 	setSelectionAreaPadding(
291             static_cast<int>(std::max(xAxis.calcMaxTickLabelSize(
292 	      paintDevice,
293               Orientation::Vertical
294             ) / 2, 10.0)), WFlags<Side>(Side::Top) | Side::Bottom);
295 	setSelectionAreaPadding(
296             static_cast<int>(xAxis.calcMaxTickLabelSize(
297 	      paintDevice,
298               Orientation::Horizontal
299             ) + 10), Side::Left);
300       } else {
301 	setSelectionAreaPadding(0, Side::Right);
302 	setSelectionAreaPadding(5, WFlags<Side>(Side::Top) | Side::Bottom | Side::Left);
303       }
304     }
305   }
306 
307   double left = horizontal ? selectionAreaPadding(Side::Left)
308     : selectionAreaPadding(Side::Top);
309   double right = horizontal ? selectionAreaPadding(Side::Right)
310     : selectionAreaPadding(Side::Bottom);
311   double top = horizontal ? selectionAreaPadding(Side::Top)
312     : selectionAreaPadding(Side::Right);
313   double bottom = horizontal ? selectionAreaPadding(Side::Bottom)
314     : selectionAreaPadding(Side::Left);
315 
316   double maxW = w - left - right;
317   WRectF drawArea(left, 0, maxW, h);
318 #ifndef WT_TARGET_JAVA
319   std::vector<WAxis::Segment> segmentsBak = xAxis.segments_;
320 #else
321   std::vector<WAxis::Segment> segmentsBak;
322   for (std::size_t i = 0; i < xAxis.segments_.size(); ++i) {
323     segmentsBak.push_back(WAxis::Segment(xAxis.segments_[i]));
324   }
325 #endif
326   double renderIntervalBak = xAxis.renderInterval_;
327   double fullRenderLengthBak = xAxis.fullRenderLength_;
328   xAxis.prepareRender(horizontal ? Orientation::Horizontal : Orientation::Vertical, drawArea.width());
329 
330   const WRectF& chartArea = chart()->chartArea_;
331   WRectF selectionRect;
332   {
333     // Determine initial position based on xTransform of chart
334     double u = -xAxisStruct.transformHandle.value().dx() /
335         (chartArea.width() * xAxisStruct.transformHandle.value().m11());
336     selectionRect = WRectF(0, top, maxW, h - (top + bottom));
337     transform_.setValue(
338           WTransform(1 / xAxisStruct.transformHandle.value().m11(), 0, 0, 1, u * maxW, 0));
339   }
340   WRectF seriesArea(left, top + 5, maxW, h - (top + bottom + 5));
341   WTransform selectionTransform = hv(WTransform(1,0,0,1,left,0) * transform_.value());
342   WRectF rect = selectionTransform.map(hv(selectionRect));
343 
344   painter.fillRect(hv(WRectF(left, top, maxW, h - top - bottom)), background_);
345   painter.fillRect(rect, selectedAreaBrush_);
346 
347   // FIXME: Refactor this code? We have very similar code now in WCartesianChart and
348   //	    WCartesian3DChart too.
349   const double TICK_LENGTH = 5;
350   const double ANGLE1 = 15;
351   const double ANGLE2 = 80;
352 
353   double tickStart = 0.0, tickEnd = 0.0, labelPos = 0.0;
354   AlignmentFlag labelHFlag = AlignmentFlag::Center, labelVFlag = AlignmentFlag::Middle;
355 
356   if (horizontal) {
357     tickStart = 0;
358     tickEnd = TICK_LENGTH;
359     labelPos = TICK_LENGTH;
360     labelVFlag = AlignmentFlag::Top;
361   } else {
362     tickStart = -TICK_LENGTH;
363     tickEnd = 0;
364     labelPos = -TICK_LENGTH;
365     labelHFlag = AlignmentFlag::Right;
366   }
367 
368   if (horizontal) {
369     if (xAxis.labelAngle() > ANGLE1) {
370       labelHFlag = AlignmentFlag::Right;
371       if (xAxis.labelAngle() > ANGLE2)
372 	labelVFlag = AlignmentFlag::Middle;
373     } else if (xAxis.labelAngle() < -ANGLE1) {
374       labelHFlag = AlignmentFlag::Left;
375       if (xAxis.labelAngle() < -ANGLE2)
376 	labelVFlag = AlignmentFlag::Middle;
377     }
378   } else {
379     if (xAxis.labelAngle() > ANGLE1) {
380       labelVFlag = AlignmentFlag::Bottom;
381       if (xAxis.labelAngle() > ANGLE2)
382 	labelHFlag = AlignmentFlag::Center;
383     } else if (xAxis.labelAngle() < -ANGLE1) {
384       labelVFlag = AlignmentFlag::Top;
385       if (xAxis.labelAngle() < -ANGLE2)
386 	labelHFlag = AlignmentFlag::Center;
387     }
388   }
389 
390   WFlags<AxisProperty> axisProperties = AxisProperty::Line;
391   if (labelsEnabled_) {
392     axisProperties |= AxisProperty::Labels;
393   }
394 
395   if (horizontal) {
396     xAxis.render(
397 	painter,
398 	axisProperties,
399 	WPointF(drawArea.left(), h - bottom),
400 	WPointF(drawArea.right(), h - bottom),
401 	tickStart, tickEnd, labelPos,
402 	WFlags<AlignmentFlag>(labelHFlag) | labelVFlag);
403     WPainterPath line;
404     line.moveTo(drawArea.left() + 0.5, h - (bottom - 0.5));
405     line.lineTo(drawArea.right(), h - (bottom - 0.5));
406     painter.strokePath(line, xAxis.pen());
407   } else {
408     xAxis.render(
409 	painter,
410 	axisProperties,
411 	WPointF(selectionAreaPadding(Side::Left) - 1, drawArea.left()),
412 	WPointF(selectionAreaPadding(Side::Left) - 1, drawArea.right()),
413 	tickStart, tickEnd, labelPos,
414 	WFlags<AlignmentFlag>(labelHFlag) | labelVFlag);
415     WPainterPath line;
416     line.moveTo(selectionAreaPadding(Side::Left) - 0.5, drawArea.left() + 0.5);
417     line.lineTo(selectionAreaPadding(Side::Left) - 0.5, drawArea.right());
418     painter.strokePath(line, xAxis.pen());
419   }
420 
421   WPainterPath curve;
422   {
423     WTransform t = WTransform(1,0,0,1,seriesArea.left(),seriesArea.top()) *
424       WTransform(seriesArea.width() / chartArea.width(), 0, 0, seriesArea.height() / chartArea.height(), 0, 0) *
425       WTransform(1,0,0,1,-chartArea.left(),-chartArea.top());
426     if (!horizontal) {
427       t = WTransform(0,1,1,0,selectionAreaPadding(Side::Left) - selectionAreaPadding(Side::Right) - 5,0) * t * WTransform(0,1,1,0,0,0);
428     }
429     curve = t.map(chart()->pathForSeries(*series_));
430   }
431 
432   {
433     WRectF leftHandle = hv(WRectF(-5, top, 5, h - top - bottom));
434     WTransform t = (WTransform(1,0,0,1,left,-top) *
435 	(WTransform().translate(transform_.value().map(selectionRect.topLeft()))));
436     painter.fillRect(hv(t).map(leftHandle), handleBrush_);
437   }
438 
439   {
440     WRectF rightHandle = hv(WRectF(0, top, 5, h - top - bottom));
441     WTransform t = (WTransform(1,0,0,1,left,-top) *
442 	(WTransform().translate(transform_.value().map(selectionRect.topRight()))));
443     painter.fillRect(hv(t).map(rightHandle), handleBrush_);
444   }
445 
446   if (selectedSeriesPen_ != &seriesPen_ && *selectedSeriesPen_ != seriesPen_) {
447     WPainterPath clipPath;
448     clipPath.addRect(hv(selectionRect));
449     painter.setClipPath(selectionTransform.map(clipPath));
450     painter.setClipping(true);
451 
452     painter.setPen(selectedSeriesPen());
453     painter.drawPath(curve);
454 
455     WPainterPath leftClipPath;
456     leftClipPath.addRect(hv(WTransform(1,0,0,1,-selectionRect.width(),0).map(selectionRect)));
457     painter.setClipPath(hv(
458 	  WTransform(1,0,0,1,left,-top) *
459 	  (WTransform().translate(transform_.value().map(selectionRect.topLeft())))
460 	).map(leftClipPath));
461 
462     painter.setPen(seriesPen());
463     painter.drawPath(curve);
464 
465     WPainterPath rightClipPath;
466     rightClipPath.addRect(hv(WTransform(1,0,0,1,selectionRect.width(),0).map(selectionRect)));
467     painter.setClipPath(hv(
468 	  WTransform(1,0,0,1,left - selectionRect.right(),-top) *
469 	  (WTransform().translate(transform_.value().map(selectionRect.topRight())))
470 	).map(rightClipPath));
471 
472     painter.drawPath(curve);
473 
474     painter.setClipping(false);
475   } else {
476     WPainterPath clipPath;
477     clipPath.addRect(hv(WRectF(left, top, maxW, h - top - bottom)));
478     painter.setClipPath(clipPath);
479     painter.setClipping(true);
480 
481     painter.setPen(seriesPen());
482     painter.drawPath(curve);
483 
484     painter.setClipping(false);
485   }
486 
487   if (getMethod() == RenderMethod::HtmlCanvas) {
488     WApplication *app = WApplication::instance();
489     WStringStream ss;
490     ss << "new " WT_CLASS ".WAxisSliderWidget("
491        << app->javaScriptClass() << ","
492        << jsRef() << ","
493        << objJsRef() << ","
494        << "{"
495        "chart:" << chart()->cObjJsRef() << ","
496        "transform:" << transform_.jsRef() << ","
497        "rect:function(){return " << rect.jsRef() << "},"
498        "drawArea:" << drawArea.jsRef() << ","
499        "series:" << chart()->seriesIndexOf(*series_) << ","
500        "updateYAxis:" << asString(yAxisZoomEnabled_).toUTF8() <<
501        "});";
502     doJavaScript(ss.str());
503   }
504 
505 #ifndef WT_TARGET_JAVA
506   xAxis.segments_ = segmentsBak;
507 #else
508   xAxis.segments_.clear();
509   for (std::size_t i = 0; i < segmentsBak.size(); ++i) {
510     xAxis.segments_.push_back(WAxis::Segment(segmentsBak[i]));
511   }
512 #endif
513   xAxis.renderInterval_ = renderIntervalBak;
514   xAxis.fullRenderLength_ = fullRenderLengthBak;
515 }
516 
sObjJsRef()517 std::string WAxisSliderWidget::sObjJsRef() const
518 {
519   return jsRef() + ".wtSObj";
520 }
521 
chart()522 WCartesianChart *WAxisSliderWidget::chart()
523 {
524   if (series_) {
525     return series_->chart();
526   } else {
527     return nullptr;
528   }
529 }
530 
chart()531 const WCartesianChart *WAxisSliderWidget::chart() const
532 {
533   return const_cast<WAxisSliderWidget *>(this)->chart();
534 }
535 
536   }
537 }
538