/*
* 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.Date;
import java.util.List;
import org.geomajas.annotation.Api;
import org.geomajas.configuration.AssociationAttributeInfo;
import org.geomajas.configuration.AttributeInfo;
import org.geomajas.configuration.FeatureInfo;
import org.geomajas.configuration.PrimitiveAttributeInfo;
import org.geomajas.configuration.PrimitiveType;
import org.geomajas.global.GeomajasConstant;
import org.geomajas.gwt.client.map.MapModel;
import org.geomajas.gwt.client.map.event.FeatureDeselectedEvent;
import org.geomajas.gwt.client.map.event.FeatureSelectedEvent;
import org.geomajas.gwt.client.map.event.FeatureSelectionHandler;
import org.geomajas.gwt.client.map.event.FeatureTransactionEvent;
import org.geomajas.gwt.client.map.event.FeatureTransactionHandler;
import org.geomajas.gwt.client.map.feature.Feature;
import org.geomajas.gwt.client.map.feature.LazyLoadCallback;
import org.geomajas.gwt.client.map.layer.VectorLayer;
import org.geomajas.gwt.client.util.StringUtil;
import org.geomajas.layer.feature.Attribute;
import org.geomajas.layer.feature.attribute.AssociationValue;
import org.geomajas.layer.feature.attribute.ManyToOneAttribute;
import org.geomajas.layer.feature.attribute.OneToManyAttribute;
import com.google.gwt.event.shared.HandlerRegistration;
import com.google.gwt.user.client.Timer;
import com.smartgwt.client.data.Record;
import com.smartgwt.client.types.Alignment;
import com.smartgwt.client.types.ListGridFieldType;
import com.smartgwt.client.types.SelectionStyle;
import com.smartgwt.client.widgets.Img;
import com.smartgwt.client.widgets.events.DoubleClickEvent;
import com.smartgwt.client.widgets.events.DoubleClickHandler;
import com.smartgwt.client.widgets.grid.ListGrid;
import com.smartgwt.client.widgets.grid.ListGridField;
import com.smartgwt.client.widgets.grid.ListGridFieldIfFunction;
import com.smartgwt.client.widgets.grid.ListGridRecord;
import com.smartgwt.client.widgets.grid.events.CellOverEvent;
import com.smartgwt.client.widgets.grid.events.CellOverHandler;
import com.smartgwt.client.widgets.grid.events.SelectionChangedHandler;
import com.smartgwt.client.widgets.grid.events.SelectionEvent;
/**
* <p>
* This widget represents a grid of feature attributes, where many different features from a single layer are shown in
* rows on the grid. Each row represents a feature. To start working with this grid, it is necessary to first set a
* layer, so this widget can build it's grid header, and so it knows what type of data to expect.
* </p>
* <p>
* Furthermore this grid has a few options to determine it's looks (on top of the basic SmartGWT options).
* <ul>
* <li><i>selectionEnabled</i>: when true, selected rows in the grid will result in selected features in the MapModel
* and vice versa.</li>
* <li><i>allAttributesDisplayed</i>: show all attributes (true) or only the 'identifying' attributes (false)?</li>
* <li><i>editingEnabled</i>: determines whether or not editing the attributes is allowed. When double clicking a row in
* the table, a {@link FeatureAttributeWindow} will appear, containing the feature of the row upon which was double
* clicked. This setting determines if the window allows editing or not.</li>
* <li><i>idInTable</i>: show the feature's ID in the table. This is false by default, and should not really be
* necessary.</li>
* </ul>
* </p>
*
* @author Pieter De Graef
* @since 1.10.0
*/
@Api
public class FeatureListGrid extends ListGrid implements FeatureSelectionHandler, SelectionChangedHandler {
/** Key which is used to store the feature id in the records. */
@Api
public static final String FIELD_NAME_FEATURE_ID = "featureId";
private MapModel mapModel;
/**
* Reference to the VectorLayer for whom features can be displayed. The grid header will show the labels of the
* attribute definitions for this layer.
*/
private VectorLayer layer;
private boolean selectionEnabled;
/**
* Show all attributes (true) or only the 'identifying' attributes (false)?
*/
private boolean allAttributesDisplayed;
/**
* Determines whether or not editing the attributes is allowed. When double clicking a row in the table, a
* {@link FeatureAttributeWindow} will appear, containing the feature of the row upon which was double clicked. This
* setting determines if the window allows editing or not.
*/
private boolean editingEnabled;
/**
* Registration for the selection event handler onto the map model.
*/
private HandlerRegistration selectionRegistration;
/**
* Registration for selection of rows within this grid.
*/
private HandlerRegistration gridSelectionRegistration;
/**
* Show the feature's ID in the table. This is false by default, and should not really be necessary.
*/
private boolean idInTable;
/**
* When hovering over image attributes, should they be shown in floating panels or not?
*/
private boolean showImageAttributeOnHover;
/**
* Used to check on doubles.
*/
private List<String> featureIds;
// -------------------------------------------------------------------------
// Constructors:
// -------------------------------------------------------------------------
public FeatureListGrid(MapModel mapModel) {
this(mapModel, null);
}
/**
* @param mapModel
* If this is null it throws an IllegalArgumentException
* @param doubleClickhandler
* It gives the possibility to show your own {@link FeatureAttributeWindow}. If this is null the default
* {@link FeatureAttributeWindow} will be shown
*/
public FeatureListGrid(MapModel mapModel, DoubleClickHandler doubleClickhandler) {
super();
if (mapModel == null) {
throw new IllegalArgumentException("The given MapModel should not be 'null'.");
}
this.mapModel = mapModel;
this.mapModel.addFeatureTransactionHandler(new ApplyFeatureTransactionToGrid());
setShowEmptyMessage(true);
setIdInTable(false);
setEditingEnabled(false);
setSelectionEnabled(true);
if (doubleClickhandler != null) {
addDoubleClickHandler(doubleClickhandler);
} else {
addDoubleClickHandler(new FeatureDoubleClickHandler());
}
}
// -------------------------------------------------------------------------
// Class specific methods:
// -------------------------------------------------------------------------
/**
* Empty the grid, thereby removing all rows. It does not clear the header though.
*/
public void empty() {
setData(new ListGridRecord[] {});
featureIds = new ArrayList<String>();
}
/**
* Adds a new feature to the list. A {@link VectorLayer} must have been set first, and the feature must belong to
* that VectorLayer.
*
* @param feature
* The feature to be added to the list.
* @return Returns true in case of success, and false if the feature is already in the list or the feature is null
* or if the feature does not belong to the correct layer or if the layer has not yet been set.
*/
public boolean addFeature(Feature feature) {
// Basic checks:
if (feature == null || layer == null || !feature.getLayer().getId().equals(layer.getId())) {
return false;
}
// Does feature already exist?
if (featureIds.contains(feature.getId())) {
return false;
}
featureIds.add(feature.getId());
// Feature checks out, add it to the grid:
ListGridRecord record = new ListGridRecord();
record.setAttribute(FIELD_NAME_FEATURE_ID, feature.getId());
copyToRecord(feature, record);
addData(record);
return true;
}
private void copyToRecord(Feature feature, ListGridRecord record) {
for (AttributeInfo attributeInfo : layer.getLayerInfo().getFeatureInfo().getAttributes()) {
String attributeName = attributeInfo.getName();
if (allAttributesDisplayed || attributeInfo.isIdentifying()) {
Attribute<?> attr = feature.getAttributes().get(attributeName);
if (attr.isPrimitive()) {
Object value = attr.getValue();
if (value != null) {
if (value instanceof Boolean) {
record.setAttribute(attributeName, (Boolean) value); // "false" != false
} else if (value instanceof Number) {
record.setAttribute(attributeName, (Number) value);
} else if (value instanceof Date) { // java.util
record.setAttribute(attributeName, (Date) value);
} else {
record.setAttribute(attributeName, value.toString());
}
} else {
record.setAttribute(attributeName, "");
}
} else {
AssociationAttributeInfo associationAttributeInfo = (AssociationAttributeInfo) attributeInfo;
FeatureInfo featureInfo = associationAttributeInfo.getFeature();
String displayName = featureInfo.getDisplayAttributeName();
if (displayName == null) {
displayName = featureInfo.getAttributes().get(0).getName();
}
switch (associationAttributeInfo.getType()) {
case MANY_TO_ONE:
ManyToOneAttribute manyToOneAttribute = (ManyToOneAttribute) attr;
AssociationValue attributeValue = manyToOneAttribute.getValue();
if (null != attributeValue) {
Object value = attributeValue.getAllAttributes().get(displayName).getValue();
if (value != null) {
record.setAttribute(attributeName, value.toString());
} else {
record.setAttribute(attributeName, "");
}
}
break;
case ONE_TO_MANY:
OneToManyAttribute oneToManyAttribute = (OneToManyAttribute) attr;
List<String> values = new ArrayList<String>();
List<AssociationValue> associationValues = oneToManyAttribute.getValue();
if (null != associationValues) {
for (AssociationValue assoc : associationValues) {
Object o = assoc.getAllAttributes().get(displayName).getValue();
if (o != null) {
values.add(o.toString());
}
}
record.setAttribute(attributeName, StringUtil.join(values, ","));
}
break;
default:
throw new IllegalStateException("Unknown switch value " +
associationAttributeInfo.getType());
}
}
}
}
}
/**
* Retrieve the <code>VectorLayer</code> that's currently applied on this grid.
*
* @return Returns the layer, or null if no layer has been set yet.
*/
public VectorLayer getLayer() {
return layer;
}
/**
* Apply a new layer onto the grid. This means that the table header will immediately take on the attributes of this
* new layer.
*
* @param layer
* The layer from whom to display the features. If this is null, the table will be cleared.
*/
public void setLayer(VectorLayer layer) {
this.layer = layer;
if (layer == null) {
clear();
} else {
empty();
updateFields();
}
}
/**
* Is this grid currently tied in to the selection of features in the {@link MapModel}? What this means is that when
* selection is enabled, selected rows in the grid will result in selected features in the MapModel and vice versa.
*
* @return is selection enabled?
*/
public boolean isSelectionEnabled() {
return selectionEnabled;
}
/**
* Adds or removes this widget as a handler for selection onto the MapModel. What this means is that when selection
* is enabled, selected rows in the grid will result in selected features in the MapModel and vice versa.
*
* @param selectionEnabled
* is selection enabled?
*/
public void setSelectionEnabled(boolean selectionEnabled) {
// Clean up first! Otherwise the handler list would just keep on growing.
if (selectionRegistration != null) {
selectionRegistration.removeHandler();
selectionRegistration = null;
}
if (gridSelectionRegistration != null) {
gridSelectionRegistration.removeHandler();
gridSelectionRegistration = null;
}
this.selectionEnabled = selectionEnabled;
// If enabled renew the handlers, and adjust grid selection type:
if (selectionEnabled) {
setAttribute("selectionType", SelectionStyle.MULTIPLE.getValue(), true);
selectionRegistration = mapModel.addFeatureSelectionHandler(this);
gridSelectionRegistration = addSelectionChangedHandler(this);
} else {
setAttribute("selectionType", SelectionStyle.NONE.getValue(), true);
}
}
/**
* Does nothing anymore! Selection is now handled through the "selectionEnabled" flag. This is done so that
* selection in this grid can match selection of features in the {@link MapModel}.
*/
@Override
public void setSelectionType(SelectionStyle selectionType) {
// Disable this setting!!
}
/**
* Determines whether or not editing the attributes is allowed. When double clicking a row in the table, a
* {@link FeatureAttributeWindow} will appear, containing the feature of the row upon which was double clicked. This
* setting determines if the window allows editing or not.
*
* @return true when editing attributes is allowed
*/
public boolean isEditingEnabled() {
return editingEnabled;
}
/**
* Determines whether or not editing the attributes is allowed. When double clicking a row in the table, a
* {@link FeatureAttributeWindow} will appear, containing the feature of the row upon which was double clicked. This
* setting determines if the window allows editing or not.
*
* @param editingEnabled
* The new value
*/
public void setEditingEnabled(boolean editingEnabled) {
this.editingEnabled = editingEnabled;
}
/**
* Is the grid currently displaying all attributes, instead of only the 'identifying' ones?
*
* @return true when all attributes are displayed
*/
public boolean isAllAttributesDisplayed() {
return allAttributesDisplayed;
}
/**
* Determine if all attributes of a layer should be shown, or only the 'identifying' ones. Changing this value will
* not change the layout of the grid. So set this value in advance.
*
* @param allAttributesDisplayed should all attributes be displayed
*/
public void setAllAttributesDisplayed(boolean allAttributesDisplayed) {
this.allAttributesDisplayed = allAttributesDisplayed;
updateFields();
}
/**
* Return whether or not the feature's ID's are currently drawn in the grid.
*
* @return true when feature id included in table
*/
public boolean isIdInTable() {
return idInTable;
}
/**
* Determine whether or not the feature's ID should be displayed in the grid. This method will immediately update
* the entire grid.
*
* @param idInTable should id be included in table
*/
public void setIdInTable(boolean idInTable) {
this.idInTable = idInTable;
updateFields();
}
public boolean isShowImageAttributeOnHover() {
return showImageAttributeOnHover;
}
public void setShowImageAttributeOnHover(boolean showImageAttributeOnHover) {
this.showImageAttributeOnHover = showImageAttributeOnHover;
}
// -------------------------------------------------------------------------
// FeatureSelectionHandler implementation:
// -------------------------------------------------------------------------
/**
* This method is used only when selection is enabled (see setSelectionEnabled). When a feature deselection event is
* sent out from the MapModel, check if we have that row selected and deselect it.
*/
public void onFeatureDeselected(FeatureDeselectedEvent event) {
Feature feature = event.getFeature();
// Only deselect if it is actually selected:
boolean selected = false;
ListGridRecord[] selections = getSelection();
for (ListGridRecord selection : selections) {
if (selection.getAttribute(FIELD_NAME_FEATURE_ID).equals(feature.getId())) {
selected = true;
break;
}
}
// If selected, find the correct row and deselect:
if (selected) {
ListGridRecord[] records = this.getRecords();
for (ListGridRecord record : records) {
if (record.getAttribute(FIELD_NAME_FEATURE_ID).equals(feature.getId())) {
deselectRecord(record);
break;
}
}
}
}
/**
* This method is used only when selection is enabled (see setSelectionEnabled). When a feature selection event is
* sent out from the MapModel, check if we have that row deselected and select it.
*/
public void onFeatureSelected(FeatureSelectedEvent event) {
Feature feature = event.getFeature();
// Only select if it is actually deselected:
boolean selected = false;
ListGridRecord[] selections = getSelection();
for (ListGridRecord selection : selections) {
if (selection.getAttribute(FIELD_NAME_FEATURE_ID).equals(feature.getId())) {
selected = true;
break;
}
}
// If deselected, find the correct row and select:
if (!selected) {
ListGridRecord[] records = this.getRecords();
for (int i = 0; i < records.length; i++) {
if (records[i].getAttribute(FIELD_NAME_FEATURE_ID).equals(feature.getId())) {
selectRecord(i);
break;
}
}
}
}
// -------------------------------------------------------------------------
// SelectionChangedHandler implementation:
// -------------------------------------------------------------------------
/**
* This method is used only when selection is enabled (see setSelectionEnabled). When the user selects or deselect a
* row in the grid, the feature it represents should also be selected or deselected in the MapModel.
*/
public void onSelectionChanged(SelectionEvent event) {
Record record = event.getRecord();
String featureId = record.getAttribute(FIELD_NAME_FEATURE_ID);
// Check if selection and deselection are really necessary, to avoid useless events.
if (event.getState()) {
// Only select a feature if it is not yet selected:
if (!layer.isFeatureSelected(featureId)) {
layer.getFeatureStore().getFeature(featureId, GeomajasConstant.FEATURE_INCLUDE_ALL,
new LazyLoadCallback() {
public void execute(List<Feature> response) {
layer.selectFeature(response.get(0));
}
});
}
} else {
// Only deselect a feature if it is not yet deselected:
if (layer.isFeatureSelected(featureId)) {
layer.getFeatureStore().getFeature(featureId, GeomajasConstant.FEATURE_INCLUDE_ALL,
new LazyLoadCallback() {
public void execute(List<Feature> response) {
layer.deselectFeature(response.get(0));
}
});
}
}
}
// -------------------------------------------------------------------------
// Private methods:
// -------------------------------------------------------------------------
/**
* Actually create or update the fields.
*/
private void updateFields() {
if (layer != null) {
// Create a header field for each attribute definition:
List<ListGridField> fields = new ArrayList<ListGridField>();
if (idInTable) {
ListGridField gridField = new ListGridField(FIELD_NAME_FEATURE_ID, "ID");
gridField.setAlign(Alignment.LEFT);
gridField.setCanEdit(false);
fields.add(gridField);
}
for (AttributeInfo attributeInfo : layer.getLayerInfo().getFeatureInfo().getAttributes()) {
if (!attributeInfo.isHidden() && (attributeInfo.isIdentifying() || allAttributesDisplayed)) {
fields.add(createAttributeGridField(attributeInfo));
}
}
setFields(fields.toArray(new ListGridField[fields.size()]));
setCanResizeFields(true);
}
}
/**
* Create a single field definition from a attribute definition.
*
* @param attributeInfo
* attribute info
* @return field for grid
*/
private ListGridField createAttributeGridField(final AttributeInfo attributeInfo) {
ListGridField gridField = new ListGridField(attributeInfo.getName(), attributeInfo.getLabel());
gridField.setAlign(Alignment.LEFT);
gridField.setCanEdit(false);
gridField.setShowIfCondition(new IdentifyingListGridFieldIfFunction(attributeInfo.isIdentifying()));
if (attributeInfo instanceof PrimitiveAttributeInfo) {
PrimitiveAttributeInfo info = (PrimitiveAttributeInfo) attributeInfo;
if (info.getType().equals(PrimitiveType.BOOLEAN)) {
gridField.setType(ListGridFieldType.BOOLEAN);
} else if (info.getType().equals(PrimitiveType.STRING)) {
gridField.setType(ListGridFieldType.TEXT);
} else if (info.getType().equals(PrimitiveType.DATE)) {
gridField.setType(ListGridFieldType.DATE);
} else if (info.getType().equals(PrimitiveType.SHORT)) {
gridField.setType(ListGridFieldType.INTEGER);
} else if (info.getType().equals(PrimitiveType.INTEGER)) {
gridField.setType(ListGridFieldType.INTEGER);
} else if (info.getType().equals(PrimitiveType.LONG)) {
gridField.setType(ListGridFieldType.INTEGER);
} else if (info.getType().equals(PrimitiveType.FLOAT)) {
gridField.setType(ListGridFieldType.FLOAT);
} else if (info.getType().equals(PrimitiveType.DOUBLE)) {
gridField.setType(ListGridFieldType.FLOAT);
} else if (info.getType().equals(PrimitiveType.IMGURL)) {
gridField.setType(ListGridFieldType.IMAGE);
if (showImageAttributeOnHover) {
addCellOverHandler(new ImageCellHandler(attributeInfo));
}
} else if (info.getType().equals(PrimitiveType.CURRENCY)) {
gridField.setType(ListGridFieldType.TEXT);
} else if (info.getType().equals(PrimitiveType.URL)) {
gridField.setType(ListGridFieldType.LINK);
}
} else if (attributeInfo instanceof AssociationAttributeInfo) {
gridField.setType(ListGridFieldType.TEXT);
}
return gridField;
}
/**
* Private class, implementing the {@link ListGridFieldIfFunction} interface that determines the visibility of a
* grid field, based upon the attribute definition's identifying value, and the {@link FeatureListGrid}'s
* <code>allAttributesDisplayed</code> value.
*
* @author Pieter De Graef
*/
private class IdentifyingListGridFieldIfFunction implements ListGridFieldIfFunction {
private boolean identifying;
public IdentifyingListGridFieldIfFunction(boolean identifying) {
this.identifying = identifying;
}
public boolean execute(ListGrid grid, ListGridField field, int fieldNum) {
if (identifying) {
return true;
}
if (grid instanceof FeatureListGrid) {
FeatureListGrid table = (FeatureListGrid) grid;
if (table.isAllAttributesDisplayed()) {
return true;
}
}
return false;
}
}
/**
* Handler for double click on feature. Shows the feature edit window.
*/
private class FeatureDoubleClickHandler implements DoubleClickHandler {
/**
* Implementation of the {@link DoubleClickHandler}, that will show a {@link FeatureAttributeWindow} containing
* the feature of the row upon which was double clicked. This happens only if the
*/
public void onDoubleClick(DoubleClickEvent event) {
ListGridRecord selected = getSelectedRecord();
String featureId = selected.getAttribute(FIELD_NAME_FEATURE_ID);
if (featureId != null && layer != null) {
layer.getFeatureStore().getFeature(featureId, GeomajasConstant.FEATURE_INCLUDE_ATTRIBUTES,
new LazyLoadCallback() {
public void execute(List<Feature> response) {
FeatureAttributeWindow window = new FeatureAttributeWindow(response.get(0),
editingEnabled);
window.centerInPage();
window.draw();
}
});
}
}
}
/**
* Display the actual image of an image-cell when the mouse goes over it. The image self-destructs after 3 seconds.
*
* @author Pieter De Graef
*/
private class ImageCellHandler implements CellOverHandler {
private Timer killTimer;
private Img img;
private int row = -1;
private AttributeInfo attributeInfo;
ImageCellHandler(AttributeInfo attributeInfo) {
this.attributeInfo = attributeInfo;
}
public void onCellOver(CellOverEvent event) {
ListGridField gridField = FeatureListGrid.this.getField(event.getColNum());
if (gridField.getName().equals(attributeInfo.getName())) {
ListGridRecord record = event.getRecord();
String value = record.getAttribute(attributeInfo.getName());
if (event.getRowNum() != row) {
if (img != null) {
cleanup();
}
img = new Img(value);
img.setMaxWidth(300);
img.setMaxHeight(300);
img.setKeepInParentRect(true);
img.setShowEdges(true);
img.setLeft(FeatureListGrid.this.getAbsoluteLeft() + 10);
img.setTop(FeatureListGrid.this.getAbsoluteTop() + 10);
img.draw();
killTimer = new Timer() {
public void run() {
img.destroy();
}
};
killTimer.schedule(Math.round(3000));
row = event.getRowNum();
}
}
}
private void cleanup() {
killTimer.cancel();
img.destroy();
img = null;
}
}
/**
* Applies the feature transaction to the grid.
*
* @author Jan De Moerloose
*/
private class ApplyFeatureTransactionToGrid implements FeatureTransactionHandler {
public void onTransactionSuccess(FeatureTransactionEvent event) {
List<ListGridRecord> updates = new ArrayList<ListGridRecord>();
for (ListGridRecord record : getRecords()) {
// update record if we can (feature in store) or leave as-is if we can't
String featureId = record.getAttribute(FIELD_NAME_FEATURE_ID);
Feature feature = layer.getFeatureStore().getPartialFeature(featureId);
if (feature != null && feature.isAttributesLoaded()) {
copyToRecord(feature, record);
}
updates.add(record);
}
setData(updates.toArray(new Record[updates.size()]));
}
}
}