/*
* This is part of Geomajas, a GIS framework, http://www.geomajas.org/.
*
* Copyright 2008-2015 Geosparc nv, http://www.geosparc.com/, Belgium.
*
* The program is available in open source according to the GNU Affero
* General Public License. All contributions in this program are covered
* by the Geomajas Contributors License Agreement. For full licensing
* details, see LICENSE.txt in the project root.
*/
package org.geomajas.gwt.client.widget;
import java.util.ArrayList;
import java.util.List;
import com.smartgwt.client.util.SC;
import org.geomajas.configuration.client.ClientLayerInfo;
import org.geomajas.configuration.client.ClientLayerTreeInfo;
import org.geomajas.configuration.client.ClientLayerTreeNodeInfo;
import org.geomajas.configuration.client.ClientToolInfo;
import org.geomajas.annotation.Api;
import org.geomajas.gwt.client.action.ToolbarBaseAction;
import org.geomajas.gwt.client.action.ToolbarCanvas;
import org.geomajas.gwt.client.action.ToolbarWidget;
import org.geomajas.gwt.client.action.layertree.LayerTreeAction;
import org.geomajas.gwt.client.action.layertree.LayerTreeModalAction;
import org.geomajas.gwt.client.action.layertree.LayerTreeRegistry;
import org.geomajas.gwt.client.i18n.I18nProvider;
import org.geomajas.gwt.client.map.MapModel;
import org.geomajas.gwt.client.map.event.LayerChangedHandler;
import org.geomajas.gwt.client.map.event.LayerDeselectedEvent;
import org.geomajas.gwt.client.map.event.LayerLabeledEvent;
import org.geomajas.gwt.client.map.event.LayerSelectedEvent;
import org.geomajas.gwt.client.map.event.LayerSelectionHandler;
import org.geomajas.gwt.client.map.event.LayerShownEvent;
import org.geomajas.gwt.client.map.event.MapModelChangedEvent;
import org.geomajas.gwt.client.map.event.MapModelChangedHandler;
import org.geomajas.gwt.client.map.layer.Layer;
import com.google.gwt.event.shared.HandlerRegistration;
import com.google.gwt.user.client.Timer;
import com.smartgwt.client.types.Alignment;
import com.smartgwt.client.types.SelectionType;
import com.smartgwt.client.widgets.Canvas;
import com.smartgwt.client.widgets.HTMLFlow;
import com.smartgwt.client.widgets.IButton;
import com.smartgwt.client.widgets.events.ClickEvent;
import com.smartgwt.client.widgets.events.ClickHandler;
import com.smartgwt.client.widgets.grid.ListGridRecord;
import com.smartgwt.client.widgets.layout.LayoutSpacer;
import com.smartgwt.client.widgets.layout.VLayout;
import com.smartgwt.client.widgets.toolbar.ToolStrip;
import com.smartgwt.client.widgets.tree.Tree;
import com.smartgwt.client.widgets.tree.TreeGrid;
import com.smartgwt.client.widgets.tree.TreeNode;
import com.smartgwt.client.widgets.tree.events.FolderClickEvent;
import com.smartgwt.client.widgets.tree.events.FolderClickHandler;
import com.smartgwt.client.widgets.tree.events.LeafClickEvent;
import com.smartgwt.client.widgets.tree.events.LeafClickHandler;
import org.geomajas.gwt.client.util.Log;
import org.geomajas.gwt.client.util.WidgetLayout;
/**
* The LayerTree shows a tree resembling the available layers for the map Several actions can be executed on the layers
* (make them invisible, ...).
*
* @author Frank Wynants
* @author Pieter De Graef
* @since 1.6.0
*/
@Api
public class LayerTree extends Canvas implements LeafClickHandler, FolderClickHandler, LayerSelectionHandler {
private final HTMLFlow htmlSelectedLayer = new HTMLFlow(I18nProvider.getLayerTree().activeLayer(
I18nProvider.getLayerTree().none()));
private ToolStrip toolStrip;
private LayerTreeTreeNode selectedLayerTreeNode;
private final TreeGrid treeGrid = new TreeGrid();
private RefreshableTree tree;
private MapModel mapModel;
private boolean initialized;
private final List<HandlerRegistration> registrations = new ArrayList<HandlerRegistration>();
// -------------------------------------------------------------------------
// Constructor:
// -------------------------------------------------------------------------
/**
* Initialize the LayerTree, using a MapWidget as base reference. It will display the map's layers, as configured in
* the XML configuration, and select/deselect the layer as the user clicks on them in the tree.
*
* @param mapWidget
* map widget this layer tree is connected to
* @since 1.6.0
*/
@Api
public LayerTree(final MapWidget mapWidget) {
super();
setHeight100();
this.mapModel = mapWidget.getMapModel();
htmlSelectedLayer.setWidth100();
// Wait for the MapModel to be loaded
mapModel.addMapModelChangedHandler(new MapModelChangedHandler() {
public void onMapModelChanged(MapModelChangedEvent event) {
if (!initialized) {
buildTree(mapModel);
toolStrip = buildToolstrip(mapWidget);
// display the toolbar and the tree
VLayout vLayout = new VLayout();
vLayout.setSize("100%", "100%");
vLayout.addMember(toolStrip);
htmlSelectedLayer.setBackgroundColor(WidgetLayout.layerTreeBackground);
htmlSelectedLayer.setAlign(Alignment.CENTER);
vLayout.addMember(htmlSelectedLayer);
vLayout.addMember(treeGrid);
LayerTree.this.addChild(vLayout);
}
initialized = true;
treeGrid.markForRedraw();
LayerTree.this.markForRedraw();
}
});
mapModel.addLayerSelectionHandler(this);
}
// -------------------------------------------------------------------------
// LayerSelectionHandler implementation:
// -------------------------------------------------------------------------
/**
* When a layer deselection event comes in, the LayerTree must also deselect the correct node in the tree, update
* the selected layer text, and update all buttons icons.
*
* @since 1.6.0
*/
@Api
public void onDeselectLayer(LayerDeselectedEvent event) {
ListGridRecord selected = treeGrid.getSelectedRecord();
if (selected != null) {
treeGrid.deselectRecord(selected);
}
selectedLayerTreeNode = null;
htmlSelectedLayer.setContents(I18nProvider.getLayerTree().activeLayer(I18nProvider.getLayerTree().none()));
Canvas[] toolStripMembers = toolStrip.getMembers();
updateButtonIconsAndStates(toolStripMembers);
}
/**
* When a layer selection event comes in, the LayerTree must also select the correct node in the tree, update the
* selected layer text, and update all buttons icons.
*
* @since 1.6.0
*/
@Api
public void onSelectLayer(LayerSelectedEvent event) {
for (TreeNode node : tree.getAllNodes()) {
if (node.getName().equals(event.getLayer().getLabel())) {
selectedLayerTreeNode = (LayerTreeTreeNode) node;
treeGrid.selectRecord(selectedLayerTreeNode);
htmlSelectedLayer.setContents(I18nProvider.getLayerTree().activeLayer(
selectedLayerTreeNode.getLayer().getLabel()));
Canvas[] toolStripMembers = toolStrip.getMembers();
updateButtonIconsAndStates(toolStripMembers);
}
}
}
// -------------------------------------------------------------------------
// LeafClickHandler, FolderClickHandler
// -------------------------------------------------------------------------
/**
* When the user clicks on a folder nothing gets selected.
*/
public void onFolderClick(FolderClickEvent event) {
mapModel.selectLayer(null);
}
/**
* When the user clicks on a leaf the header text of the tree table is changed to the selected leaf and the toolbar
* buttons are updated to represent the correct state of the buttons.
*/
public void onLeafClick(LeafClickEvent event) {
LayerTreeTreeNode layerTreeNode = (LayerTreeTreeNode) event.getLeaf();
if (null != selectedLayerTreeNode
&& layerTreeNode.getLayer().getId().equals(selectedLayerTreeNode.getLayer().getId())) {
mapModel.selectLayer(null);
} else {
mapModel.selectLayer(layerTreeNode.getLayer());
}
}
// -------------------------------------------------------------------------
// Getters:
// -------------------------------------------------------------------------
/**
* Get the currently selected tree node.
*
* @return selected node
*/
public LayerTreeTreeNode getSelectedLayerTreeNode() {
return selectedLayerTreeNode;
}
// -------------------------------------------------------------------------
// Private methods:
// -------------------------------------------------------------------------
/**
* Builds the toolbar
*
* @param mapWidget
* The mapWidget containing the layerTree
* @return {@link com.smartgwt.client.widgets.toolbar.ToolStrip} which was built
*/
private ToolStrip buildToolstrip(MapWidget mapWidget) {
toolStrip = new ToolStrip();
toolStrip.setWidth100();
toolStrip.setPadding(3);
ClientLayerTreeInfo layerTreeInfo = mapModel.getMapInfo().getLayerTree();
if (layerTreeInfo != null) {
for (ClientToolInfo tool : layerTreeInfo.getTools()) {
String id = tool.getToolId();
Canvas button = null;
ToolbarBaseAction action = LayerTreeRegistry.getToolbarAction(id, mapWidget);
if (action instanceof ToolbarWidget) {
toolStrip.addMember(((ToolbarWidget) action).getWidget());
} else if (action instanceof ToolbarCanvas) {
button = ((ToolbarCanvas) action).getCanvas();
} else if (action instanceof LayerTreeAction) {
button = new LayerTreeButton(this, (LayerTreeAction) action);
} else if (action instanceof LayerTreeModalAction) {
button = new LayerTreeModalButton(this, (LayerTreeModalAction) action);
} else {
String msg = "LayerTree tool with id " + id + " unknown.";
Log.logError(msg); // console log
SC.warn(msg); // in your face
}
if (button != null) {
toolStrip.addMember(button);
LayoutSpacer spacer = new LayoutSpacer();
spacer.setWidth(WidgetLayout.layerTreePadding);
toolStrip.addMember(spacer);
}
}
}
final Canvas[] toolStripMembers = toolStrip.getMembers();
// delaying this fixes an image 'undefined' error
Timer t = new Timer() {
public void run() {
updateButtonIconsAndStates(toolStripMembers);
}
};
t.schedule(10);
return toolStrip;
}
/**
* Builds up the tree showing the layers
*
* @param mapModel
* The mapModel containing the layerTree
*/
private void buildTree(MapModel mapModel) {
treeGrid.setWidth100();
treeGrid.setHeight100();
treeGrid.setShowHeader(false);
tree = new RefreshableTree();
final TreeNode nodeRoot = new TreeNode("ROOT");
tree.setRoot(nodeRoot); // invisible ROOT node (ROOT node is required)
ClientLayerTreeInfo layerTreeInfo = mapModel.getMapInfo().getLayerTree();
if (layerTreeInfo != null) {
ClientLayerTreeNodeInfo treeNode = layerTreeInfo.getTreeNode();
processNode(treeNode, nodeRoot, tree, mapModel, false);
}
treeGrid.setData(tree);
treeGrid.addLeafClickHandler(this);
treeGrid.addFolderClickHandler(this);
// -- add event listeners to layers
for (Layer<?> layer : mapModel.getLayers()) {
registrations.add(layer.addLayerChangedHandler(new LayerChangedHandler() {
public void onLabelChange(LayerLabeledEvent event) {
}
public void onVisibleChange(LayerShownEvent event) {
for (TreeNode node : tree.getAllNodes()) {
if (node.getName().equals(event.getLayer().getLabel())) {
if (node instanceof LayerTreeTreeNode) {
((LayerTreeTreeNode) node).updateIcon();
}
}
}
}
}));
}
}
/** Remove all handlers on unload. */
protected void onUnload() {
if (registrations != null) {
for (HandlerRegistration registration : registrations) {
registration.removeHandler();
}
}
super.onUnload();
}
/**
* Processes a treeNode (add it to the TreeGrid)
*
* @param treeNode
* The treeNode to process
* @param nodeRoot
* The root node to which the treeNode has te be added
* @param tree
* The tree to which the node has to be added
* @param mapModel
* map model
* @param refresh
* True if the tree is refreshed (causing it to keep its expanded state)
*/
private void processNode(final ClientLayerTreeNodeInfo treeNode, final TreeNode nodeRoot, final Tree tree,
final MapModel mapModel, final boolean refresh) {
if (null != treeNode) {
String treeNodeLabel = treeNode.getLabel();
final TreeNode node = new TreeNode(treeNodeLabel);
tree.add(node, nodeRoot);
// (final leafs)
for (ClientLayerInfo info : treeNode.getLayers()) {
Layer<?> layer = mapModel.getLayer(info.getId());
tree.add(new LayerTreeTreeNode(this.tree, layer), node);
}
// treeNodes
List<ClientLayerTreeNodeInfo> children = treeNode.getTreeNodes();
for (ClientLayerTreeNodeInfo newNode : children) {
processNode(newNode, node, tree, mapModel, refresh);
}
// expand tree nodes
// when not refreshing expand them like configured
// when refreshing expand them as before the refresh
boolean isTreeNodeExpanded = treeNode.isExpanded();
if (!refresh) {
if (isTreeNodeExpanded) {
tree.openFolder(node);
}
} else {
// TODO close previously opened tree nodes, close others
}
}
}
/**
* Updates the icons and the state of the buttons in the toolbar based upon the currently selected layer
*
* @param toolStripMembers
* data for the toolbar
*/
private void updateButtonIconsAndStates(Canvas[] toolStripMembers) {
for (Canvas toolStripMember : toolStripMembers) {
if (toolStripMember instanceof LayerTreeModalButton) {
((LayerTreeModalButton) toolStripMember).update();
} else if (toolStripMember instanceof LayerTreeButton) {
((LayerTreeButton) toolStripMember).update();
}
}
}
/**
* General definition of an action button for the layer tree.
*
* @author Frank Wynants
* @author Pieter De Graef
*/
private static class LayerTreeButton extends IButton {
private LayerTree tree;
private LayerTreeAction action;
public LayerTreeButton(final LayerTree tree, final LayerTreeAction action) {
this.tree = tree;
this.action = action;
setWidth(WidgetLayout.layerTreeButtonSize);
setHeight(WidgetLayout.layerTreeButtonSize);
setIconSize(WidgetLayout.layerTreeButtonSize - WidgetLayout.layerTreeStripHeight);
setIcon(action.getIcon());
setTooltip(action.getTooltip());
setActionType(SelectionType.BUTTON);
setShowDisabledIcon(false);
addClickHandler(new ClickHandler() {
public void onClick(ClickEvent event) {
try {
action.onClick(tree.getSelectedLayerTreeNode().getLayer());
update();
} catch (Throwable t) {
Log.logError("LayerTreeButton onClick error", t);
}
}
});
}
public void update() {
LayerTreeTreeNode selected = tree.getSelectedLayerTreeNode();
if (selected != null && action.isEnabled(selected.getLayer())) {
setDisabled(false);
setIcon(action.getIcon());
setTooltip(action.getTooltip());
} else {
setDisabled(true);
setIcon(action.getDisabledIcon());
setTooltip("");
}
}
}
/**
* General definition of a modal button for the layer tree.
*
* @author Frank Wynants
* @author Pieter De Graef
*/
private static class LayerTreeModalButton extends IButton {
private LayerTree tree;
private LayerTreeModalAction modalAction;
/**
* Constructor
*
* @param tree
* The currently selected layer
* @param modalAction
* The action coupled to this button
*/
public LayerTreeModalButton(final LayerTree tree, final LayerTreeModalAction modalAction) {
this.tree = tree;
this.modalAction = modalAction;
setWidth(WidgetLayout.layerTreeButtonSize);
setHeight(WidgetLayout.layerTreeButtonSize);
setIconSize(WidgetLayout.layerTreeButtonSize - WidgetLayout.layerTreeStripHeight);
setIcon(modalAction.getDeselectedIcon());
setActionType(SelectionType.CHECKBOX);
setTooltip(modalAction.getDeselectedTooltip());
setShowDisabledIcon(false);
this.addClickHandler(new ClickHandler() {
public void onClick(ClickEvent event) {
LayerTreeTreeNode selectedLayerNode = tree.getSelectedLayerTreeNode();
if (LayerTreeModalButton.this.isSelected()) {
modalAction.onSelect(selectedLayerNode.getLayer());
} else {
modalAction.onDeselect(selectedLayerNode.getLayer());
}
selectedLayerNode.updateIcon();
update();
}
});
}
public void update() {
LayerTreeTreeNode selected = tree.getSelectedLayerTreeNode();
if (selected != null && modalAction.isEnabled(selected.getLayer())) {
setDisabled(false);
} else {
setSelected(false);
setDisabled(true);
setIcon(modalAction.getDisabledIcon());
setTooltip("");
}
if (selected != null && modalAction.isSelected(selected.getLayer())) {
setIcon(modalAction.getSelectedIcon());
setTooltip(modalAction.getSelectedTooltip());
select();
} else if (selected != null) {
setIcon(modalAction.getDeselectedIcon());
setTooltip(modalAction.getDeselectedTooltip());
deselect();
}
}
}
/**
* A SmartGWT Tree with one extra method 'refresh'. This is needed to update icons on the fly in a tree
*
* @author Frank Wynants
*/
private static class RefreshableTree extends Tree {
/**
* Refreshes the icons in the tree, this is done by closing and reopening all nodes A dirty solution but no
* other option was found at the time
*/
public void refreshIcons() {
TreeNode[] openNodes = this.getOpenList(this.getRoot());
this.closeAll();
for (TreeNode openNode : openNodes) {
this.openFolder(openNode);
}
}
}
/**
* A node inside the LayerTree.
*
* @author Frank Wynants
* @author Pieter De Graef
*/
private static class LayerTreeTreeNode extends TreeNode {
private RefreshableTree tree;
private Layer<?> layer;
/**
* Constructor creates a TreeNode with layer.getLabel as label.
*
* @param tree
* tree for node
* @param layer
* The layer object
*/
public LayerTreeTreeNode(RefreshableTree tree, Layer<?> layer) {
super(layer.getLabel());
this.layer = layer;
this.tree = tree;
updateIcon();
}
/**
* Causes the node to check its status (visible, showing labels,...) and to update its icon to match its
* status.
*/
public void updateIcon() {
if (getLayer().isShowing()) {
if (getLayer().isLabelsShowing()) {
// show icon labeled and showing
setIcon(WidgetLayout.iconLayerShowLabeled);
} else {
// show showing icon
setIcon(WidgetLayout.iconLayerShow);
}
} else {
// show not showing
setIcon(WidgetLayout.iconLayerHide);
}
tree.refreshIcons();
}
/**
* Get layer.
*
* @return layer
*/
public Layer<?> getLayer() {
return layer;
}
}
}