org.vaadin.scenejs.SceneJSCanvas.java Source code

Java tutorial

Introduction

Here is the source code for org.vaadin.scenejs.SceneJSCanvas.java

Source

/**
 * SceneJS Vaadin Addon
 * Copyright (C) 2012 Federico Russo <chiccorusso@gmail.com>
 * Licensed under the MIT license.
 *
 * Includes SceneJS WebGL Scene Graph Library for JavaScript http://scenejs.org/
 * Dual licensed under the MIT or GPL Version 2 licenses. http://scenejs.org/license
 * Copyright 2010, Lindsay Kay
 */
package org.vaadin.scenejs;

import com.vaadin.terminal.PaintException;
import com.vaadin.terminal.PaintTarget;
import com.vaadin.ui.AbstractComponent;
import com.vaadin.ui.ClientWidget;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.apache.commons.collections.MultiHashMap;
import org.apache.commons.collections.MultiMap;
import org.vaadin.scenejs.client.ui.VSceneJSCanvas;

/**
 * The SceneJS canvas is a WebGL enabled component. It should work on every modern web browser
 * that supports WebGL. The programming paradigm of this Canvas is as similar as possible to
 * SceneJS', and most of its funcionalities are provided by its hierarchical node structure.
 *
 * In SceneJS, all the scene is composed by a hierarchical structure of nodes; each node has a type,
 * some properties and an array of children. All SceneJS node types will be wrapped in Java objects
 * that the addon will synchronize with their Javascript representations in the client.
 *
 * So, you can create a tree of nodes and put it in the Canvas. Scope of the Addon is to create such
 * trees server side in Java in the same way you'd create it in SceneJS on the client side in
 * Javascript.
 *
 * Server side component for the VSceneJSCanvas widget.
 *
 * @author Federico Russo
 */
@ClientWidget(VSceneJSCanvas.class)
public class SceneJSCanvas extends AbstractComponent {

    private static final long serialVersionUID = 1L;
    private boolean stopping = false;

    //<editor-fold defaultstate="collapsed" desc="Constructors">
    /**
     * Default constructor. The Canvas is always immediate.
     */
    public SceneJSCanvas() {
        setImmediate(true);
    }
    //</editor-fold>
    //<editor-fold defaultstate="collapsed" desc="Nodes">

    /**
     * Adds a node to the current scene. The scene can have many child nodes.
     * @param node Node to be added to the scene.
     * @return The newly added node.
     */
    public SceneJSNode addNode(SceneJSNode node) {
        node.attach(this);
        addNewNode(node);
        requestRepaint();
        return node;
    }

    /**
     * An alias for {@code addNode()}.
     */
    public SceneJSNode a(SceneJSNode node) {
        return addNode(node);
    }

    /**
     * Adds a node to the current scene. The scene can have many child nodes.
     * @param node Node to be added to the scene.
     * @return The canvas itself.
     */
    public SceneJSCanvas addNodeP(SceneJSNode node) {
        addNode(node);
        return this;
    }

    /**
     * An alias for {@code addNodeP()}.
     */
    public SceneJSCanvas p(SceneJSNode node) {
        return addNodeP(node);
    }

    /**
     * This list contains the newly added nodes that have yet no representation in the client side.
     * This list is flushed by {@code paintContent()}.
     * When you add to the scene new nodes that already have children, the children are not added
     * to this list.
     * Strictly speaking, this is a new subtrees list.
     */
    private final List<SceneJSNode> newNodes = new LinkedList<>();

    /**
     * Adds a node to the list of the new nodes.
     * @param newNode New node that is being added.
     */
    protected void addNewNode(SceneJSNode newNode) {
        newNodes.add(newNode);
    }
    //</editor-fold>
    //<editor-fold defaultstate="collapsed" desc="Heigth and Width">

    @Override
    public void setHeight(String height) {
        super.setHeight(height);
        setDirty("height");
    }

    @Override
    @Deprecated
    public void setHeight(float height) {
        super.setHeight(height);
        setDirty("height");
    }

    @Override
    public void setHeight(float height, int unit) {
        super.setHeight(height, unit);
        setDirty("height");
    }

    @Override
    public void setWidth(String width) {
        super.setWidth(width);
        setDirty("width");
    }

    @Override
    @Deprecated
    public void setWidth(float width) {
        super.setWidth(width);
        setDirty("width");
    }

    @Override
    public void setWidth(float width, int unit) {
        super.setWidth(width, unit);
        setDirty("width");
    }

    //</editor-fold>
    //<editor-fold defaultstate="collapsed" desc="Dirty fields and partialPaint()">
    private final DirtySupport dirty = new DirtySupport(null);

    private void setDirty(String fieldName) {
        dirty.set(fieldName);
    }

    private void setNotDirty(String fieldName) {
        dirty.remove(fieldName);
    }

    private boolean isDirty(String fieldName) {
        return dirty.is(fieldName);
    }
    //</editor-fold>
    //<editor-fold defaultstate="collapsed" desc="Movement type">

    /**
     * Kind of possible movements. There is currently no possibility to move thew viewpoint.
     */
    public static enum MovementType {

        /** Rotate the viewpoint around itself. */
        AROUND,
        /** Rotate the model around itself. */
        MODEL,
        /** Rotate the model
         * around itself, but constrain the rotation axis. */
        CONSTRAINED
    }

    private MovementType movementType = null;

    /**
     * Sets the movement type.
     * @param movementType
     */
    public void setMovementType(MovementType movementType) {
        this.movementType = movementType;
        setDirty("movement");
    }
    //</editor-fold>
    //<editor-fold defaultstate="collapsed" desc="Vaadin methods">

    /**
     * Paints the modified properties to the client.
     * @param target
     * @throws PaintException
     */
    @Override
    public void paintContent(PaintTarget target) throws PaintException {
        super.paintContent(target);

        if (stopping) {
            // If we are stopping we send the stop variable and nothing else.
            stopping = false;
            target.addAttribute("stop", true);
            return;
        }

        // Height and width variables
        if (isDirty("height")) {
            target.addAttribute("height", "" + getHeight());
            setNotDirty("height");
        }
        if (isDirty("width")) {
            target.addAttribute("width", "" + getWidth());
            setNotDirty("width");
        }

        // Change in movement strategy.
        if (null != movementType && isDirty("movement")) {
            target.addAttribute("movement", movementType.name());
            setNotDirty("movement");
        }

        // New nodes to add.
        // The "newNodes" attribute is built as a concatenation of parent node id and node json.
        if (!newNodes.isEmpty()) {
            StringBuilder newNodesJson = new StringBuilder();
            for (SceneJSNode newNode : newNodes) {
                // Each of these nodes was expressely added to the new node list.
                if (!currentNodes.containsKey(newNode.id)) {
                    SceneJSNode parent = newNode.getParent();
                    String parentId = null == parent ? "" : parent.getId();
                    newNodesJson.append(parentId).append(";").append(newNode.getJson()).append(";");
                    addCurrentNode(newNode);
                    newNode.dirty.clear();
                }
            }
            target.addAttribute("newNodes", newNodesJson.toString());
            newNodes.clear();
        }

        // New PickListeners to attach. 
        if (!newPickListeners.isEmpty()) {
            StringBuilder pickListenersJson = new StringBuilder();
            for (Map.Entry<String, Set<PickListener>> entry : newPickListeners.entrySet()) {
                String nodeName = entry.getKey();
                Set<PickListener> listeners = entry.getValue();
                for (PickListener listener : listeners) {
                    currentPickListeners.put(nodeName, listener);
                    pickListenersJson.append(nodeName).append(";");
                }
            }
            target.addAttribute("newPickListeners", pickListenersJson.toString());
            newPickListeners.clear();
        }

        // Nodes to be updated.
        if (!dirtyNodes.isEmpty()) {
            StringBuilder dirtyNodesJson = new StringBuilder();
            for (SceneJSNode dirtyNode : dirtyNodes) {
                dirtyNodesJson.append(dirtyNode.getId()).append(";").append(dirtyNode.getShallowJson()).append(";");
                dirtyNode.dirty.clear();
            }
            target.addAttribute("dirtyNodes", dirtyNodesJson.toString());
            dirtyNodes.clear();
        }
    }

    /**
     * Receives variable changes from the client side.
     * @param source
     * @param variables
     */
    @Override
    public void changeVariables(Object source, Map<String, Object> variables) {
        super.changeVariables(source, variables);
        for (Map.Entry<String, Object> entry : variables.entrySet()) {
            String variableName = entry.getKey();
            Object variableValue = entry.getValue();

            if (null != variableName) {
                switch (variableName) {
                case "pick":
                    // We received a pick. The client is sending us the name af the Name node that was
                    // picked. The name property of the Name node is equal to its ID.
                    String nodeName = (String) variableValue;
                    Collection<PickListener> listeners = (Collection<PickListener>) currentPickListeners
                            .get(nodeName);
                    if (null != listeners) {
                        Name pickedNode = (Name) currentNodes.get(nodeName);
                        for (PickListener listener : listeners) {
                            listener.onPick(pickedNode);
                        }
                    }
                    break;
                case "nopick":
                    fireNothingPickedEvent();
                    break;
                }
            }
        }
    }

    //</editor-fold>
    private final Map<String, SceneJSNode> currentNodes = new HashMap<>(16);
    /**
     * Map of the pickListeners to add to the client mode. These PickListeners are extracted from
     * the new nodes when they are sent to the client and are added to the currentPickListeners map.
     * This map is keyed on the Name nodes name (that is also the node ID).
     */
    private final Map<String, Set<PickListener>> newPickListeners = new HashMap<>(16);
    private final MultiMap currentPickListeners = new MultiHashMap();

    /**
     * Recursive method that adds each node of a subtree to the currentNodes map.
     * @param node New node to add to the map.
     */
    private void addCurrentNode(SceneJSNode node) {
        currentNodes.put(node.getId(), node);

        // If present, we add the pickListeners
        if (node instanceof Name) {
            Name name = (Name) node;
            Set<PickListener> pickListeners = name.getPickListeners();
            if (!pickListeners.isEmpty()) {
                newPickListeners.put(name.getId(), pickListeners);
            }
        }

        for (SceneJSNode child : node.getChildren()) {
            addCurrentNode(child);
        }
    }

    public void stop() {
        stopping = true;
        requestRepaint();
    }

    private final Collection<SceneJSNode> dirtyNodes = new HashSet<>(16);

    /**
     * Adds the specified node to the set of dirty nodes. This method should be invoked by each node
     * that knows it's becoming dirty.
     * @param node
     */
    protected void addDirtyNode(SceneJSNode node) {
        dirtyNodes.add(node);
    }
    //<editor-fold defaultstate="collapsed" desc="Pick Listeners">

    /**
     * Interface of the listener of the event of the pick on a Name node.
     */
    public static interface PickListener {

        void onPick(Name sender);
    }

    /**
     * Interface of the listener of the event of the pick outside anything.
     */
    public static interface NothingPickedListener {

        void nothingPicked();
    }

    private final Collection<NothingPickedListener> nothingPickedListeners = new HashSet<>(16);

    public void addNothingPickedListener(NothingPickedListener listener) {
        nothingPickedListeners.add(listener);
    }

    public void removeNothingPickedListener(NothingPickedListener listener) {
        nothingPickedListeners.remove(listener);
    }

    protected void fireNothingPickedEvent() {
        for (NothingPickedListener listener : nothingPickedListeners) {
            listener.nothingPicked();
        }
    }
    //</editor-fold>
}