From 9401940217725b79a84201f55f1855894153daef Mon Sep 17 00:00:00 2001
From: Bruce Toll <btoll@dhsus.com>
Date: Mon, 20 Jul 2015 15:17:33 -0400
Subject: [PATCH 1/5] Add support for zoom/pan map area updates.

This patch adds basic support for client-side updates to map areas, as
created by the subclasses of WAbstractArea. It complements the recent
enhancements that provide optional client side interactive zoom/pan in
WCartesianChart. It is activated automatically on interactive charts.

At each client-side repaint of the WCartesianChart/WPaintedWidget, the
map areas are updated using the same combinedTransform that is used by
the WPaintedWidget. A JSON array produced by
WImage::updateAreaCoordsJSON() contains the list of updates and it is
built by calling a new method, updateAreaCoordsJS() in the
WAbstractArea subclasses.

When an interactive chart is detected, WCartesianChart will add an
area mask using addAreaMask to prevent tooltips from appearing outside
the active chart area.

Some new API is visible, though not really intended for general use:

    setTransformable() / isTransformable() -- New for WAbstractArea and
    its subclasses. It indicates whether a map area is part of the
    zoom/pan chart area and should be transformed. The default is true.

    WImage::setTargetJS() -- Used internally by WPaintedWidget to
    provide a WImage with JavaScript code to access the WPaintedWidget
    client-side "target" object. The target object holds information
    used to facilitate client-side interactive zoom/pan. Calling
    setTargetJS is also used to signal the need for WImage interactive
    features.

    The client-side WPaintedWidget "target" object which currently
    contains both documented and undocumented members has been augmented
    with a combinedTransform() method that can be used to obtain the
    current combinedTransform.

This patch has some limitations that are addressed in subsequent patches:

1. The integer resolution of the existing WAbstractArea subclasses is
   insufficient for support of accurate zooming. With this patch, the
   areas are updated, but they will appear to drift from their proper
   locations at higher zoom levels.

2. Performance suffers when there are large numbers of areas, and this
   is especially apparent with Firefox.

Other notes:

This patch does not adjust the radii of WCircleAreas when scaled. This
seems reasonable for markers, which are also not scaled when painting.
Also, there is no ellipse area available to support independent scaling
of the x and y axes.
---
 src/Wt/Chart/WCartesianChart   |  2 +-
 src/Wt/Chart/WCartesianChart.C | 23 ++++++++++++
 src/Wt/WAbstractArea           |  7 ++++
 src/Wt/WAbstractArea.C         |  8 +++++
 src/Wt/WCircleArea             |  1 +
 src/Wt/WCircleArea.C           | 10 ++++++
 src/Wt/WImage                  |  9 +++++
 src/Wt/WImage.C                | 79 ++++++++++++++++++++++++++++++++++++++++++
 src/Wt/WPaintedWidget.C        |  9 +++++
 src/Wt/WPolygonArea            |  1 +
 src/Wt/WPolygonArea.C          | 15 ++++++++
 src/Wt/WRectArea               |  1 +
 src/Wt/WRectArea.C             | 10 ++++++
 src/js/WCartesianChart.js      |  1 +
 src/js/WImage.js               | 57 ++++++++++++++++++++++++++++++
 15 files changed, 232 insertions(+), 1 deletion(-)
 create mode 100644 src/js/WImage.js

diff --git a/src/Wt/Chart/WCartesianChart b/src/Wt/Chart/WCartesianChart
index 379318e..ed6da00 100644
--- a/src/Wt/Chart/WCartesianChart
+++ b/src/Wt/Chart/WCartesianChart
@@ -190,7 +190,6 @@ class WChartPalette;
  * \note Client side interaction is only available if the chart is drawn
  *       on an HTML canvas. This is the default rendering method on modern browsers.
  * \note Some features are currently not supported in interactive mode:
- *	  - Tooltips are not zoomed or panned along with the chart
  *	  - Axes set at ZeroValue position will not always be drawn correctly.
  *	    They may be clipped off outside of the chart area, and when zooming,
  *	    the axis ticks will change size.
@@ -1053,6 +1052,7 @@ private:
   WTransform combinedTransform() const;
   WTransform combinedTransform(WTransform xTransform, WTransform yTransform) const;
   void setInitialZoomAndPan();
+  void addAreaMask();
 
   WPointF hv(double x, double y, double width) const;
   WPointF inverseHv(double x, double y, double width) const;
diff --git a/src/Wt/Chart/WCartesianChart.C b/src/Wt/Chart/WCartesianChart.C
index 0f3fc79..7740731 100644
--- a/src/Wt/Chart/WCartesianChart.C
+++ b/src/Wt/Chart/WCartesianChart.C
@@ -1080,6 +1080,8 @@ void WCartesianChart::addDataPointArea(const WDataSeries& series,
 				       const WModelIndex& xIndex,
 				       WAbstractArea *area)
 {
+  if (areas().empty())
+    addAreaMask();
   addArea(area);
 }
 
@@ -3069,5 +3071,26 @@ void WCartesianChart::removeAxisSliderWidget(WAxisSliderWidget *slider)
   }
 }
 
+void WCartesianChart::addAreaMask()
+{
+  WRectF all = hv(WRectF(0, 0, width_, height_));
+  WRectF chart = hv(chartArea_);
+  std::vector<WRectF> rects;
+
+  rects.push_back(WRectF(all.topLeft(), WPointF(all.right(), chart.top())));
+  rects.push_back(WRectF(WPointF(all.left(), chart.bottom()), all.bottomRight()));
+  rects.push_back(WRectF(WPointF(all.left(), chart.top()), chart.bottomLeft()));
+  rects.push_back(WRectF(chart.topRight(), WPointF(all.right(), chart.bottom())));
+
+  for (int i = 0; i < rects.size(); ++i) {
+    if (rects[i].height() > 0 && rects[i].width() > 0) {
+      WRectArea *rect = new WRectArea(rects[i]);
+      rect->setHole(true);
+      rect->setTransformable(false);
+      addArea(rect);
+    }
+  }
+}
+
   }
 }
diff --git a/src/Wt/WAbstractArea b/src/Wt/WAbstractArea
index 0cbd846..1532deb 100644
--- a/src/Wt/WAbstractArea
+++ b/src/Wt/WAbstractArea
@@ -55,6 +55,9 @@ public:
    */
   bool isHole() const { return hole_; }
 
+  void setTransformable(bool transformable);
+  bool isTransformable() const { return transformable_; }
+
   /*! \brief Sets a link.
    *
    * By setting a link, the area behaves like a WAnchor.
@@ -407,6 +410,7 @@ private:
   };
 
   bool hole_;
+  bool transformable_;
   AnchorImpl *anchor_;
 
   void createAnchorImpl();
@@ -416,7 +420,9 @@ protected:
   WAbstractArea();
 
   virtual bool updateDom(DomElement& element, bool all);
+  virtual std::string updateAreaCoordsJS() = 0;
   void repaint();
+  std::string jsRef() { return impl()->jsRef(); }
 
 private:
   WInteractWidget *impl();
@@ -426,6 +432,7 @@ private:
 
   friend class WImage;
   friend class Impl::AreaWidget;
+  friend class WPaintedWidget;
 };
 
 }
diff --git a/src/Wt/WAbstractArea.C b/src/Wt/WAbstractArea.C
index 02ed0a6..2affdf4 100644
--- a/src/Wt/WAbstractArea.C
+++ b/src/Wt/WAbstractArea.C
@@ -65,6 +65,7 @@ namespace Wt {
 WAbstractArea::WAbstractArea()
   : impl_(new Impl::AreaWidget(this)),
     hole_(false),
+    transformable_(true),
     anchor_(0)
 { }
 
@@ -188,6 +189,13 @@ void WAbstractArea::setHole(bool hole)
   repaint();
 }
 
+void WAbstractArea::setTransformable(bool transformable)
+{
+  transformable_ = transformable;
+
+  repaint();
+}
+
 void WAbstractArea::setLink(const WLink& link)
 {
   createAnchorImpl();
diff --git a/src/Wt/WCircleArea b/src/Wt/WCircleArea
index fbbdeda..7d8d2df 100644
--- a/src/Wt/WCircleArea
+++ b/src/Wt/WCircleArea
@@ -83,6 +83,7 @@ private:
 
 protected:
   virtual bool updateDom(DomElement& element, bool all);
+  virtual std::string updateAreaCoordsJS() WT_CXX11ONLY(override);
 };
 
 }
diff --git a/src/Wt/WCircleArea.C b/src/Wt/WCircleArea.C
index a433722..7589124 100644
--- a/src/Wt/WCircleArea.C
+++ b/src/Wt/WCircleArea.C
@@ -57,4 +57,14 @@ bool WCircleArea::updateDom(DomElement& element, bool all)
   return WAbstractArea::updateDom(element, all);
 }
 
+std::string WCircleArea::updateAreaCoordsJS()
+{
+  std::stringstream coords;
+
+  coords << "[" << jsRef() << ",["
+    << x_ << ',' << y_ << ',' << r_ << "]]";
+
+  return coords.str();
+}
+
 }
diff --git a/src/Wt/WImage b/src/Wt/WImage
index 4e6576c..0a3c342 100644
--- a/src/Wt/WImage
+++ b/src/Wt/WImage
@@ -222,6 +222,10 @@ public:
    */
   EventSignal<>& imageLoaded();
 
+  void setTargetJS(std::string targetJS);
+  virtual std::string updateAreasJS();
+  virtual std::string setAreaCoordsJS();
+
 private:
   static const char *LOAD_SIGNAL;
 
@@ -233,6 +237,7 @@ private:
   WLink            imageLink_;
   Impl::MapWidget *map_;
   std::bitset<3>   flags_;
+  std::string      targetJS_;
 
   void resourceChanged();
 
@@ -240,10 +245,14 @@ protected:
   virtual void getDomChanges(std::vector<DomElement *>& result,
 			     WApplication *app);
   virtual void updateDom(DomElement& element, bool all);
+  virtual void defineJavaScript();
+  virtual void render(WFlags<RenderFlag> flags);
   virtual DomElementType domElementType() const;
   virtual void propagateRenderOk(bool deep);
+  virtual std::string updateAreaCoordsJSON() const;
 
   friend class WLabel;
+  friend class Impl::MapWidget;
 
   static std::vector<WAbstractArea *> noAreas_;
 };
diff --git a/src/Wt/WImage.C b/src/Wt/WImage.C
index 693a8c7..1893dbd 100644
--- a/src/Wt/WImage.C
+++ b/src/Wt/WImage.C
@@ -11,6 +11,10 @@
 
 #include "DomElement.h"
 
+#ifndef WT_DEBUG_JS
+#include "js/WImage.min.js"
+#endif
+
 namespace Wt {
 
 LOGGER("WImage");
@@ -23,6 +27,15 @@ LOGGER("WImage");
       MapWidget() { }
 
     protected:
+      virtual void render(WFlags<RenderFlag> flags)
+      {
+	WContainerWidget::render(flags);
+
+	WImage *parent_img = dynamic_cast<WImage *>(parent());
+	if (!parent_img->targetJS_.empty())
+	  parent_img->doJavaScript(parent_img->setAreaCoordsJS());
+      }
+
       virtual void updateDom(DomElement& element, bool all)
       {
 	if (all)
@@ -269,9 +282,75 @@ void WImage::propagateRenderOk(bool deep)
   WInteractWidget::propagateRenderOk(deep);
 }
 
+void WImage::defineJavaScript()
+{
+  WApplication *app = WApplication::instance();
+
+  LOAD_JAVASCRIPT(app, "js/WImage.js", "WImage", wtjs1);
+
+  WStringStream ss;
+  ss << "new " WT_CLASS ".WImage("
+     << app->javaScriptClass() << "," << jsRef() << "," << targetJS_ << ");";
+  doJavaScript(ss.str());
+}
+
+void WImage::render(WFlags<RenderFlag> flags)
+{
+  if (flags & RenderFull) {
+    if (!targetJS_.empty())
+      defineJavaScript();
+  }
+
+  WInteractWidget::render(flags);
+}
+
 DomElementType WImage::domElementType() const
 {
   return map_ ? DomElement_SPAN : DomElement_IMG;
 }
 
+void WImage::setTargetJS(std::string targetJS)
+{
+  targetJS_ = targetJS;
+}
+
+std::string WImage::updateAreasJS()
+{
+  WStringStream ss;
+  if (!targetJS_.empty()) {
+    ss << "jQuery.data(" << jsRef() << ", 'obj').updateAreas();";
+  }
+  return ss.str();
+}
+
+std::string WImage::setAreaCoordsJS()
+{
+  WStringStream ss;
+  if (!targetJS_.empty()) {
+    ss << "jQuery.data(" << jsRef() << ", 'obj').setAreaCoordsJSON("
+       << updateAreaCoordsJSON() << ");";
+  }
+  return ss.str();
+}
+
+std::string WImage::updateAreaCoordsJSON() const
+{
+  WStringStream js;
+  const std::vector<WAbstractArea *> &areas = this->areas();
+
+  if (!areas.empty()) {
+    for (int i = 0; i < areas.size(); ++i) {
+      if (areas[i]->isTransformable()) {
+        if (js.empty())
+          js << "[";
+        else
+          js << ",";
+        js << areas[i]->updateAreaCoordsJS();
+      }
+    }
+    js << "]";
+  }
+  return js.str();
+}
+
 }
diff --git a/src/Wt/WPaintedWidget.C b/src/Wt/WPaintedWidget.C
index 28dbac1..79cf6ec 100644
--- a/src/Wt/WPaintedWidget.C
+++ b/src/Wt/WPaintedWidget.C
@@ -6,6 +6,7 @@
 
 #include <boost/lexical_cast.hpp>
 
+#include "Wt/WAbstractArea"
 #include "Wt/WApplication"
 #include "Wt/WCanvasPaintDevice"
 #include "Wt/WEnvironment"
@@ -632,6 +633,10 @@ void WWidgetCanvasPainter::createContents(DomElement *result,
     WStringStream ss;
     ss << widget_->objJsRef() << ".repaint=function(){";
     ss << canvasDevice->recordedJs_.str();
+    if (widget_->areaImage_) {
+      widget_->areaImage_->setTargetJS(widget_->objJsRef());
+      ss << widget_->areaImage_->updateAreasJS();
+    }
     ss << "};";
     el->callJavaScript(ss.str());
   }
@@ -681,6 +686,10 @@ void WWidgetCanvasPainter::updateContents(std::vector<DomElement *>& result,
     WStringStream ss;
     ss << widget_->objJsRef() << ".repaint=function(){";
     ss << canvasDevice->recordedJs_.str();
+    if (widget_->areaImage_) {
+      widget_->areaImage_->setTargetJS(widget_->objJsRef());
+      ss << widget_->areaImage_->updateAreasJS();
+    }
     ss << "};";
     el->callJavaScript(ss.str());
   }
diff --git a/src/Wt/WPolygonArea b/src/Wt/WPolygonArea
index 2263edf..2a33f3f 100644
--- a/src/Wt/WPolygonArea
+++ b/src/Wt/WPolygonArea
@@ -112,6 +112,7 @@ private:
 
 protected:
   virtual bool updateDom(DomElement& element, bool all);
+  virtual std::string updateAreaCoordsJS() WT_CXX11ONLY(override);
 };
 
 }
diff --git a/src/Wt/WPolygonArea.C b/src/Wt/WPolygonArea.C
index 5602233..0ef9074 100644
--- a/src/Wt/WPolygonArea.C
+++ b/src/Wt/WPolygonArea.C
@@ -85,4 +85,19 @@ bool WPolygonArea::updateDom(DomElement& element, bool all)
   return WAbstractArea::updateDom(element, all);
 }
 
+std::string WPolygonArea::updateAreaCoordsJS()
+{
+  std::stringstream coords;
+
+  coords << "[" << jsRef() << ",[";
+  for (unsigned i = 0; i < points_.size(); ++i) {
+    if (i != 0)
+      coords << ',';
+    coords << points_[i].x() << ',' << points_[i].y();
+  }
+  coords << "]]";
+
+  return coords.str();
+}
+
 }
diff --git a/src/Wt/WRectArea b/src/Wt/WRectArea
index 5fc9cb8..0329d42 100644
--- a/src/Wt/WRectArea
+++ b/src/Wt/WRectArea
@@ -96,6 +96,7 @@ private:
 
 protected:
   virtual bool updateDom(DomElement& element, bool all);
+  virtual std::string updateAreaCoordsJS() WT_CXX11ONLY(override);
 };
 
 }
diff --git a/src/Wt/WRectArea.C b/src/Wt/WRectArea.C
index 8337ea1..f5dc12f 100644
--- a/src/Wt/WRectArea.C
+++ b/src/Wt/WRectArea.C
@@ -75,4 +75,14 @@ bool WRectArea::updateDom(DomElement& element, bool all)
   return WAbstractArea::updateDom(element, all);
 }
 
+std::string WRectArea::updateAreaCoordsJS()
+{
+  std::stringstream coords;
+
+  coords << "[" << jsRef() << ",["
+    << x_ << ',' << y_ << ',' << (x_ + width_) << ',' << (y_ + height_) << "]]";
+
+  return coords.str();
+}
+
 }
diff --git a/src/js/WCartesianChart.js b/src/js/WCartesianChart.js
index 7bf8614..c2ab7e6 100644
--- a/src/js/WCartesianChart.js
+++ b/src/js/WCartesianChart.js
@@ -205,6 +205,7 @@ WT_DECLARE_WT_MEMBER
 	 return mult([1,0,0,-1,l,b], mult(transform(X), mult(transform(Y), [1,0,0,-1,-l,b])));
       }
    }
+   target.combinedTransform = combinedTransform;
 
    function transformedChartArea() {
       return mult(combinedTransform(), config.area);
diff --git a/src/js/WImage.js b/src/js/WImage.js
new file mode 100644
index 0000000..242125e
--- /dev/null
+++ b/src/js/WImage.js
@@ -0,0 +1,57 @@
+/*
+ * Copyright (C) 2015 Emweb bvba, Herent, Belgium.
+ *
+ * See the LICENSE file for terms of use.
+ */
+
+/* Note: this is at the same time valid JavaScript and C++. */
+
+WT_DECLARE_WT_MEMBER
+(1, JavaScriptConstructor, "WImage",
+ function(APP, el, target) {
+   jQuery.data(el, 'obj', this);
+
+   var self = this;
+   var WT = APP.WT;
+
+   var areaCoordsJSON = null;
+
+   this.setAreaCoordsJSON = function(newAreaCoordsJSON) {
+     areaCoordsJSON = newAreaCoordsJSON;
+     this.updateAreas();
+   }
+
+   this.updateAreas = function() {
+     this.updateAreaCoords();
+   }
+
+   this.updateAreaCoords = function() {
+     var combinedTransformJS = target.combinedTransform;
+     if (combinedTransformJS === undefined || areaCoordsJSON === null)
+       return;
+     var combinedTransform = combinedTransformJS();
+     var mult = APP.WT.gfxUtils.transform_mult;
+
+     for (var c = 0; c < areaCoordsJSON.length; c++)
+     {
+       var coordEntry = areaCoordsJSON[c];
+       var areaEl = coordEntry[0];
+       var points = coordEntry[1];
+
+       var len = points.length;
+       var new_coords = "";
+       for (var i = 0; i + 1 < len; i += 2) {
+         if (i > 0)
+           new_coords += ",";
+         var p = mult(combinedTransform, points.slice(i, i + 2));
+         new_coords += Math.round(p[0]).toString() + "," + Math.round(p[1]).toString();
+       }
+
+     if (i < len) // a circle -- copy radius unchanged
+       new_coords += ("," + points[i].toString());
+     if (areaEl)
+       areaEl.coords = new_coords;
+     }
+   }
+
+ });
-- 
1.9.1

