Mercurial > dive4elements > river
view flys-client/src/main/java/de/intevation/flys/client/client/ui/chart/CrossSectionChartThemePanel.java @ 2545:f25a3eb9785d
Issue 671.
Added tooltips to chart toolbar buttons.
flys-client/trunk@4503 c6561f87-3c4e-4783-a992-168aeb5c3f6f
author | Raimund Renkert <raimund.renkert@intevation.de> |
---|---|
date | Thu, 24 May 2012 13:22:15 +0000 |
parents | 04bd9f1611a1 |
children | 51ed89b754ae |
line wrap: on
line source
package de.intevation.flys.client.client.ui.chart; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; import java.util.Map; import java.util.LinkedHashMap; import java.util.List; import com.google.gwt.core.client.GWT; import com.google.gwt.i18n.client.NumberFormat; import com.google.gwt.user.client.rpc.AsyncCallback; import com.smartgwt.client.util.SC; import com.smartgwt.client.types.ListGridFieldType; import com.smartgwt.client.data.Record; import com.smartgwt.client.widgets.Button; import com.smartgwt.client.widgets.Canvas; import com.smartgwt.client.widgets.Label; 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.HLayout; import com.smartgwt.client.widgets.layout.VLayout; import com.smartgwt.client.widgets.form.fields.FormItem; import com.smartgwt.client.widgets.form.fields.SpinnerItem; import com.smartgwt.client.widgets.form.DynamicForm; import com.smartgwt.client.widgets.form.FormItemValueFormatter; import com.smartgwt.client.widgets.form.FormItemValueParser; import com.smartgwt.client.widgets.form.fields.SelectItem; import com.smartgwt.client.widgets.form.fields.TextItem; import com.smartgwt.client.widgets.form.fields.events.ChangeEvent; import com.smartgwt.client.widgets.form.fields.events.ChangedEvent; import com.smartgwt.client.widgets.form.fields.events.ChangedHandler; import com.smartgwt.client.widgets.form.fields.events.ChangeHandler; import com.smartgwt.client.widgets.form.fields.events.KeyPressEvent; import com.smartgwt.client.widgets.form.fields.events.KeyPressHandler; import com.smartgwt.client.widgets.menu.Menu; import com.smartgwt.client.widgets.menu.MenuItem; import com.smartgwt.client.widgets.form.fields.events.BlurEvent; import com.smartgwt.client.widgets.form.fields.events.BlurHandler; import com.smartgwt.client.widgets.events.ClickEvent; import com.smartgwt.client.widgets.menu.events.ClickHandler; import com.smartgwt.client.widgets.menu.events.MenuItemClickEvent; import de.intevation.flys.client.client.Config; import de.intevation.flys.client.shared.model.Artifact; import de.intevation.flys.client.shared.model.Data; import de.intevation.flys.client.shared.model.DefaultArtifact; import de.intevation.flys.client.shared.model.DefaultData; import de.intevation.flys.client.shared.model.FacetRecord; import de.intevation.flys.client.shared.model.OutputMode; import de.intevation.flys.client.shared.model.Theme; import de.intevation.flys.client.shared.model.ThemeList; import de.intevation.flys.client.client.services.CrossSectionKMServiceAsync; import de.intevation.flys.client.client.services.LoadArtifactService; import de.intevation.flys.client.client.services.LoadArtifactServiceAsync; import de.intevation.flys.client.client.ui.CollectionView; /** * 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 { /** Artifact Clone/Creation service. */ protected LoadArtifactServiceAsync loadService = GWT.create(LoadArtifactService.class); /** Service to query measurement points of cross sections. */ CrossSectionKMServiceAsync kmService = GWT.create( de.intevation.flys.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>() { public void onFailure(Throwable caught) { GWT.log("Could not feed artifact (" + artifact + ") with master marker: " + caught.getMessage()); SC.warn(MSG.getString(caught.getMessage())); enable(); } 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; Theme firstCS = 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(); firstCS = theme; } 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() { 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>() { public void onFailure(Throwable caught) { GWT.log("Could not un-master artifact (" + getCurrentCSMaster() + "): " + caught.getMessage()); SC.warn(MSG.getString(caught.getMessage())); enable(); } 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). */ public void disable() { this.layout.setDisabled(true); } /** DisDisable the UI (becomes ungray, responsive to user input). */ 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. * @return value from in that is closest to to, -1 if none. */ public static double closest(Double[] in, double to) { if (in == null || in.length == 0) { return -1; } if (in[0] == to) { return to; } 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; } 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. */ public void spinnerValueEntered(final SpinnerItem item, final double enteredKm, final FacetRecord facetRecord ) { 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); 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 the handler for ChangeEvents on the Spinner in the * facets that control km of cross section. * * @param facetRecord The FacetRecord (~row in table) where this * handler is added to (to a child, to be exact). */ // TODO obsolete? public final ChangedHandler createSpinnerHandler( final FacetRecord facetRecord) { ChangedHandler handler = new ChangedHandler() { @Override public void onChanged(final ChangedEvent ce) { if (ce.getValue() == null) { return; } String value = ce.getValue().toString(); // Now, query the value with the kmService. final double selected_km = Double.parseDouble(value); final SpinnerItem item = (SpinnerItem) ce.getItem(); spinnerValueEntered(item, selected_km, facetRecord); } }; return handler; } /** * Create a "kilometer spinner" for CrossSection Facets. * @param facetRecord The respective Facet/Theme. * @return label, intialized SpinnerItem. */ public SpinnerItem createSpinnerItem(FacetRecord facetRecord) { SpinnerItem spinnerItem = new SpinnerItem(); spinnerItem.setShowTitle(false); spinnerItem.setTitle("Waterlevel-Spinner"); spinnerItem.setWidth(45); spinnerItem.setDefaultValue(Double.valueOf(facetRecord.getTheme() .getCollectionItem() .getData().get(CS_KM))); spinnerItem.setMin(0); spinnerItem.setMax(2000); spinnerItem.setStep(0.1d); spinnerItem.setChangeOnKeypress(true); return spinnerItem; } /** * SpinnerItem-like element with text label and up/down buttons. */ public class KmSpinner extends HLayout { protected Label label; protected FacetRecord facetRecord; protected double currentValue; public KmSpinner(FacetRecord facetRecord) { super(2); this.facetRecord = facetRecord; final FacetRecord _facetRecord = facetRecord; currentValue = Double.valueOf(facetRecord.getTheme() .getCollectionItem().getData().get(CS_KM)); // Buttons and labels. int height = 18; // minusButton shall ask service for previous available cs. Button minusButton = new Button("-"); minusButton.setWidth(10); minusButton.setHeight(height); minusButton.addClickHandler(new com.smartgwt.client.widgets.events.ClickHandler() { public void onClick(ClickEvent evt) { spinnerValueEntered(null, currentValue - 0.1d, _facetRecord); } }); DynamicForm form = new DynamicForm(); final TextItem kmField = new TextItem(); kmField.setValue(currentValue); kmField.setWidth(35); kmField.setTitle(""); kmField.setHeight(height); FormItemValueFormatter doubleFormat = new FormItemValueFormatter() { public String formatValue(Object value, Record record, DynamicForm form, FormItem item) { if (value != null) { NumberFormat nf = NumberFormat.getDecimalFormat(); try { double d = Double.valueOf(value.toString()).doubleValue(); return nf.format(d); } catch (Exception e) { return value.toString(); } } else { return null; } } }; kmField.setEditorValueFormatter(doubleFormat); FormItemValueParser doubleParser = new FormItemValueParser() { public Object parseValue(String value, DynamicForm form, FormItem item) { if (value == null) return null; try { NumberFormat nf = NumberFormat.getDecimalFormat(); double d = nf.parse(value.toString()); return (new Double(d)).toString(); } catch(NumberFormatException nfe) { return value; } } }; kmField.setEditorValueParser(doubleParser); // Update on focus lost and enter-pressed. kmField.addBlurHandler(new BlurHandler() { @Override public void onBlur(BlurEvent be) { if (kmField.getValue() != null) { try { spinnerValueEntered(null, Double.parseDouble(kmField.getValue().toString()), _facetRecord); } catch(NumberFormatException nfe) { GWT.log("entered string cannot be parsed to double."); } } } }); kmField.addKeyPressHandler(new KeyPressHandler(){ @Override public void onKeyPress(KeyPressEvent kpe) { if (kpe.getKeyName().equals("Enter")) { kmField.blurItem(); } } }); // TODO: i18n Now add all the validators, formatters, editors/parsers etc. form.setFields(kmField); form.setTitle(""); form.setTitlePrefix(""); form.setTitleSuffix(""); form.setTitleWidth(0); form.setWidth(40); form.setHeight(height); // PlusButton shall ask service for next available cs. Button plusButton = new Button("+"); plusButton.setWidth(10); plusButton.setHeight(height); plusButton.addClickHandler(new com.smartgwt.client.widgets.events.ClickHandler() { public void onClick(ClickEvent evt) { spinnerValueEntered(null, currentValue + 0.1d, _facetRecord); } }); this.addMember(minusButton); this.addMember(form); this.addMember(plusButton); this.setHeight(height*2); this.setWidth(60); } } /** * Create and configure the Grid to display. * @return ListGrid with Themes and related controls inside. */ @Override protected ListGrid createGrid() { 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)) { /* TODO: if (facetRecord.getTheme().getActive() != 1) { spinnerItem.disable(); } */ /* // To have visual representation of synchronous // navigation or not per theme, snip: if (synchronCrossSectionThemes.contains (themeHash (facetRecord.getTheme()))) { spinnerItem.setTextBoxStyle("bgBlueDark"); } */ return new KmSpinner(facetRecord); } 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(), 65); 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. */ 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. */ protected boolean areAreaCompatible(Theme a, Theme b) { if (a.equals(b)) { return false; } return (a.getFacet().equals("cross_section") || a.getFacet().equals("cross_section_water_line")) && (b.getFacet().equals("cross_section") || b.getFacet().equals("cross_section_water_line")); } /** * True if context menu should contain 'create area' submenu on * this theme. */ protected boolean canArea(Theme a) { return a.getFacet().equals("cross_section") || a.getFacet().equals("cross_section_water_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() { public void onClick(MenuItemClickEvent evt) { synchronCrossSectionThemes.remove (themeHash); } }); } else { synchronNavigationMenuItem.setTitle(MSG.chart_themepanel_synchron()); synchronNavigationMenuItem.addClickHandler(new ClickHandler() { 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 :