Skip to content


Wicket Heatmap – Ajax mit Parametern

Wer wissen möchte, wohin die Nutzer in der eigenen Anwendung so klicken (z.B. auf Dinge, von denen man selbst nicht annehmen würde, das Nutzer darauf klicken), der muss jeden Mausklick des Nutzers aufzeichnen. Aus diesen Daten kann man dann ermitteln, wohin die Nutzer ihren Mauszeiger so wandern lassen. Dafür gibt es bereits Opensourcelösungen, die meist auf PHP basieren. Unter dem Suchbergriff Heatmap wird man auch bei Google fündig. Da sich alles in diesem Blog um Java und dann noch um Wicket dreht, lag es natürlich nahe, zu prüfen, ob und wie man diese Anforderung mit Wicket realisieren kann. Dieser Beitrag wäre viel kürzer, wenn man die Frage nach dem “ob” mit nein beantworten müsste. Kommen wir also zum “wie?”.

Für die Umsetzung habe ich mir anfänglich einiges aus dem Beispielen aus folgendem Blogbeitrag entlehnt. Im Laufe der Zeit ist zwar davon nicht mehr viel zu sehen, das Grundprinzip ist aber das Gleiche geblieben.

Folgende Fragestellung stand am Anfang dieses Versuchs: Wie kann man mit Wicket Werte in einem AjaxRequest übergeben (z.B. die Mausposition). Die erste Idee bestand darin, ein unsichtbares Formular zu erstellen, in das man die Werte per Javascript einfügt und dieses Formular per Ajax abschickt. Das hat auch funktioniert, war aber irgendwie auch ein wenig zu aufwendig. Nach einer Reihe missglückter Versuche, den richtigen Ansatz zu finden, stellte ich diese Frage in der Wicket-Mailingliste und bekam den entscheidenen Tipp von Ernesto Reinaldo Barreiro. Es gab einen Vortrag der London Wicket User Group, der dieses Problem löste. Mit dieser Vorarbeit begann ich das Thema umzusetzen. Dabei hat sich die eigene Implementierung vom Original entfernt. Hier nun Schritt für Schritt der vollständige Code:

AbstractParameterizedDefaultAjaxBehavior

 Java(TM) 2 Platform Standard Edition 5.0 |  copy code |? 
001
package de.wicketpraxis.web.blog.pages.questions.ajax.parameter;
002
 
003
import java.util.HashMap;
004
import java.util.Map;
005
 
006
import org.apache.wicket.Request;
007
import org.apache.wicket.RequestCycle;
008
import org.apache.wicket.ResourceReference;
009
import org.apache.wicket.ajax.AbstractDefaultAjaxBehavior;
010
import org.apache.wicket.ajax.AjaxRequestTarget;
011
import org.apache.wicket.markup.html.IHeaderResponse;
012
import org.apache.wicket.util.time.Duration;
013
 
014
public abstract class AbstractParameterizedDefaultAjaxBehavior extends AbstractDefaultAjaxBehavior
015
{
016
  static int sec=0;
017
 
018
  private Duration _throttleDelay;
019
 
020
  @Override
021
  public void renderHead(IHeaderResponse response)
022
  {
023
    super.renderHead(response);
024
    response.renderJavascriptReference(new ResourceReference(AbstractParameterizedDefaultAjaxBehavior.class,"AbstractParameterizedDefaultAjaxBehavior.js"));
025
  }
026
 
027
  @Override
028
  protected void respond(AjaxRequestTarget target)
029
  {
030
    Request request = RequestCycle.get().getRequest();
031
 
032
    Map<String,Object> map=new HashMap<String, Object>();
033
    Parameter<?>[] parameter = getParameter();
034
    for (Parameter<?> p : parameter)
035
    {
036
      String svalue = request.getParameter(p.getName());
037
      if (svalue!=null)
038
      {
039
        Object value=getComponent().getConverter(p.getType()).convertToObject(svalue, getComponent().getLocale());
040
        map.put(p.getName(), value);
041
      }
042
    }
043
 
044
    respond(target, new ParameterMap(map));
045
  }
046
 
047
  @Override
048
  public CharSequence getCallbackUrl(boolean onlyTargetActivePage)
049
  {
050
    StringBuilder sb=new StringBuilder();
051
    sb.append(super.getCallbackUrl(onlyTargetActivePage));
052
 
053
    Parameter<?>[] parameter = getParameter();
054
    for (Parameter<?> p : parameter)
055
    {
056
      sb.append("&").append(p.getName()).append("='+").append(p.getJavascript()).append("+'");
057
    }
058
 
059
    return sb.toString();
060
  }
061
 
062
  @Override
063
  protected final CharSequence getCallbackScript()
064
  {
065
    if (_throttleDelay!=null)
066
    {
067
      return throttleScript(super.getCallbackScript(),"thw"+(sec++),_throttleDelay);
068
    }
069
    return super.getCallbackScript();
070
  }
071
 
072
  protected static class Parameter<T>
073
  {
074
    String _name;
075
    String _javascript;
076
    Class<T> _type;
077
 
078
    protected Parameter(String name,Class<T> type,String javascript)
079
    {
080
      _name=name;
081
      _type=type;
082
      _javascript=javascript;
083
    }
084
 
085
    protected String getName()
086
    {
087
      return _name;
088
    }
089
    protected String getJavascript()
090
    {
091
      return _javascript;
092
    }
093
    protected Class<T> getType()
094
    {
095
      return _type;
096
    }
097
  }
098
 
099
  protected static <T> Parameter<T> of(String name,Class<T> type,String javascript)
100
  {
101
    return new Parameter<T>(name, type, javascript);
102
  }
103
 
104
  protected static class ParameterMap
105
  {
106
    Map<String, Object> _map;
107
 
108
    protected ParameterMap(Map<String, Object> map)
109
    {
110
      _map=map;
111
    }
112
 
113
    public <T> T getValue(Parameter<T> parameter)
114
    {
115
      return (T) _map.get(parameter.getName());
116
    }
117
  }
118
 
119
  public final AbstractParameterizedDefaultAjaxBehavior setThrottleDelay(Duration throttleDelay)
120
  {
121
    _throttleDelay=throttleDelay;
122
    return this;
123
  }
124
 
125
  protected abstract void respond(AjaxRequestTarget target,ParameterMap parameterMap);
126
 
127
  protected abstract Parameter<?>[] getParameter();
128
 
129
}

Die drei wichtigsten Bestandteile dieser Klasse sind folgende: Die Klasse Parameter definiert den Namen, den Typ, und das Javascript, das für das Ermitteln des Wertes im Browser aufgerufen muss. Die Methode respond(AjaxRequestTarget) liest die Werte aus dem Request aus, konvertiert diese in den gewünschen Typ und ruf damit eine zu überschreibende Methode auf. Die Methode getCallbackUrl() liefert das Javascript-Fragment für die Url, die dann per Ajax aufgerufen wird.

Um die Position des Mauszeigers ermitteln zu können, muss man sich mit einer eigenen Funktion für so einen Event registrieren. Damit mehr als eine Funktion auf so einen Event reagieren kann, sollte man die Funktion, die davor registriert war, ebenfalls aufrufen. Um für diese Problematik, die vermutlich in allen abgeleiteten Klassen vorhanden ist, besser lösen zu können, binden wir automatisch eine hilfreiche Javascript-Klasse ein.

 Javascript |  copy code |? 
01
Callback = {
02
  create: function(oldCallback,newCallback)
03
  {
04
    return function(a,b,c,d,e,f)
05
    {
06
      if (oldCallback) 
07
      {
08
        oldCallback(a,b,c,d,e,f);
09
      }
10
      newCallback(a,b,c,d,e,f);
11
    }
12
  },
13
};

Der Code sieht etwas merkwürdig aus. Das liegt an folgenden Gründen: zum einen stehe ich mit Javascript immer noch auf Kriegsfuß (mit Wicket kann man den Javascript-Teil sehr schön verstecken) und habe daher keine geeignete Lösung gefunden, mit der es mir möglich war, alle Argumente des Funktionsaufrufs an die zwei Funktionen weiterzureichen. Da es aber Javascript egal ist, mit wie vielen Parametern man eine Funktion aufruft, werden auf diese Weise bis zu 6 Übergabeparameter weitergereicht. Für Hinweise an dieser Stelle bin ich extrem dankbar.

WicketWindowJavascript

Für vieles bringt Wicket bereits fertige Implementierungen mit. Allerdings verstecken sich diese manchmal an unauffälligen Stellen. Für das Ermitteln des sichtbaren Bereichs fand ich die nötigen Javascript-Funktionen bei der ModelWindow-Klasse. Da ich nicht das vollständige ModalWindow-Javascript einbinden wollte, habe ich diese allgemeinen Funktionen in eine eigene Resource verpackt.

 Java(TM) 2 Platform Standard Edition 5.0 |  copy code |? 
01
package de.wicketpraxis.web.blog.pages.questions.ajax.parameter;
02
 
03
import org.apache.wicket.ResourceReference;
04
 
05
public class WicketWindowJavascript
06
{
07
  private WicketWindowJavascript()
08
  {
09
  }
10
 
11
  public static final ResourceReference RESOURCE=new ResourceReference(WicketWindowJavascript.class,"WicketWindowJavascript.js");
12
}

 Javascript |  copy code |? 
01
// aus der Datei modal.js zur Klasse ModalWindow
02
 
03
if (typeof(Wicket.Window) == "undefined") {
04
  Wicket.Window = { };
05
}
06
 
07
/**
08
 * Returns the height of visible area.
09
 */
10
Wicket.Window.getViewportHeight = function() {
11
  if (window.innerHeight != window.undefined) 
12
    return window.innerHeight;
13
 
14
  if (document.compatMode == 'CSS1Compat') 
15
    return document.documentElement.clientHeight;
16
 
17
  if (document.body) 
18
    return document.body.clientHeight;
19
 
20
  return window.undefined; 
21
}
22
 
23
/**
24
 * Returns the width of visible area.
25
 */
26
Wicket.Window.getViewportWidth =  function() {
27
  if (window.innerWidth != window.undefined) 
28
    return window.innerWidth;
29
 
30
  if (document.compatMode == 'CSS1Compat') 
31
    return document.documentElement.clientWidth; 
32
 
33
  if (document.body) 
34
    return document.body.clientWidth;
35
 
36
  return window.undefined;
37
}
38
 
39
/**
40
 * Returns the horizontal scroll offset
41
 */
42
Wicket.Window.getScrollX = function() {
43
  var iebody = (document.compatMode && document.compatMode != "BackCompat") ? document.documentElement : document.body  
44
  return document.all? iebody.scrollLeft : pageXOffset
45
}
46
 
47
/**
48
 * Returns the vertical scroll offset
49
 */
50
Wicket.Window.getScrollY = function() {
51
  var iebody = (document.compatMode && document.compatMode != "BackCompat") ? document.documentElement : document.body  
52
  return document.all? iebody.scrollTop : pageYOffset
53
}
54
 
55
 
56
/**
57
 * Returns element offset
58
 */
59
Wicket.Window.getXYOffset = function(obj)
60
{
61
  var curleft = 0;
62
  var curtop = 0;
63
  if (obj.offsetParent)
64
  {
65
    while (obj.offsetParent)
66
    {
67
      curleft += obj.offsetLeft;
68
      curtop += obj.offsetTop;
69
      obj = obj.offsetParent;
70
    }
71
  }
72
  else
73
  {
74
    if (obj.x)
75
    {
76
      curleft += obj.x;
77
    }
78
    if (obj.y)
79
    {
80
      curtop += obj.y;
81
    }
82
  }
83
 
84
  if (Wicket.Browser.isIE())
85
  {
86
    bodyElement=document.getElementsByTagName('body')[0];
87
    // In IE there's a default margin in the page body. If margin's not defined,
88
    // use defaults
89
    var marginLeftExplorer  = parseInt(bodyElement.style.marginLeft);
90
    var marginTopExplorer   = parseInt(bodyElement.style.marginTop);
91
    /* assume default 10px/15px margin in explorer */
92
    if (isNaN(marginLeftExplorer)) {marginLeftExplorer=10;}
93
    if (isNaN(marginTopExplorer)) {marginTopExplorer=15;}
94
    curleft=curleft+marginLeftExplorer;
95
    curtop=curtop+marginTopExplorer;
96
  }
97
 
98
  return [curleft,curtop];
99
}

Hinzugefügt habe ich nur die Methode Wicket.Window.getXYOffset(), um den Offset für ein bestimmtes oder das erste Kindelement der Seite ermitteln zu können. Eine Klasse, die diese Methoden benötigt, muss die Resource entsprechend einbinden.

WindowResizeBehavior

Um die Heatmap in der richtigen Größe zeichnen zu können, benötigen wir Informationen über die Dimensionen des sichtbaren Bereichs. Für window.onresize wird ein neuer Callback registriert. Die Werte können über die ensprechend Wicket.Window-Funktionen ermittelt werden. Daher wird nicht nur der Callback registiert, sondern die Funktion nach dem Laden der Seite direkt aufgerufen. Auf diese Weise bekommen wir die Informationen über den sichtbaren Bereich nicht erst, wenn der Nutzer die Fenstergröße verändert.

 Java(TM) 2 Platform Standard Edition 5.0 |  copy code |? 
01
package de.wicketpraxis.web.blog.pages.questions.ajax.parameter;
02
 
03
import org.apache.wicket.ResourceReference;
04
import org.apache.wicket.ajax.AjaxRequestTarget;
05
import org.apache.wicket.markup.html.IHeaderResponse;
06
import org.apache.wicket.util.time.Duration;
07
 
08
public abstract class WindowResizeBehavior extends AbstractParameterizedDefaultAjaxBehavior
09
{
10
  static final Parameter<Integer> WIDTH=of("width", Integer.class, "Wicket.Window.getViewportWidth()");
11
  static final Parameter<Integer> HEIGHT=of("height", Integer.class, "Wicket.Window.getViewportHeight()");
12
 
13
  @Override
14
  public void renderHead(IHeaderResponse response)
15
  {
16
    super.renderHead(response);
17
    response.renderJavascriptReference(WicketWindowJavascript.RESOURCE);
18
    response.renderOnDomReadyJavascript(getJavascript());
19
    response.renderOnDomReadyJavascript(getCallbackScript().toString());
20
  }
21
 
22
  protected final String getJavascript()
23
  {
24
    return "window.onresize = Callback.create(window.onresize,function () {"+getCallbackScript()+"});";
25
  }
26
 
27
  @Override
28
  protected final Parameter<?>[] getParameter()
29
  {
30
    return new Parameter<?>[]{ WIDTH,HEIGHT};
31
  }
32
 
33
  @Override
34
  protected void respond(AjaxRequestTarget target, ParameterMap parameterMap)
35
  {
36
    onResize(target, parameterMap.getValue(WIDTH), parameterMap.getValue(HEIGHT));    
37
  }
38
 
39
  protected abstract void onResize(AjaxRequestTarget target, int width, int height);
40
}

ElementOffsetBehavior

Die Bestimmung der richtigen Mauskoordinaten gestaltet sich insofern schwierig, als das nicht jede Webseite bei jeder Fenstergröße gleich aussieht. Damit die Koordinaten bei der Auswertung verwendbar bleiben, berechnen wir die Position in Relation zu einem Element, dass den Rahmen der Seite darstellen sollte (z.B. das erste div-Tag innerhalb des body-Tags, dass auf eine feste Breite gesetzt wurde und im sichtbaren Bereich zentriert dargestellt wird).

 Java(TM) 2 Platform Standard Edition 5.0 |  copy code |? 
01
package de.wicketpraxis.web.blog.pages.questions.ajax.parameter;
02
 
03
import org.apache.wicket.ResourceReference;
04
import org.apache.wicket.ajax.AjaxRequestTarget;
05
import org.apache.wicket.markup.html.IHeaderResponse;
06
 
07
import de.wicketpraxis.web.blog.pages.questions.ajax.parameter.AbstractParameterizedDefaultAjaxBehavior.Parameter;
08
 
09
public abstract class ElementOffsetBehavior extends AbstractParameterizedDefaultAjaxBehavior
10
{
11
  static final Parameter<Integer> X_OFFSET=of("xOffset", Integer.class, "xOffset");
12
  static final Parameter<Integer> Y_OFFSET=of("yOffset", Integer.class, "yOffset");
13
 
14
  private String _contentId;
15
 
16
  @Override
17
  public void renderHead(IHeaderResponse response)
18
  {
19
    super.renderHead(response);
20
    response.renderJavascriptReference(WicketWindowJavascript.RESOURCE);
21
    response.renderJavascriptReference(new ResourceReference(ElementOffsetBehavior.class,"ElementOffsetBehavior.js"));
22
    response.renderOnDomReadyJavascript(getJavascript());
23
  }
24
 
25
  public ElementOffsetBehavior()
26
  {
27
 
28
  }
29
 
30
  public ElementOffsetBehavior(String contentId)
31
  {
32
    _contentId=contentId;
33
  }
34
 
35
  protected final String getJavascript()
36
  {
37
    if (_contentId!=null) return "ElementOffsetBehavoir.init(function (xOffset,yOffset) {"+getCallbackScript()+"},'"+_contentId+"');";
38
    return "ElementOffsetBehavoir.init(function (xOffset,yOffset) {"+getCallbackScript()+"});";
39
  }
40
 
41
  @Override
42
  protected final Parameter<?>[] getParameter()
43
  {
44
    return new Parameter<?>[]{ X_OFFSET,Y_OFFSET};
45
  }
46
 
47
  @Override
48
  protected void respond(AjaxRequestTarget target, ParameterMap parameterMap)
49
  {
50
    onOffset(target, parameterMap.getValue(X_OFFSET), parameterMap.getValue(Y_OFFSET));   
51
  }
52
 
53
  protected abstract void onOffset(AjaxRequestTarget target, int xOffset, int yOffset);
54
}

Im Gegensatz zum letzten Behavior sind wir in diesem Fall auf etwas mehr Javascript angewiesen.

 Javascript |  copy code |? 
01
ElementOffsetBehavoir =
02
{
03
  init : function(callback, contentId)
04
  {
05
    function Listener(callback, contentId)
06
    {
07
      this.xOffset = 0;
08
      this.yOffset = 0;
09
      this.firstElement = null;
10
 
11
      // function(xOffset,yOffset)
12
      this.onOffsetChanged = callback;
13
 
14
      this.updateOffsets = function()
15
      {
16
        var offsets = Wicket.Window.getXYOffset(this.firstElement);
17
        this.xOffset = offsets[0];
18
        this.yOffset = offsets[1];
19
        this.onOffsetChanged(this.xOffset, this.yOffset);
20
      };
21
 
22
      var bodyElement = document.getElementsByTagName('body')[0];
23
      this.firstElement = bodyElement.childNodes[1];
24
      if (contentId != null)
25
      {
26
        this.firstElement = document.getElementById(contentId);
27
      }
28
    }
29
 
30
    var listener = new Listener(callback, contentId);
31
    listener.updateOffsets();
32
    window.onresize = Callback.create(window.onresize, function()
33
    {
34
      listener.updateOffsets();
35
    });
36
  }
37
}

Der Listener wird über die init-Methode initialisiert und wird ebenfalls aufgerufen, wenn die Fenstergröße verändert wurde. Außerdem wird die updateOffsets()-Methode bereits bei der Initialisierung aufgerufen. Somit ist auch in diesem Fall der Offset eines Elements bekannt.

PageMouseClickBehavior

Das letzte Behavior ist gleichzeitig das aufwendigste. In diesem Fall muss nicht nur die Position des Klicks ermittelt werden, sondern gleichzeitig in Relation zu einem Element gebracht werden. Auf diese Weise kommen die Koordinaten schon passend an und können z.B. auch negative Werte annehmen.

 Java(TM) 2 Platform Standard Edition 5.0 |  copy code |? 
01
package de.wicketpraxis.web.blog.pages.questions.ajax.parameter;
02
 
03
import org.apache.wicket.Request;
04
import org.apache.wicket.ResourceReference;
05
import org.apache.wicket.ajax.AjaxRequestTarget;
06
import org.apache.wicket.markup.html.IHeaderResponse;
07
 
08
public abstract class PageMouseClickBehavior extends AbstractParameterizedDefaultAjaxBehavior
09
{
10
  static final Parameter<Integer> MOUSE_X=of("x", Integer.class, "x");
11
  static final Parameter<Integer> MOUSE_Y=of("y", Integer.class, "y");
12
 
13
  String _contentId;
14
 
15
  public PageMouseClickBehavior()
16
  {
17
  }
18
 
19
  public PageMouseClickBehavior(String contentId)
20
  {
21
    _contentId=contentId;
22
  }
23
 
24
  @Override
25
  public void renderHead(IHeaderResponse response)
26
  {
27
    super.renderHead(response);
28
 
29
    response.renderJavascriptReference(WicketWindowJavascript.RESOURCE);
30
    response.renderJavascriptReference(new ResourceReference(PageMouseClickBehavior.class,"PageMouseClickBehavior.js"));
31
    response.renderOnDomReadyJavascript(getJavascript());
32
  }
33
 
34
  protected String getJavascript()
35
  {
36
    if (_contentId!=null) return "PageMouseClickBehavoir.init(function (x,y,xOffset,yOffset) {"+getCallbackScript()+"},'"+_contentId+"');";
37
    return "PageMouseClickBehavoir.init(function (x,y,xOffset,yOffset) {"+getCallbackScript()+"});";
38
  }
39
 
40
  @Override
41
  protected Parameter<?>[] getParameter()
42
  {
43
    return new Parameter<?>[] { MOUSE_X, MOUSE_Y };
44
  }
45
 
46
  @Override
47
  protected void respond(AjaxRequestTarget target, ParameterMap map)
48
  {
49
    onClick(target, map.getValue(MOUSE_X), map.getValue(MOUSE_Y));    
50
  }
51
 
52
  protected abstract void onClick(AjaxRequestTarget target, int x, int y);
53
}

Das dazugehörige Javascript kümmert sich um die Verarbeitung:

 Javascript |  copy code |? 
01
PageMouseClickBehavoir = 
02
{
03
  init: function(callback,contentId)
04
  {
05
    function Listener(callback,contentId)
06
    {
07
      this.xOffset=0;
08
      this.yOffset=0;
09
      this.firstElement=null;
10
 
11
      // function(x,y)
12
      this.onMouseEvent=callback;
13
 
14
      this.mouseEvent= function(e)
15
      {
16
        tempX=0;
17
        tempY=0;
18
 
19
        if (Wicket.Browser.isIE() || Wicket.Browser.isGecko())
20
        {
21
          tempX = e.clientX + Wicket.Window.getScrollX();
22
          tempY = e.clientY + Wicket.Window.getScrollY();
23
        }
24
        else
25
        {
26
          tempX = e.pageX
27
          tempY = e.pageY
28
        }
29
        tempX-=this.xOffset;
30
        tempY-=this.yOffset;
31
        this.onMouseEvent(tempX,tempY);
32
        return true;
33
      };
34
 
35
      this.updateOffsets=function()
36
      {
37
        var offsets=Wicket.Window.getXYOffset(this.firstElement);
38
        this.xOffset=offsets[0];
39
        this.yOffset=offsets[1];
40
      };
41
 
42
      var bodyElement=document.getElementsByTagName('body')[0];
43
      this.firstElement=bodyElement.childNodes[1];
44
      if (contentId!=null)
45
      {
46
        this.firstElement=document.getElementById(contentId);
47
      }
48
 
49
      this.updateOffsets();
50
    }
51
 
52
 
53
    var listener=new Listener(callback,contentId);
54
 
55
    document.onmousedown=Callback.create(document.onmousedown,function(e)
56
    {
57
      listener.mouseEvent(e)
58
    });
59
    window.onresize=Callback.create(window.onresize,function()
60
    {
61
      listener.updateOffsets();
62
    });
63
  },
64
}
65

Sobald der Nutzer klickt (auch wenn es eigentlich nichts klickbares gibt), wird eine Request an die Anwendung gesendet. Diese Behavior können wir nun in die Anwendung einbauen, um die Mausklicks der Nutzer aufzuzeichnen.

Die Anwendung

In unserer Beispielanwendung werfen wir alles in einen Topf. Wir zeichnen alle Mausklicks auf und aktualisieren dann die Heatmap.

 Java(TM) 2 Platform Standard Edition 5.0 |  copy code |? 
001
package de.wicketpraxis.web.blog.pages.questions.ajax.parameter;
002
 
003
import java.awt.AlphaComposite;
004
import java.awt.BasicStroke;
005
import java.awt.Color;
006
import java.awt.Composite;
007
import java.awt.Graphics2D;
008
import java.awt.RenderingHints;
009
import java.io.Serializable;
010
import java.util.ArrayList;
011
import java.util.List;
012
 
013
import org.apache.wicket.ajax.AjaxRequestTarget;
014
import org.apache.wicket.markup.html.WebMarkupContainer;
015
import org.apache.wicket.markup.html.WebPage;
016
import org.apache.wicket.markup.html.image.Image;
017
import org.apache.wicket.markup.html.image.NonCachingImage;
018
import org.apache.wicket.markup.html.image.resource.DynamicImageResource;
019
import org.apache.wicket.markup.html.image.resource.RenderedDynamicImageResource;
020
import org.apache.wicket.markup.html.panel.FeedbackPanel;
021
import org.apache.wicket.protocol.http.WebResponse;
022
import org.apache.wicket.util.time.Duration;
023
 
024
public class HeatMapPage extends WebPage
025
{
026
  List<Pos> _points=new ArrayList<Pos>();
027
  int _xOffset=0;
028
  int _yOffset=0;
029
 
030
  public HeatMapPage()
031
  {
032
    final FeedbackPanel feedbackPanel = new FeedbackPanel("feedback");
033
    feedbackPanel.setOutputMarkupId(true);
034
    add(feedbackPanel);
035
 
036
    final WebMarkupContainer box=new WebMarkupContainer("box");
037
    final ClickMap imageResource = new ClickMap(100, 100);
038
    final Image image = new NonCachingImage("map",imageResource);
039
    box.setOutputMarkupId(true);
040
    box.add(image);
041
    add(box);
042
 
043
    add(new ElementOffsetBehavior("#content")
044
    {
045
      @Override
046
      protected void onOffset(AjaxRequestTarget target, int xOffset, int yOffset)
047
      {
048
        info("Offset: "+xOffset+","+yOffset);
049
        _xOffset=xOffset;
050
        _yOffset=yOffset;
051
        imageResource.invalidate();
052
        target.addComponent(feedbackPanel);
053
        target.addComponent(box);
054
      }
055
    }.setThrottleDelay(Duration.milliseconds(250)));
056
 
057
    add(new PageMouseClickBehavior("#content")
058
    {
059
      @Override
060
      protected void onClick(AjaxRequestTarget target, int x, int y)
061
      {
062
        info("Clicked: "+x+","+y);
063
        _points.add(new Pos(x,y));
064
        imageResource.invalidate();
065
        target.addComponent(box);
066
        target.addComponent(feedbackPanel);
067
      }
068
    }.setThrottleDelay(Duration.milliseconds(50)));
069
 
070
    add(new WindowResizeBehavior()
071
    {
072
      @Override
073
      protected void onResize(AjaxRequestTarget target, int width, int height)
074
      {
075
        info("Size changed: "+width+","+height);
076
        imageResource.setWidth(width);
077
        imageResource.setHeight(height);
078
        imageResource.invalidate();
079
 
080
        target.addComponent(feedbackPanel);
081
        target.addComponent(box);
082
      }
083
    }.setThrottleDelay(Duration.milliseconds(250)));
084
 
085
  }
086
 
087
  class ClickMap extends RenderedDynamicImageResource
088
  {
089
    public ClickMap(int width, int height)
090
    {
091
      super(width, height,"jpg");
092
      setCacheable(false);
093
    }
094
 
095
    @Override
096
    protected boolean render(Graphics2D graphics)
097
    {
098
      graphics.setBackground(new Color(255,255,255));
099
      graphics.setColor(new Color(0,0,0,50));
100
      graphics.clearRect(0, 0, getWidth(), getHeight());
101
 
102
      graphics.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
103
 
104
      for (Pos p : _points)
105
      {
106
        graphics.fillArc(_xOffset+p.getX()-5, _yOffset+p.getY()-5, 9, 9, 0, 360);
107
      }
108
      return true;
109
    }
110
  }
111
 
112
  static class Pos implements Serializable
113
  {
114
    int _x;
115
    int _y;
116
 
117
    public Pos(int x, int y)
118
    {
119
      super();
120
      _x = x;
121
      _y = y;
122
    }
123
 
124
    public int getX()
125
    {
126
      return _x;
127
    }
128
 
129
    public int getY()
130
    {
131
      return _y;
132
    }
133
  }
134
}

Im Markup müssen wir dann nur das Image hinter der Seite platzieren und schon stimmen Mausklick und Heatmap überein.

 HTML |  copy code |? 
01
<html>
02
  <head>
03
    <title>Heatmap Page</title>
04
  </head>
05
  <body>
06
    <div id="#content" style="width:800px; margin:auto; border:1px solid #888;">
07
      <div wicket:id="feedback"></div>
08
    </div>
09
    <div wicket:id="box" style="z-index: -1; position: absolute; top: 0px;left: 0px;">
10
      <img wicket:id="map">
11
    </div>
12
  </body>
13
</html>

In diesem Beispiel haben ich den Rahmen auf eine Breite von 800 Pixeln gesetzt und zentriert. Trotzdem werden die Mausklicks immer korrekt angezeigt.

wicket-heatmap-ajax

Wie man sehen kann, ist die Interaktion von Wicket mit Ereignissen, die durch Javascript ausgelöst werden, ohne weiteres möglich. Dabei können diese Komponenten ohne weiteres in beliebigen Anwendungen benutzt werden, ohne das sich ein anderer Entwickler mit den Implementierungsdetails beschäftigen muss. Allerdings hätte ich mir gewünscht, dass die Parameterisierung von Ajax-Aufrufen bereits in den Kern von Wicket integriert wäre.

Viel Spass mit dem ausprobieren:)

Share and Enjoy:
  • Digg
  • Sphinn
  • del.icio.us
  • Facebook
  • Mixx
  • Google Bookmarks

Andere Beiträge

Posted in Wicket.

Tagged with , , , .


2 Responses

Stay in touch with the conversation, subscribe to the RSS feed for comments on this post.

  1. Thorsten Krüger says

    Das sieht aus, als müsste ich es dringend mal ausprobieren – einige schöne Tricks! Eine saubere Implementierung von Ajax mit Parametern ist ja an sich schon was Nettes.

    Was das JavaScript angeht, hier sollte apply() helfen. Der folgende Code:
    oldCallback.apply(this,arguments)
    ruft oldCallback mit dem Scope der aktuellen Funktion und allen Parametern dieses Scopes auf. Ein kleines Beispiel dazu findest Du unter .

    • Thorsten Krüger says

      Hmm, der Link wollte mich nicht. Zweiter Versuch:



Some HTML is OK

or, reply to this post via trackback.