/*
* This is part of Geomajas, a GIS framework, http://www.geomajas.org/.
*
* Copyright 2008-2011 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 org.geomajas.command.CommandResponse;
import org.geomajas.command.dto.SearchFeatureRequest;
import org.geomajas.command.dto.SearchFeatureResponse;
import org.geomajas.gwt.client.command.CommandCallback;
import org.geomajas.gwt.client.command.GwtCommand;
import org.geomajas.gwt.client.command.GwtCommandDispatcher;
import org.geomajas.gwt.client.i18n.I18nProvider;
import org.geomajas.gwt.client.map.MapModel;
import org.geomajas.gwt.client.map.event.LayerDeselectedEvent;
import org.geomajas.gwt.client.map.event.LayerSelectedEvent;
import org.geomajas.gwt.client.map.event.LayerSelectionHandler;
import org.geomajas.gwt.client.map.event.MapModelEvent;
import org.geomajas.gwt.client.map.event.MapModelHandler;
import org.geomajas.gwt.client.map.feature.Feature;
import org.geomajas.gwt.client.map.layer.Layer;
import org.geomajas.gwt.client.map.layer.VectorLayer;
import org.geomajas.gwt.client.widget.attribute.AttributeCriterionPane;
import org.geomajas.gwt.client.widget.event.SearchEvent;
import org.geomajas.gwt.client.widget.event.SearchHandler;
import org.geomajas.layer.feature.SearchCriterion;
import com.google.gwt.event.shared.HandlerRegistration;
import com.smartgwt.client.types.Alignment;
import com.smartgwt.client.types.VerticalAlignment;
import com.smartgwt.client.util.SC;
import com.smartgwt.client.widgets.Canvas;
import com.smartgwt.client.widgets.HTMLPane;
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.form.DynamicForm;
import com.smartgwt.client.widgets.form.fields.BlurbItem;
import com.smartgwt.client.widgets.form.fields.FormItem;
import com.smartgwt.client.widgets.form.fields.RadioGroupItem;
import com.smartgwt.client.widgets.form.fields.SelectItem;
import com.smartgwt.client.widgets.form.fields.events.ChangedEvent;
import com.smartgwt.client.widgets.form.fields.events.ChangedHandler;
import com.smartgwt.client.widgets.layout.HLayout;
import com.smartgwt.client.widgets.layout.LayoutSpacer;
import com.smartgwt.client.widgets.layout.VLayout;
import com.smartgwt.client.widgets.layout.VStack;
/**
* <p>
* Widget that supports searching for features through their alpha numerical attributes. Requires a value for
* "manualLayerSelection" at construction time. If true, a select box will be shown so the user can select what layer to
* search in. The possible list of layers consists of all the vector layers that are present in the given MapModel. If
* false, this widget will react to the layer select events that come from the MapMdodel. In that case searching happens
* in the selected layer (if it's a vector layer).
* </p>
* <p>
* When a search has been executed, and a result is returned from the server, a {@link SearchEvent} will be triggered.
* So in order to do something with the result, add a {@link SearchHandler} to this widget. Hint: There is a
* <code>DefaultSearchHandler</code> that displays the resulting set of features in a {@link FeatureListGrid} widget.
* </p>
*
* @author Pieter De Graef
*/
public class FeatureSearch extends Canvas {
public static final String STYLE_SEARCH_ROW = "searchRow";
public static final String STYLE_SEARCH_HEADER = "searchHeader";
public static final String STYLE_HEADER_BAR = "headerBar";
/**
* The logical operator. This operator determines whether all the criteria have to be met in the search (AND), or
* just one of them (OR).
*
* @author Pieter De Graef
*/
public static enum LogicalOperator {
AND, OR
}
private VectorLayer layer; // The vector layer to search in.
private FormItem layerSelect; // A form item that shows the user what layer he's searching in.
private RadioGroupItem logicalOperatorRadio; // Logical operator; match one or match all criteria?
private IButton searchButton; // The actual button that executes the search.
private IButton resetButton; // A button that resets the search layout.
private VStack criterionStack;
private VStack buttonStack;
private MapModel mapModel;
private List<AttributeCriterionPane> criterionPanes;
private List<HLayout> buttonPanes;
private List<HandlerRegistration> addHandlers;
private List<HandlerRegistration> removeHandlers;
/** The maximum number of allowed results. No more features will be retrieved then this number. */
private int maximumResultSize = 100;
private boolean manualLayerSelection;
// -------------------------------------------------------------------------
// Constructors:
// -------------------------------------------------------------------------
/**
* Create a search widget for searching in a specific map model. This widget will automatically react to the
* selection of layers within that map model, and redraw to compensate for the selected layer. In other words,
* searching always happens on the selected layer.
*
* @param mapModel
* The MapModel containing the possible layer to search in.
* @param manualLayerSelection
* If true, a select box will be shown so the user can select what layer to search in. The possible list
* of layers consists of all the vector layers that are present in the given MapModel. If false, this
* widget will react to the layer select events that come from the MapMdodel. In that case searching
* happens in the selected layer (if it's a vector layer).<br/>
* This value cannot be altered anymore.
*/
public FeatureSearch(MapModel mapModel, boolean manualLayerSelection) {
super();
this.mapModel = mapModel;
this.manualLayerSelection = manualLayerSelection;
criterionPanes = new ArrayList<AttributeCriterionPane>();
buttonPanes = new ArrayList<HLayout>();
addHandlers = new ArrayList<HandlerRegistration>();
removeHandlers = new ArrayList<HandlerRegistration>();
buildUI();
setLogicalOperator(LogicalOperator.AND);
}
// -------------------------------------------------------------------------
// Public methods:
// -------------------------------------------------------------------------
/**
* <p>
* Add a handler for the {@link SearchEvent}. A search event, is the event that is triggered when the result of a
* search request successfully returns from the server. The list of features from that result will be present in
* that search event object.
* </p>
* <p>
* Hint: There is a <code>DefaultSearchHandler</code> that displays the resulting set of features in a
* {@link FeatureListGrid} widget.
* </p>
*
* @param handler search handler
* @return handler registration
*/
public HandlerRegistration addSearchHandler(SearchHandler handler) {
return doAddHandler(handler, SearchEvent.TYPE);
}
/**
* Add a new empty row in the search grid, at the given position. An single row can be used to create a single
* criterion.
*
* @param index
* Row number in the search grid, that indicates where to add/insert a new empty row.
*/
public void addEmptyRow(final int index) {
if (layer == null) {
return;
}
searchButton.setDisabled(false);
if (index > 0) {
resetButton.setDisabled(false);
}
// Empty row:
AttributeCriterionPane newRow = new AttributeCriterionPane(layer);
newRow.setHeight(32);
newRow.setStyleName(STYLE_SEARCH_ROW);
HLayout btnLayout = new HLayout();
btnLayout.setHeight(32);
btnLayout.setMembersMargin(5);
btnLayout.setAlign(Alignment.CENTER);
btnLayout.setPadding(4);
btnLayout.setStyleName(STYLE_SEARCH_ROW);
IButton btnAddRow = new IButton();
btnAddRow.setWidth(26);
btnAddRow.setIcon("[ISOMORPHIC]/geomajas/silk/add.png");
HandlerRegistration addReg = btnAddRow.addClickHandler(new ClickHandler() {
public void onClick(ClickEvent event) {
IButton btnAddRow2 = (IButton) event.getSource();
for (int i = 0; i < buttonPanes.size(); i++) {
IButton button = (IButton) buttonPanes.get(i).getMember(0);
if (btnAddRow2.equals(button)) {
addEmptyRow(i + 1);
break;
}
}
buttonPanes.get(0).getMember(1).setDisabled(false);
}
});
IButton btnRemoveRow = new IButton();
btnRemoveRow.setWidth(26);
btnRemoveRow.setIcon("[ISOMORPHIC]/geomajas/silk/remove.png");
HandlerRegistration removeReg = btnRemoveRow.addClickHandler(new ClickHandler() {
public void onClick(ClickEvent event) {
IButton sourceBtn = (IButton) event.getSource();
for (int i = 0; i < buttonPanes.size(); i++) {
IButton button = (IButton) buttonPanes.get(i).getMember(1);
if (sourceBtn.equals(button)) {
criterionStack.removeMember(criterionPanes.remove(i));
// Remove row 0 from add button/registration:
buttonStack.removeMember(buttonPanes.remove(i));
addHandlers.remove(i).removeHandler();
removeHandlers.remove(i).removeHandler();
break;
}
}
if (buttonPanes.size() == 1) {
buttonPanes.get(0).getMember(1).setDisabled(true);
resetButton.setDisabled(true);
}
}
});
if (index == 0) {
btnRemoveRow.setDisabled(true);
}
btnLayout.addMember(btnAddRow);
btnLayout.addMember(btnRemoveRow);
// Add to the stacks:
criterionStack.addMember(newRow, index + 1);
buttonStack.addMember(btnLayout, index + 1);
// Add to the lists:
criterionPanes.add(index, newRow);
buttonPanes.add(index, btnLayout);
addHandlers.add(index, addReg);
removeHandlers.add(index, removeReg);
}
/**
* Get the full list of search criteria from the criterion grid.
*
* @return list of criteria
*/
public List<SearchCriterion> getSearchCriteria() {
List<SearchCriterion> criteria = new ArrayList<SearchCriterion>();
for (AttributeCriterionPane criterionPane : criterionPanes) {
if (criterionPane.hasErrors()) {
SC.warn(I18nProvider.getSearch().warningInvalidCriteria());
return null;
}
SearchCriterion criterion = criterionPane.getSearchCriterion();
if (criterion != null) {
criteria.add(criterion);
}
}
if (criteria.size() == 0) {
SC.warn(I18nProvider.getSearch().warningNoCriteria());
}
return criteria;
}
/**
* Empty the grid, thereby removing all rows. When that is done, a new empty row will be displayed.
*/
public void empty() {
searchButton.setDisabled(true);
resetButton.setDisabled(true);
for (AttributeCriterionPane criterionPane : criterionPanes) {
criterionStack.removeMember(criterionPane);
}
criterionPanes.clear();
for (HLayout criterionPane : buttonPanes) {
buttonStack.removeMember(criterionPane);
}
buttonPanes.clear();
for (HandlerRegistration handlerRegistration : addHandlers) {
handlerRegistration.removeHandler();
}
addHandlers.clear();
for (HandlerRegistration handlerRegistration : removeHandlers) {
handlerRegistration.removeHandler();
}
removeHandlers.clear();
addEmptyRow(0);
}
/**
* Execute the actual search. All features that are returned in the result will be added to the layer's feature
* store. Then a {@link SearchEvent} is fired.
*/
public void search() {
if (layer != null) {
// First we try to get the list of criteria:
List<SearchCriterion> criteria = getSearchCriteria();
if (criteria != null && !criteria.isEmpty()) {
SearchFeatureRequest request = new SearchFeatureRequest();
String value = (String) logicalOperatorRadio.getValue();
if (value.equals(I18nProvider.getSearch().radioOperatorAnd())) {
request.setBooleanOperator("AND");
} else {
request.setBooleanOperator("OR");
}
request.setCriteria(criteria.toArray(new SearchCriterion[criteria.size()]));
request.setCrs(mapModel.getCrs());
request.setLayerId(layer.getServerLayerId());
request.setMax(maximumResultSize);
request.setFilter(layer.getFilter());
request.setFeatureIncludes(GwtCommandDispatcher.getInstance().getLazyFeatureIncludesSelect());
GwtCommand command = new GwtCommand(SearchFeatureRequest.COMMAND);
command.setCommandRequest(request);
GwtCommandDispatcher.getInstance().execute(command, new CommandCallback() {
public void execute(CommandResponse response) {
if (response instanceof SearchFeatureResponse) {
SearchFeatureResponse resp = (SearchFeatureResponse) response;
List<Feature> features = new ArrayList<Feature>();
for (org.geomajas.layer.feature.Feature dtoFeature : resp.getFeatures()) {
Feature feature = new Feature(dtoFeature, layer);
layer.getFeatureStore().addFeature(feature);
features.add(feature);
}
SearchEvent event = new SearchEvent(layer, features);
FeatureSearch.this.fireEvent(event);
}
}
});
}
}
}
// -------------------------------------------------------------------------
// Getters and setters:
// -------------------------------------------------------------------------
/**
* Set a new value for the logical operator. This operator determines whether all the criteria have to be met in the
* search, or just one of them.
*
* @param operator logical operator
*/
public void setLogicalOperator(LogicalOperator operator) {
switch (operator) {
case AND:
logicalOperatorRadio.setValue(I18nProvider.getSearch().radioOperatorAnd());
break;
case OR:
logicalOperatorRadio.setValue(I18nProvider.getSearch().radioOperatorOr());
}
}
/**
* Return the current value for the logical operator.This operator determines whether all the criteria have to be
* met in the search, or just one of them.
*
* @return current logical operator
*/
public LogicalOperator getLogicalOperator() {
String value = (String) logicalOperatorRadio.getValue();
if (value.equals(I18nProvider.getSearch().radioOperatorAnd())) {
return LogicalOperator.AND;
}
return LogicalOperator.OR;
}
/**
* Return the layer onto which searching should happen. (the MapModel's selected layer)
*
* @return layer to search on
*/
public VectorLayer getLayer() {
return layer;
}
/**
* Set a new layer onto which searching should happen.
*
* @param layer layer to search on
*/
public void setLayer(VectorLayer layer) {
this.layer = layer;
Object value = layerSelect.getValue();
if (value == null || !value.equals(layer.getLabel())) {
layerSelect.setValue(layer.getLabel());
}
empty();
}
/**
* Get the maximum number of allowed results. No more features will be retrieved then this number.
*
* @return maximum results
*/
public int getMaximumResultSize() {
return maximumResultSize;
}
/**
* Set a new maximum number of allowed results. No more features will be retrieved then this number.
*
* @param maximumResultSize
* The new value.
*/
public void setMaximumResultSize(int maximumResultSize) {
this.maximumResultSize = maximumResultSize;
}
// -------------------------------------------------------------------------
// Private methods:
// -------------------------------------------------------------------------
private void buildUI() {
// Create the layout:
VLayout layout = new VLayout();
layout.setWidth100();
layout.setHeight100();
logicalOperatorRadio = new RadioGroupItem("logicalOperator");
logicalOperatorRadio.setValueMap(I18nProvider.getSearch().radioOperatorOr(), I18nProvider.getSearch()
.radioOperatorAnd());
logicalOperatorRadio.setVertical(false);
logicalOperatorRadio.setRequired(true);
logicalOperatorRadio.setAlign(Alignment.LEFT);
logicalOperatorRadio.setWidth(250);
logicalOperatorRadio.setShowTitle(false);
HLayout optionLayout = new HLayout();
optionLayout.setHeight(50);
optionLayout.setWidth100();
VLayout leftLayout = new VLayout();
leftLayout.setAlign(Alignment.LEFT);
HLayout layerLayout = new HLayout();
layerLayout.setWidth(420);
DynamicForm layerForm = new DynamicForm();
layerForm.setHeight(30);
if (manualLayerSelection) {
layerSelect = new SelectItem();
layerSelect.setTitle(I18nProvider.getSearch().labelLayerSelected());
layerSelect.setWidth(250);
layerSelect.setHint(I18nProvider.getSearch().labelNoLayerSelected());
((SelectItem) layerSelect).setShowHintInField(true);
layerSelect.addChangedHandler(new ChangedHandler() {
public void onChanged(ChangedEvent event) {
String layerLabel = (String) event.getValue();
for (Layer<?> vLayer : mapModel.getLayers()) {
if (vLayer.getLabel().equals(layerLabel)) {
setLayer((VectorLayer) vLayer);
}
}
}
});
mapModel.addMapModelHandler(new MapModelHandler() {
public void onMapModelChange(MapModelEvent event) {
List<String> layers = new ArrayList<String>();
for (Layer<?> vLayer : mapModel.getLayers()) {
if (vLayer instanceof VectorLayer) {
layers.add(vLayer.getLabel());
}
}
layerSelect.setValueMap(layers.toArray(new String[layers.size()]));
}
});
} else {
mapModel.addLayerSelectionHandler(new LayerSelectionHandler() {
public void onDeselectLayer(LayerDeselectedEvent event) {
empty();
updateLabelTitle(I18nProvider.getSearch().labelNoLayerSelected());
}
public void onSelectLayer(LayerSelectedEvent event) {
if (event.getLayer() instanceof VectorLayer) {
setLayer((VectorLayer) event.getLayer());
if (event.getLayer() != null) {
updateLabelTitle(event.getLayer().getLabel());
}
}
}
});
layerSelect = new BlurbItem();
layerSelect.setShowTitle(true);
layerSelect.setTitle(I18nProvider.getSearch().labelLayerSelected());
layerSelect.setWidth(250);
layerSelect.setValue("<b>" + I18nProvider.getSearch().labelNoLayerSelected() + "</b>");
}
layerForm.setFields(layerSelect);
layerLayout.addMember(layerForm);
leftLayout.addMember(layerLayout);
DynamicForm logicalForm = new DynamicForm();
logicalForm.setAutoWidth();
logicalForm.setLayoutAlign(Alignment.CENTER);
logicalForm.setFields(logicalOperatorRadio);
leftLayout.setWidth(420);
leftLayout.addMember(logicalForm);
VLayout rightLayout = new VLayout();
rightLayout.setLayoutAlign(VerticalAlignment.TOP);
rightLayout.setMargin(5);
rightLayout.setMembersMargin(5);
rightLayout.setWidth(100);
searchButton = new IButton(I18nProvider.getSearch().btnSearch());
searchButton.setIcon("[ISOMORPHIC]/geomajas/silk/find.png");
searchButton.setWidth(100);
searchButton.setDisabled(true);
searchButton.addClickHandler(new ClickHandler() {
public void onClick(ClickEvent event) {
search();
}
});
resetButton = new IButton(I18nProvider.getSearch().btnReset());
resetButton.setIcon("[ISOMORPHIC]/geomajas/silk/undo.png");
resetButton.setWidth(100);
resetButton.setDisabled(true);
resetButton.addClickHandler(new ClickHandler() {
public void onClick(ClickEvent event) {
empty();
}
});
rightLayout.addMember(searchButton);
rightLayout.addMember(resetButton);
optionLayout.addMember(leftLayout);
optionLayout.addMember(new LayoutSpacer());
optionLayout.addMember(rightLayout);
// Create a header for the criterionStack:
HLayout headerLayout = new HLayout();
headerLayout.setHeight(26);
headerLayout.setStyleName(STYLE_HEADER_BAR);
HTMLPane attrHeader = new HTMLPane();
attrHeader.setStyleName(STYLE_SEARCH_HEADER);
attrHeader.setContents("Attribute");
attrHeader.setWidth(140);
HTMLPane operatorHeader = new HTMLPane();
operatorHeader.setContents("Operator");
operatorHeader.setWidth(140);
operatorHeader.setStyleName(STYLE_SEARCH_HEADER);
HTMLPane valueHeader = new HTMLPane();
valueHeader.setContents("Value");
valueHeader.setStyleName(STYLE_SEARCH_HEADER);
criterionStack = new VStack();
criterionStack.setAlign(VerticalAlignment.TOP);
headerLayout.addMember(attrHeader);
headerLayout.addMember(operatorHeader);
headerLayout.addMember(valueHeader);
criterionStack.addMember(headerLayout);
buttonStack = new VStack();
buttonStack.setWidth(70);
buttonStack.setAlign(VerticalAlignment.TOP);
HTMLPane btnHeader = new HTMLPane();
btnHeader.setStyleName(STYLE_HEADER_BAR);
btnHeader.setWidth(70);
btnHeader.setHeight(26);
buttonStack.addMember(btnHeader);
HLayout searchGrid = new HLayout();
searchGrid.addMember(criterionStack);
searchGrid.addMember(buttonStack);
searchGrid.setBorder("1px solid lightgrey");
layout.addMember(optionLayout);
layout.addMember(searchGrid);
addChild(layout);
}
private void updateLabelTitle(String title) {
layerSelect.setValue("<b>" + title + "</b>");
}
}