Mercurial > dive4elements > river
diff gwt-client/src/main/java/org/dive4elements/river/client/client/ui/chart/CrossSectionChartThemePanel.java @ 5838:5aa05a7a34b7
Rename modules to more fitting names.
author | Sascha L. Teichmann <teichmann@intevation.de> |
---|---|
date | Thu, 25 Apr 2013 15:23:37 +0200 |
parents | flys-client/src/main/java/org/dive4elements/river/client/client/ui/chart/CrossSectionChartThemePanel.java@821a02bbfb4e |
children | 172338b1407f |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/gwt-client/src/main/java/org/dive4elements/river/client/client/ui/chart/CrossSectionChartThemePanel.java Thu Apr 25 15:23:37 2013 +0200 @@ -0,0 +1,722 @@ +package org.dive4elements.river.client.client.ui.chart; + +import com.google.gwt.core.client.GWT; +import com.google.gwt.user.client.rpc.AsyncCallback; + +import com.smartgwt.client.types.ListGridFieldType; +import com.smartgwt.client.util.SC; +import com.smartgwt.client.widgets.Canvas; +import com.smartgwt.client.widgets.form.DynamicForm; +import com.smartgwt.client.widgets.form.fields.SelectItem; +import com.smartgwt.client.widgets.form.fields.events.ChangeEvent; +import com.smartgwt.client.widgets.form.fields.events.ChangeHandler; +import com.smartgwt.client.widgets.grid.ListGrid; +import com.smartgwt.client.widgets.grid.ListGridField; +import com.smartgwt.client.widgets.grid.ListGridRecord; +import com.smartgwt.client.widgets.layout.VLayout; +import com.smartgwt.client.widgets.menu.Menu; +import com.smartgwt.client.widgets.menu.MenuItem; +import com.smartgwt.client.widgets.menu.events.ClickHandler; +import com.smartgwt.client.widgets.menu.events.MenuItemClickEvent; + +import org.dive4elements.river.client.client.Config; +import org.dive4elements.river.client.client.services.CrossSectionKMServiceAsync; +import org.dive4elements.river.client.client.services.LoadArtifactService; +import org.dive4elements.river.client.client.services.LoadArtifactServiceAsync; +import org.dive4elements.river.client.client.ui.CollectionView; +import org.dive4elements.river.client.client.widgets.KMSpinner; +import org.dive4elements.river.client.client.widgets.KMSpinnerChangeListener; +import org.dive4elements.river.client.shared.model.Artifact; +import org.dive4elements.river.client.shared.model.Data; +import org.dive4elements.river.client.shared.model.DefaultArtifact; +import org.dive4elements.river.client.shared.model.DefaultData; +import org.dive4elements.river.client.shared.model.FacetRecord; +import org.dive4elements.river.client.shared.model.OutputMode; +import org.dive4elements.river.client.shared.model.Theme; +import org.dive4elements.river.client.shared.model.ThemeList; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + + +/** + * ThemePanel much like ChartThemePanel, but shows an "Actions" column, + * needed for interaction in the CrossSection Charts and a selector to + * declare which cross section profile is "master" (waterlevels refer to the + * chosen kilometer of that cross section profile). + * Also can show 'area creation' context menus. + */ +public class CrossSectionChartThemePanel +extends ChartThemePanel +implements KMSpinnerChangeListener +{ + /** Artifact Clone/Creation service. */ + protected LoadArtifactServiceAsync loadService = + GWT.create(LoadArtifactService.class); + + /** Service to query measurement points of cross sections. */ + CrossSectionKMServiceAsync kmService = GWT.create( + org.dive4elements.river.client.client.services.CrossSectionKMService.class); + + /** UUID of the current "master" cross section. */ + protected String currentCSMasterUUID; + + /** The layout (used for visual active/inactive feedback). */ + protected VLayout layout; + + /** Data item name for CrossSections selected km. */ + protected static String CS_KM = "cross_section.km"; + + /** Data item name for CrossSections selected km. */ + protected static String CS_IS_MASTER = "cross_section.master?"; + + /** List of cross-section themes through which is moved through synchronously. */ + protected HashSet synchronCrossSectionThemes = new HashSet(); + + /** Data for master artifact combobox.*/ + protected LinkedHashMap<String, String> masters; + + /** Combobox for master artifacts.*/ + protected SelectItem masterCb; + + + /** + * Trivial constructor. + */ + public CrossSectionChartThemePanel( + OutputMode mode, + CollectionView view) + { + super(mode, view); + } + + + /** Create DefaultArtifact. */ + public static DefaultArtifact artifactReference(String uuid) { + return new DefaultArtifact(uuid, "TODO:hash"); + } + + + /** Access data of collection item of theme. */ + public static String dataOf(Theme theme, String dataItemName) { + if (theme != null && theme.getCollectionItem() != null + && theme.getCollectionItem().getData() != null + ) { + return theme.getCollectionItem().getData().get(dataItemName); + } + + return null; + } + + + /** + * Feed an artifact to let it know that it is master wrt cross-sections. + * @param artifact uuid of an artifact. + */ + public void feedTellMaster(final String artifact) { + Data[] feedData = DefaultData.createSimpleStringDataArray( + CS_IS_MASTER, "1"); + + feedService.feed( + Config.getInstance().getLocale(), + artifactReference(artifact), + feedData, + new AsyncCallback<Artifact>() { + @Override + public void onFailure(Throwable caught) { + GWT.log("Could not feed artifact (" + artifact + + ") with master marker: " + caught.getMessage()); + SC.warn(MSG.getString(caught.getMessage())); + enable(); + } + @Override + public void onSuccess(Artifact artifact) { + GWT.log("Successfully injected master mark to " + artifact); + setCurrentCSMaster(artifact.getUuid()); + requestRedraw(); + enable(); + } + }); + } + + + /** + * Sets currentCSMasterUUID. + */ + public String findCurrentCSMaster() { + ThemeList themeList = getThemeList(); + int count = getThemeList().getThemeCount(); + String firstCSUuid = null; + + for (int i = 1; i <= count; i++) { + Theme theme = themeList.getThemeAt(i); + String value = dataOf(theme, CS_IS_MASTER); + + if (value != null) { + if (firstCSUuid == null) { + firstCSUuid = theme.getArtifact(); + } + if (!value.equals("0")) { + setCurrentCSMaster(theme.getArtifact()); + GWT.log("found a master: " + currentCSMasterUUID + + "/" + theme.getDescription()); + return theme.getDescription(); + } + } + } + // There is none selected. Take the first one. + if (firstCSUuid != null) { + setCurrentCSMaster(firstCSUuid); + feedTellMaster(firstCSUuid); + } + return null; + } + + + /** + * Create Layout, add a master selection box beneath. + */ + @Override + protected VLayout createLayout() { + layout = super.createLayout(); + + // Create "set master" combobox. + masterCb = new SelectItem(); + + masterCb.setTitle(MSG.chart_themepanel_set_master()); + masterCb.setType("comboBox"); + masters = getThemeList().toMapArtifactUUIDDescription("cross_section"); + masterCb.setValueMap(masters); + + final DynamicForm form = new DynamicForm(); + form.setWidth(200); + form.setFields(masterCb); + layout.addMember(form, 0); + + Config config = Config.getInstance(); + final String locale = config.getLocale(); + findCurrentCSMaster(); + masterCb.setValue(getCurrentCSMaster()); + + // Add Change Handler to first unset the old master and then set the + // new master. + masterCb.addChangeHandler(new ChangeHandler() { + @Override + public void onChange(ChangeEvent event) { + String selectedItem = (String) event.getValue(); + final String artifact = selectedItem; + + disable(); + + // Tell current master that he is not master anymore. + if (getCurrentCSMaster() != null) { + Data[] feedData = DefaultData.createSimpleStringDataArray( + CS_IS_MASTER, "0"); + feedService.feed( + locale, + artifactReference(getCurrentCSMaster()), + feedData, + new AsyncCallback<Artifact>() { + @Override + public void onFailure(Throwable caught) { + GWT.log("Could not un-master artifact (" + + getCurrentCSMaster() + "): " + + caught.getMessage()); + SC.warn(MSG.getString(caught.getMessage())); + enable(); + } + @Override + public void onSuccess(Artifact oldMaster) { + GWT.log("Successfully un-mastered artifact."); + feedTellMaster(artifact); + } + }); + } + else { + feedTellMaster(artifact); + } + } + }); + + return layout; + } + + + /** Disable the UI (becomes gray, inresponsive to user input). */ + @Override + public void disable() { + this.layout.setDisabled(true); + } + + + /** DisDisable the UI (becomes ungray, responsive to user input). */ + @Override + public void enable() { + this.layout.setDisabled(false); + } + + + /** + * Returns a double from the list that has the smallest distance to the + * given to value. In case of multiple values with the same difference, + * the last one is taken. + * @param in possible return values. + * @param to the value to be as close to as possible. + * @param up if true, prefer numerically higher values in case of two + * values with equal distance to \param to. + * @return value from in that is closest to to, -1 if none. + */ + public static double closest(Double[] in, double to, boolean up) { + if (in == null || in.length == 0) { + return -1; + } + if (in[0] == to) { + return to; + } + for (int i = 0; i < in.length; i++) { + GWT.log ("Close? " + in[i]); + } + + double minDiff = Math.abs(to - in[0]); + double bestMatch = in[0]; + for (int i = 1; i < in.length; i++) { + if (in[i] == to) { + return to; + } + if ((in[i] < to && up) + || (in[i] > to && !up)) { + continue; + } + double diff = Math.abs(to - in[i]); + if (diff <= minDiff) { + minDiff = diff; + bestMatch = in[i]; + } + } + return bestMatch; + } + + + /** + * Feed a single artifact with the km of the crosssection to display. + * If its the selected master, also feed the collectionmaster. + * + * @param artifacts List of artifacts to feed. + * @param kmD The km to set. + */ + public void sendFeed(final List<Artifact> artifacts, final double kmD) { + Config config = Config.getInstance(); + final String locale = config.getLocale(); + + Data[] feedData = + DefaultData.createSimpleStringDataArray(CS_KM, + Double.valueOf(kmD).toString()); + + disable(); + // TODO + // The ones who do not have data for this km should not show line! + feedService.feedMany( + locale, + artifacts, + feedData, + new AsyncCallback<List<Artifact>>() { + @Override + public void onFailure(Throwable caught) { + GWT.log("Could not feed many artifacts " + caught.getMessage()); + SC.warn(MSG.getString(caught.getMessage())); + enable(); + } + @Override + public void onSuccess(List<Artifact> artifact) { + GWT.log("Successfully fed many with km"); + requestRedraw(); + enable(); + } + }); + } + + + /** + * Feed a single artifact with the km of the crosssection to display. + * If its the selected master, also feed the collectionmaster. + * @param artUUID The UUID of the artifact to feed. + * @param kmD The km to set. + */ + public void sendFeed(final String artUUID, final double kmD) { + Config config = Config.getInstance(); + final String locale = config.getLocale(); + + Data[] feedData = + DefaultData.createSimpleStringDataArray(CS_KM, + Double.valueOf(kmD).toString()); + + disable(); + feedService.feed( + locale, + artifactReference(artUUID), + feedData, + new AsyncCallback<Artifact>() { + @Override + public void onFailure(Throwable caught) { + GWT.log("Could not feed artifact " + caught.getMessage()); + SC.warn(MSG.getString(caught.getMessage())); + enable(); + } + @Override + public void onSuccess(Artifact artifact) { + GWT.log("Successfully fed with km"); + requestRedraw(); + enable(); + } + }); + } + + + /** Remove the themes, also from the master (reference) select box. */ + @Override + protected void removeThemes(final ListGridRecord[] records) { + // TODO take care of what happens if that was the last + // cross section and/or the cross section currently selected as master. + for (ListGridRecord record: records) { + FacetRecord facet = (FacetRecord) record; + Theme theme = facet.getTheme(); + masters.remove(theme.getArtifact()); + } + masterCb.setValueMap(masters); + super.removeThemes(records); + } + + + /** + * Callback for when a value has been accepted in the km-spinner + * of a Cross-Section Profile theme. + * @param item The SpinnerItem which was manipulated. + * @param enteredKm The double-parsed value that has been entered. + * @param facetRecord The underlying datastores record. + * @param up If true, numerically higher values are preferred if + * two values in \param in are in the same distance to + * \param to. + */ + @Override + public void spinnerValueEntered(KMSpinner spinner, final double enteredKm, final FacetRecord facetRecord, final boolean up) { + disable(); + Config config = Config.getInstance(); + final String locale = config.getLocale(); + + Map<Integer, Double> map = new HashMap<Integer,Double>(); + int _dbid = -1; + try { + _dbid = Integer.valueOf(facetRecord.getTheme() + .getCollectionItem() + .getData().get("cross_section.dbid")); + } + catch (NumberFormatException nfe) { + GWT.log("Could not extract cross-section db id from data."); + } + final int dbid = _dbid; + + map.put(dbid, enteredKm); + + // Query the available cross section measurements. + kmService.getCrossSectionKMs(locale, map, 2, + new AsyncCallback<Map<Integer, Double[]>>() { + @Override + public void onFailure(Throwable caught) { + GWT.log("Could not get single km for " + + dbid + ": "+ caught.getMessage()); + SC.warn(MSG.getString(caught.getMessage())); + updateCollection(); + //updateGrid(); + enable(); + } + + @Override + public void onSuccess(Map<Integer, Double[]> obj) { + Double[] kms = obj.get(dbid); + double closest = + CrossSectionChartThemePanel.closest(kms, enteredKm, up); + GWT.log("Got single km close to " + enteredKm + " for " + dbid + ", it is " + + closest); + + // Do not set value, as it will trigger strange + // "javascript" bugs. /*item.setValue(closest);*/ + if (synchronCrossSectionThemes.contains (themeHash + (facetRecord.getTheme()))) { + // Move all other synchrons + ThemeList themes = getThemeList(); + int nThemes = themes.getThemeCount(); + List<Artifact> artifacts = new ArrayList<Artifact>(); + for (int i = 0; i < nThemes; i++) { + final Theme theme = themes.getThemeAt(i+1); + if (theme.getFacet().equals("cross_section") && + theme.getActive() == 1 && + synchronCrossSectionThemes.contains(themeHash(theme)) + ) { + artifacts.add(artifactReference(theme.getArtifact())); + } + } + sendFeed(artifacts, closest); + } + else { + sendFeed(facetRecord.getTheme().getArtifact(), + closest); + } + } + }); + } + + + /** + * Create and configure the Grid to display. + * @return ListGrid with Themes and related controls inside. + */ + @Override + protected ListGrid createGrid() { + final CrossSectionChartThemePanel parent = this; + + ListGrid list = new ListGrid() { + @Override + protected Canvas createRecordComponent( + final ListGridRecord record, + Integer colNum) + { + // Only cross_section Facets display an action widget. + final FacetRecord facetRecord = (FacetRecord) record; + if (!facetRecord.getTheme().getFacet().equals( + "cross_section")) + { + return null; + } + + String fieldName = this.getFieldName(colNum); + + if (fieldName.equals(GRID_FIELD_ACTIONS)) { + double currentValue = + Double.valueOf(facetRecord.getTheme().getCollectionItem().getData().get(CS_KM)); + KMSpinner kmSpinner = new KMSpinner(currentValue, facetRecord); + kmSpinner.addChangeListener(parent); + return kmSpinner; + } + else { + return null; + } + } + }; + list.setCanResizeFields(true); + list.setShowRecordComponents(true); + list.setShowRecordComponentsByCell(true); + list.setShowAllRecords(true); + list.setShowHeaderContextMenu(false); + list.setLeaveScrollbarGap(false); + return list; + } + + + /** + * Initializes the components (columns) of the theme grid. + */ + @Override + protected void initGrid() { + list.setCanEdit(true); + list.setCanSort(false); + list.setShowRecordComponents(true); + list.setShowRecordComponentsByCell(true); + list.setShowHeader(true); + list.setWidth100(); + list.setHeight100(); + + list.addEditCompleteHandler(this); + + ListGridField active = new ListGridField(GRID_FIELD_ACTIVE, " ", 20); + active.setType(ListGridFieldType.BOOLEAN); + + ListGridField name = new ListGridField( + GRID_FIELD_NAME, MSG.chart_themepanel_header_themes()); + name.setType(ListGridFieldType.TEXT); + + ListGridField actions = new ListGridField(GRID_FIELD_ACTIONS, + MSG.chart_themepanel_header_actions(), 100); + + list.setFields(active, name, actions); + } + + + /** Get Current Cross-section Masters uuid. */ + public String getCurrentCSMaster() { + return currentCSMasterUUID; + } + + + /** Set Current Cross-section Masters uuid. */ + public void setCurrentCSMaster(String currentMasterUuid) { + this.currentCSMasterUUID = currentMasterUuid; + } + + + /** Returns name of cross section area facets. */ + @Override + protected String getAreaFacetName() { + return "cross_section.area"; + } + + + /** + * Return true if two themes are canditates for an area being + * rendered between them. + * TODO join with canArea, generalize to allow easier modification + * in subclasses. + */ + @Override + protected boolean areAreaCompatible(Theme a, Theme b) { + if (a.equals(b)) { + return false; + } + return (a.getFacet().equals("cross_section") + || a.getFacet().endsWith("line")) + && (b.getFacet().equals("cross_section") + || b.getFacet().endsWith("line")); + } + + + /** + * True if context menu should contain 'create area' submenu on + * this theme. + */ + @Override + protected boolean canArea(Theme a) { + return a.getFacet().equals("cross_section") + || a.getFacet().equals("cross_section_water_line") + || a.getFacet().endsWith("line"); + } + + + protected String themeHash(Theme theme) { + return theme.getArtifact() + theme.getFacet() + theme.getIndex(); + } + + + /** + * Include synchron navigation item. + */ + @Override + protected Menu getSingleContextMenu(final ListGridRecord[] records) { + Menu contextMenu = super.getSingleContextMenu(records); + + Theme facetTheme = ((FacetRecord)records[0]).getTheme(); + String item = facetTheme.getFacet(); + + if (item.equals("cross_section")) { + // Synchron checking. + MenuItem synchronNavigationMenuItem = new MenuItem(); + final String themeHash = themeHash(facetTheme); + if (synchronCrossSectionThemes.contains(themeHash)) { + synchronNavigationMenuItem.setTitle(MSG.chart_themepanel_asynchron()); + synchronNavigationMenuItem.addClickHandler(new ClickHandler() { + @Override + public void onClick(MenuItemClickEvent evt) { + synchronCrossSectionThemes.remove (themeHash); + } + }); + } + else { + synchronNavigationMenuItem.setTitle(MSG.chart_themepanel_synchron()); + synchronNavigationMenuItem.addClickHandler(new ClickHandler() { + @Override + public void onClick(MenuItemClickEvent evt) { + synchronCrossSectionThemes.add (themeHash); + } + }); + } + contextMenu.addItem(synchronNavigationMenuItem); + } + + return contextMenu; + } + + + /** + * This method is used to clear the current theme grid and add new updated + * data. + */ + @Override + protected void updateGrid() { + GWT.log("CrossSectionChartThemePanel.updateGrid"); + + ListGridRecord[] selected = list.getSelectedRecords(); + + clearGrid(); + + ThemeList themeList = getThemeList(); + + if (themeList == null) { + GWT.log("ERROR: No theme list."); + return; + } + + int count = themeList.getThemeCount(); + + for (int i = 1; i <= count; i++) { + Theme theme = themeList.getThemeAt(i); + + if (theme == null) { + continue; + } + + if (theme.getFacet().equals("empty.facet")) { + theme.setVisible(0); + } + + if (theme.getVisible() == 0) { + continue; + } + + if (theme.getFacet().equals("cross_section")) { + addToReferences(theme); + } + + FacetRecord newRecord = createRecord(theme); + addFacetRecord(newRecord); + + String newArtifact = theme.getArtifact(); + String newFacet = theme.getFacet(); + int newIndex = theme.getIndex(); + + for (ListGridRecord r: selected) { + FacetRecord sel = (FacetRecord) r; + Theme oldTheme = sel.getTheme(); + + if (oldTheme.getArtifact().equals(newArtifact) + && oldTheme.getFacet().equals(newFacet) + && oldTheme.getIndex() == newIndex) { + list.selectRecord(newRecord); + } + } + } + + fireOutputParameterChanged(); + + } + + + /** + * Adds a cross section theme to the master artifacts combobox and finds + * a new master if necessary. + * + * @param theme The cross section theme. + */ + protected void addToReferences(Theme theme) { + if (theme.getVisible() != 0) { + masters.put(theme.getArtifact(), theme.getDescription()); + masterCb.setValueMap(masters); + } + findCurrentCSMaster(); + if (masterCb.getSelectedRecord() == null) { + masterCb.setValue(getCurrentCSMaster()); + } + } +} +// vim:set ts=4 sw=4 si et sta sts=4 fenc=utf8 :