Essai d’utilisation de la bibliothèque « KineticJS » dans un projet GWT (Google Web Toolkit)

Introduction

Le but de cet article est de tester la bibliothèque "KineticJS". Celle-ci bénéficie d'une meilleure intégration à GWT que "D3.js" car un wrapper "KineticGWT" existe.

Du coup elle devrait la remplacer avantageusement dans un projet de ce type.

Projet de test

Pour générer le projet de test je vais utiliser un archetype Maven pour générer le squelette du projet.

mvn archetype:generate \
   -DarchetypeGroupId=org.codehaus.mojo \
   -DarchetypeArtifactId=gwt-maven-plugin \
   -DarchetypeVersion=2.5.0
  • groupId: conceptforge.kinetics
  • artifactId: TestKinetics
  • version: 1.0-SNAPSHOT
  • package: conceptforge.kinetics
  • module: TestKinetics

A ce stade, le projet est fonctionnel, mais c'est le projet par défaut généré par l'archetype Maven.

Après une bonne cure d'amaigrissement, le projet est devenu un magnifique "Hello World" à partir duquel je vais pouvoir commencer à intégrer "KineticJS".

Voici un aperçu des fichiers qui restent dans le projet :

Le code source complet de ce Hello World est disponible ici : TestKinetics1.tar.gz

Intégration de la librairie KineticsJS

Le portage de cette librairie sous GWT porte (naturellement) le nom de "KineticGWT". Le lien "github" met à disposition de la documentation pour l'intégration de cette librairie dans une application GWT/Maven. Ça tombe bien, c'est exactement le cas de ce programme de test.

Il faut donc :

  • Ajouter les nouvelles dépendances dans "pom.xml"
  • Ajouter le module dans "TestKinetics.gwt.xml"
  • Modifier l'implémentation de "onModuleLoad()"
package conceptforge.kinetics.client;
 
import com.google.gwt.core.client.EntryPoint;
import com.google.gwt.dom.client.Element;
import com.google.gwt.user.client.DOM;
import com.google.gwt.user.client.ui.RootPanel;
import net.edzard.kinetic.*;
 
/**
 * Entry point classes define
 * <code>onModuleLoad()</code>.
 */
public class TestKinetics implements EntryPoint {
 
    /**
     * This is the entry point method.
     */
    public void onModuleLoad() {
        // Kinetic needs a special div in the DOM
        Element div = DOM.createDiv();
        RootPanel.getBodyElement().appendChild(div);
 
        // Setup stage
        Stage stage = Kinetic.createStage(div, 400, 400);
        Layer layer = Kinetic.createLayer();
        stage.add(layer);
 
        Rectangle c = Kinetic.createRectangle(new Box2d(10, 10, 200, 200));
        layer.add(c);
        stage.draw();
    }
}

Résultat

Site du zéro

Par chance le site du zéro propose un tutoriel sur KineticsJS.

Formes prédéfinies

Voici un exemple librement inspiré de la première partie du tutoriel (formes prédéfinies)

package conceptforge.kinetics.client;
 
import com.google.gwt.core.client.EntryPoint;
import com.google.gwt.dom.client.Element;
import com.google.gwt.user.client.DOM;
import com.google.gwt.user.client.ui.RootPanel;
import net.edzard.kinetic.*;
 
/**
 * Entry point classes define
 * <code>onModuleLoad()</code>.
 */
public class TestKinetics implements EntryPoint {
 
    /**
     * This is the entry point method.
     */
    public void onModuleLoad() {
        Element div = DOM.createDiv();
        RootPanel.getBodyElement().appendChild(div);
 
        Stage scene = Kinetic.createStage(div, 400, 400);
        Layer calque = Kinetic.createLayer();
        scene.add(calque);
 
        Rectangle r = Kinetic.createRectangle(new Box2d(50, 75, 350, 300));
        r.setFill(Colour.blue);
        r.setStroke(Colour.blue);
        r.setStrokeWidth(0);
 
        Circle c = Kinetic.createCircle(new Vector2d(50, 50), 50);
        c.setFill(Colour.green);
        c.setStroke(Colour.darkgray);        
 
        RegularPolygon h = Kinetic.createRegularPolygon(new Vector2d(200,150), 50, 6);
        h.setRotation(30);
        h.setFill(Colour.red);
 
        calque.add(r);
        calque.add(c);
        calque.add(h);
 
        scene.draw();
    }
}

Résultat

Ombres

En m'inspirant du tutoriel "Opacité et ombre", j'ai ajouté une ombre aux formes de l'exemple précédent :

Pour arriver à ce résultat il suffit de créer l'instance suivante de "Shadow":

Shadow shadow = new Shadow(Colour.black, 12, new Vector2d(8, 8), 0.7);

Ensuite, il faut utiliser la méthode "setShadow" pour passer cette valeur à toutes les formes.

Gestion des événements

En m'inspirant du tutoriel "Interactions", j'ai écrit le code suivant pour observer le contenu de la variable "evt" passée en paramètre dans la gestion d'événements :

r.addEventListener(Event.Type.CLICK, new Node.EventListener() {
 
            public boolean handle(Event evt) {
                try {
                    Window.alert(evt.toString());
                } catch (Exception e) {
                    Window.alert(e.getMessage());
                }
                return true;
            }
        });

En testant, j'ai eu la désagréable surprise de recevoir le message suivant :

Grosse déception

Alors que tous mes tests jusqu'à maintenant me montraient une bonne librairie pour manipuler la balise Canvas et un bon wrapper pour GWT. Je viens de toucher aux limites de cette implémentation.
Lors de la gestion d'événements, la méthode "handle" d'un EventListener contient une variable evt mais celle-ci contient toujours "null".
Après une petite recherche dans le code source, j'ai trouvé (dans l'implémentation de la classe "Node") la méthode suivante qui explique mon problème :

	/**
	 * Add a multi-event listener to the node.
	 * @param eventTypes A number of events to listen to
	 * @param handler The handler.
	 */
	// Seems to be buggy in kineticjs. Always getting mousemove events
	// TODO Maik: It's not buggy, it just fails and then creates too many events due to the failure.
	//             the evt variable is directly from the browser, not from kineticjs, that's why there's
	//             a mismatch between event types
	//             Touch also needs to be handled differently if evt should be used.
	//       quick fix: ignore evt object
	public final native void addEventListener(List eventTypes, EventListener handler) /*-{
		this.on(@net.edzard.kinetic.Node::createEventTypeString(Ljava/util/List;)(eventTypes), function(evt) {
//			if (evt != null) {
//				console.log(evt.type);
//				var javaEvt = @net.edzard.kinetic.Event::new(Lnet/edzard/kinetic/Event$Type;Lnet/edzard/kinetic/Event$Button;II)(
//					@net.edzard.kinetic.Event.Type::valueOf(Ljava/lang/String;)(evt.type.toUpperCase()),
//					@net.edzard.kinetic.Event.Button::fromInteger(I)(evt.button),
//					evt.offsetX,
//					evt.offsetY
//				);
//				javaEvt.@net.edzard.kinetic.Event::setShape(Lnet/edzard/kinetic/Shape;)(evt.shape);
//				var bubble = handler.@net.edzard.kinetic.Node.EventListener::handle(Lnet/edzard/kinetic/Event;)(javaEvt);
//				evt.cancelBubble = !bubble;
//			} else {
				handler.@net.edzard.kinetic.Node.EventListener::handle(Lnet/edzard/kinetic/Event;)(null);
//			}
		});
	}-*/;

En attendant une correction de cette méthode, je propose une solution de contournement (pas très élégante, forcément) :

r.addEventListener(Event.Type.CLICK, new Node.EventListener() {
 
            public boolean handle(Event evt) {
                if (r.getFill().toString().equals(Colour.blue.toString())) {
                    r.setFill(Colour.white);
                } else {
                    r.setFill(Colour.blue);
                }
 
                scene.draw();
                return true;
            }
        });

Je fais un accès direct à l'objet. Ce n'est pas élégant et c'est beaucoup moins souple, mais, au moins, c'est fonctionnel.
En écrivant cette petite routine, j'ai eu une deuxième mauvaise surprise, mais moins grave que la précédente. La comparaison de la couleur ne fonctionne ni avec "==" ni avec "equals".

Conclusion

Tout s'est très bien passé jusqu'à ce que j'essaye de gérer les événements. Mon soupçon est que la librairie "KineticJS" est très fonctionnelle. Ce soupçon n'est pas fondé sur des arguments techniques, mais plutôt construit à partir des différents commentaires que j'ai pu lire à ce sujet sur le Web.

Par contre, le wrapper "KineticGWT" m'a permis de réaliser très facilement les exemples du tutoriel que j'ai utilisé tant que je ne faisais pas de gestion d'événements. Pour l'instant, il n'est qu'en version "0.9.1" c'est donc certainement un bug de jeunesse, mais à ce stade je ne sais pas si j'ose prendre le risque d'intégrer ce wrapper dans un projet réel et contourner tous les petits problèmes résiduels...

Représentation de données arborescentes à l’aide de D3.js

Introduction

Le but de cet article est de mettre en oeuvre la librairie D3.js pour représenter des données sous forme d’arborescence.

Données

Les données que je souhaite exploiter dans cet exemple sont stockées dans une base de données. La table "MenuItem" contient les éléments suivants :

Le champ "PARENT_ITEM" contient le "ID_MENU_ITEM" d'un élément de menu d'un ordre supérieur.

Cette simple table permet de stocker des menus sous forme arborescente.

Extraction

Etant donné que ces données sont déjà largement utilisées dans des projets Java, je vais écrire un petit programme de test en réutilisant au maximum les routines existantes. Mon but est d'obtenir un fichier JSON pour que D3.js puisse le représenter .

Maven

Dans les projets de production, Maven a été massivement utilisé pour lier les différents librairies et sous-projets. Je vais donc aussi utiliser Maven pour mon projet de test et ainsi importer facilement les librairies dont j'ai besoin et surtout l'accès aux données via hibernate qui va largement me simplifier la vie.

pom.xml

Ce fichier me permet de lier les librairies dont j'ai besoin :

  • JUnit : Pour les tests unitaires.
  • NavigationEntity : Librairie "maison" contenant les entités hibernate en rapport avec les données qui permettent à l'utilisateur de "naviguer" dans le programme.
  • Guice : Outil d'injection de dépendances.
  • Mysql-Connector : Pour accéder à mes données de test
  • XStream : Outils de sérialisation d'objets
  • Jettison : Librairie qui permet à XStream de sérialiser les données au format JSON
 
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
 
    <groupId>ch.conceptforge.group</groupId>
    <artifactId>json_d3</artifactId>
    <version>1.0-SNAPSHOT</version>
    <packaging>jar</packaging>
 
    <name>json_d3</name>
    <url>http://maven.apache.org</url>
 
    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>
 
    <dependencies>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>3.8.1</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>${project.groupId}</groupId>
            <artifactId>NavigationEntity</artifactId>
            <version>2.0-SNAPSHOT</version>
        </dependency>
        <dependency>
            <groupId>com.google.inject</groupId>
            <artifactId>guice</artifactId>
            <version>3.0</version>
        </dependency>
        <dependency>
            <groupId>com.google.inject.extensions</groupId>
            <artifactId>guice-persist</artifactId>
            <version>3.0</version>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>5.1.14</version>
        </dependency> 
        <dependency>
            <groupId>com.thoughtworks.xstream</groupId>
            <artifactId>xstream</artifactId>
            <version>1.4</version>
        </dependency>
        <dependency>
            <groupId>org.codehaus.jettison</groupId>
            <artifactId>jettison</artifactId>
            <version>1.3.2</version>
        </dependency>        
    </dependencies>
</project>

Hibernate

Je ne vais pas entrer trop dans les détails, car c'est trop spécifique à la librairie maison "NavigationEntity". Celle-ci utilise Hibernate et Guice pour se connecter à la base de données et extraire les entités sous forme d'objets. Ceci nécessite de créer un fichier "persistence.xml" et de le configurer pour pouvoir utiliser la classe "MenuItem" qui encapsule les données présentées précédemment dans des objets.

Guice

De même que pour hibernate, cette partie est propre à la librairie maison "NavigationEntity", donc je ne vais pas entrer trop dans les détails. Je vais simplement préciser qu'il est nécessaire de construire une classe qui implémente l'interface "Module" de guice. Pour plus d'informations à ce propos, je vous invite à consulter les documents suivants :

Programme principal

package ch.conceptforge.paquet;
 
import ch.conceptforge.paquet.MenuItemTree;
import com.google.inject.Guice;
import com.google.inject.Injector;
import com.thoughtworks.xstream.XStream;
import com.thoughtworks.xstream.io.HierarchicalStreamWriter;
import com.thoughtworks.xstream.io.json.JsonHierarchicalStreamDriver;
import com.thoughtworks.xstream.io.json.JsonWriter;
import java.io.Writer;
import javax.persistence.EntityManager;
 
public class App
{
 
    private static EntityManager createEntityManager() {
        Injector injector = Guice.createInjector(new GuiceModule());
        return injector.getInstance(EntityManager.class);
    }
 
    private static XStream createXStream() {
        XStream xstream = new XStream(new JsonHierarchicalStreamDriver() {
 
            @Override
            public HierarchicalStreamWriter createWriter(Writer writer) {
                return new JsonWriter(writer, JsonWriter.DROP_ROOT_MODE);
            }
        });
 
        return xstream;
    }
 
    public static void main( String[] args )
    {
        // Liaison de l'entityManager à la classe MenuItem
        MenuItemTree.setEm(createEntityManager());
 
        // Construction de l'arborescence
        MenuItemTree mit = new MenuItemTree();
 
        // Exportation de l'arbre en JSON sur la console
        System.out.println(createXStream().toXML(mit));
    }
}

La partie la plus importante de ce petit programme de test est la construction de l'instance de "MenuItemTree". Le code qui précède cette instanciation permet d’accéder aux entités hibernate. Le passage de l'entityManager par une méthode statique n'est pas élégant, mais, s'agissant d'un petit programme de test, je ne vais pas y remédier. Le code qui suit l'instanciation de "MenuItemTree" permet de sérialiser cet objet. Il suffira ensuite de mettre cette chaine de caractères dans un fichier texte et de la lier à D3.js.

MenuItemTree

Cette classe va s'occuper d'interroger la base de données et de construire l’arborescence d'objets. Dans un esprit de réutilisabilité, j'ai découpé cette classe en deux niveaux d'abstraction.

Tree

Cette classe contient la structure de base d'un arbre qui pourra être utilisé par D3.js

package ch.conceptforge.paquet.tree;
 
import java.util.ArrayList;
import java.util.List;
 
public class Tree {
 
    private String name;
    private List children = null;
 
    public Tree() {
 
    }        
 
    public Tree(String name) {
        this.name = name;
    }
 
    protected void addChild(Tree tree) {
        if (children == null) {
            children = new ArrayList();
        }
        children.add(tree);
    }
 
    public List getChildren() {
        return children;
    }
}

MenuItemTree

Cette classe contient l'implémentation concrète de la structure arborescente pour mes MenuItems.

package ch.ultrasoft.cortex.tree;
 
import com.ultrasoft.server.core.entity.navigation.MenuItem;
import java.util.List;
import javax.persistence.EntityManager;
import javax.persistence.Query;
 
/**
 *
 * @author silenus
 */
public class MenuItemTree extends Tree {
 
    private static EntityManager em;        
 
    public static EntityManager getEm() {
        return em;
    }
 
    public static void setEm(EntityManager em) {
        MenuItemTree.em = em;
    }           
 
    public MenuItemTree() {
        super();
        parseItems(em.createQuery("from MenuItem where parentItem = null"));
    }
 
    public MenuItemTree(int id, String name) {
        super(name);
        parseItems(em.createQuery("from MenuItem where parentItem = " + id));
    }
 
    private void parseItems(Query q) {
        for (MenuItem mi : (List) q.getResultList()) {
            addChild(new MenuItemTree(mi.getId(),mi.getText()));
        }
    }
}

JSON

Comme expliqué précédemment, ce petit programme affiche sur la console les données au format json. Voici un extrait :

{
  "children": [
    {
      "name": "Stammdaten",
      "children": [
        {
          "name": "Adressetyp"
        },
        {
          "name": "Bauart"
        },
        {
          "name": "Betriebstyp"
...
        }
      ]
    }
  ]
}

La totalité des données est stockée dans le fichier test2.json

D3.js

A partir des nombreux exemples d'utilisation de cette librairie, voici quelques propositions d'affichage pour mon arborescence.

Le code html n'est pas particulièrement soigné, je souhaitais dans cet article me concentrer sur le rendu visuel des différentes variantes. Au final, une seule sera choisie pour être implémentée dans une application. C'est à ce moment-là que le code sera repris en profondeur.

Exemple de mise en oeuvre de SCXML-JS

Introduction

Voici quelques articles que je parle des machines à états SCXML. Il faut dire que j'ai passé pas mal de temps à les étudier. Le cas que j'ai présenté jusqu'à maintenant (Exemple de mise en oeuvre de SCXML - partie 1) simule une application Java/Swing. Il m'arrive souvent de travailler sur des applications réseau. Celles-ci sont basées sur les technologies du Web. J'utilise beaucoup GWT, Google Web Toolkit, (dont je parlerai prochainement) qui produit une application Web HTML/CSS/JavaScript.

Quand j'ai étudié SCXML, j'avais simulé une application Web. Il s'agissait d'une calculatrice quatre opérations. C'est sur la base de cette étude que j'ai écrit les articles précédents.

Calculatrice

Comme expliqué précédemment, ce tutoriel présente une calculatrice. Cette application d'étude a été écrite il y a quelque temps et j'ai repris cette base pour écrire les articles précédents.

La calculatrice est séparée en deux parties

  • HTML : IHM de l'application Web
  • JavaScript : Machine à états
Voici l'application elle-même : calculator.html

Machine à états

La machine à états de l'exemple de mise en oeuvre de SCXML - partie 1 a été construite à partir de celle-ci. Elle est donc à 90% identique. Les différences sont au niveau du langage. En Java, les listeners s'occupent de surveiller l'activité de la machine et de réagir en fonction des changements d'état. Cette machine-ci utilise JavaScript, il est donc possible d'inclure le code dans la balise <script> de SCXML.

Étant donné le peu de différence entre cette machine et celle du premier tutoriel, je ne vais pas revenir sur le fonctionnement général. Je vais juste reprendre les états et les transitions qui, selon moi, méritent une petite explication et mettre en évidence les morceaux de JavaScript.

Clear

  • La balise <log> produit une trace dans la console du browser
  • <assign> donne une valeur (new String('')) à la variable "result", donc l'initialise avec une chaine vide.
  • <script> contient le code JavaScript "$('#display').val(0)". C'est un appel à la librairie jquery qui aura pour effet de mettre la valeur "0" dans le input ayant l'id "display".

Reset

Remise à zéro des variables op1, op2 et oper.

operand1 / onDigit

Comme dans la première partie de cette série de tutoriels, la transition "onDigit" est déclenchée dès qu'un chiffre est saisi. Le code présenté ci-dessous permet d'ajouter (concaténer) ce chiffre à la variable "op1".

  • la balise <log> affiche dans la console l'objet "_event".

Par exemple après un click sur le bouton "1", voici ce que nous pouvons voir dans la console de Firebug :

Object { name="onDigit", data="1"}

operand1 / received_op1

Mise à jour de l'affichage

onOperation

Mise à jour de la variable "oper".

Compute

Cet état contient le coeur de la calculatrice, le calcul lui-même.

  • La balise <assign> contient le calcul proprement dit. l'expression "eval(op1 + oper + op2)" demande à JavaScript d'interpréter la concaténation de ces trois variables. Avec l'exemple habituel "eval('123' + '+' + '12')" => 135

IHM

L'interface utilisateur de cette application web est réalisée en HTML. Elle est fortement inspirée d'un article de daniweb : A four function calculator (HTML). Par rapport à daniweb, j'ai modifié les points suivants :

  • utilisation de jquery pour simplifier le code javascript
  • intégration de "calculator.js"
  • modification de tous les javascripts pour utiliser la machine à états

Liaison avec la machine à états

Il s'agit maintenant de lier l'interface en HTML avec la machine à états en XML. Pour ce faire, il faut utiliser SCXML-JS.

Télécharger SCXML-JS

Un dépôt SubVersion est mis à disposition et nous devons aller chercher les sources :

svn checkout http://svn.apache.org/repos/asf/commons/sandbox/gsoc/2010/scxml-js/trunk commons-scxml-js

"Compiler" la machine à états

./run.sh --backend state --beautify --ie /path/to/calculator.scxml &gt; /path/to/calculator.js

lier la machine à états à l'IHM

D'abord, il faut faire connaitre à l'IHM le scipt généré ci-dessus

   ...
   <!-- Machine à états (calculator.scxml compilée avec scxml-js) --><script type="text/javascript" src="calculator.js"></script>

Ensuite, il faut créer l'instance JavaScript de la machine à états

...
<!-- Initialisation de la machine à états finis --><script type="text/javascript">// <![CDATA[
    sec = new StatechartExecutionContext();
    sec.initialize();
// ]]></script>

Réaliser une transition

Il est maintenant possible d'implémenter l'événement "onClick" d'un bouton de l'IHM pour qu'il demande à la machine à états de réaliser une transition.

<input onclick="sec.onClear()" type="button" name="clear" value=" C " />

Conclusion

L'application est disponible ici : calculator.html
La machine à états est ici : calculator.scxml

Cet article montre comment utiliser SCXML dans un contexte différent d'une application Java/Swing. En rédigeant ces quelques lignes, je me suis aperçu que, malheureusement, le projet SCXML-JS ne sera pas mis à jour. C'est dommage, j'ai fait de très bonnes expériences avec ce projet. Au moins, le projet SCXML-JS est utilisable et le code source est publié, il nous reste à retrousser nos manches et à le faire évoluer nous-mêmes.