org.opendatakit.aggregate.client.popups.VisualizationPopup.java Source code

Java tutorial

Introduction

Here is the source code for org.opendatakit.aggregate.client.popups.VisualizationPopup.java

Source

/*
 * Copyright (C) 2011 University of Washington
 *
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
 * use this file except in compliance with the License. You may obtain a copy of
 * the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations under
 * the License.
 */

package org.opendatakit.aggregate.client.popups;

import java.util.ArrayList;
import java.util.HashMap;

import org.opendatakit.aggregate.client.AggregateUI;
import org.opendatakit.aggregate.client.FilterSubTab;
import org.opendatakit.aggregate.client.SecureGWT;
import org.opendatakit.aggregate.client.form.KmlSettings;
import org.opendatakit.aggregate.client.submission.Column;
import org.opendatakit.aggregate.client.submission.SubmissionUI;
import org.opendatakit.aggregate.client.table.BinaryPopupClickHandler;
import org.opendatakit.aggregate.client.widgets.AggregateButton;
import org.opendatakit.aggregate.client.widgets.ClosePopupButton;
import org.opendatakit.aggregate.client.widgets.ColumnListBox;
import org.opendatakit.aggregate.client.widgets.EnumListBox;
import org.opendatakit.aggregate.client.widgets.KmlSettingListBox;
import org.opendatakit.aggregate.client.widgets.RepeatViewButton;
import org.opendatakit.aggregate.constants.common.ChartType;
import org.opendatakit.aggregate.constants.common.GeoPointConsts;
import org.opendatakit.aggregate.constants.common.UIConsts;
import org.opendatakit.common.web.constants.BasicConsts;
import org.opendatakit.common.web.constants.HtmlConsts;

import com.google.gwt.event.dom.client.ChangeEvent;
import com.google.gwt.event.dom.client.ChangeHandler;
import com.google.gwt.event.dom.client.ClickEvent;
import com.google.gwt.event.dom.client.ClickHandler;
import com.google.gwt.maps.client.HasMap;
import com.google.gwt.maps.client.MapOptions;
import com.google.gwt.maps.client.MapTypeId;
import com.google.gwt.maps.client.MapWidget;
import com.google.gwt.maps.client.base.InfoWindow;
import com.google.gwt.maps.client.base.LatLng;
import com.google.gwt.maps.client.event.Event;
import com.google.gwt.maps.client.event.HasMouseEvent;
import com.google.gwt.maps.client.event.MouseEventCallback;
import com.google.gwt.maps.client.overlay.Marker;
import com.google.gwt.user.client.Window;
import com.google.gwt.user.client.rpc.AsyncCallback;
import com.google.gwt.user.client.ui.FlexTable;
import com.google.gwt.user.client.ui.FlowPanel;
import com.google.gwt.user.client.ui.HTMLPanel;
import com.google.gwt.user.client.ui.Image;
import com.google.gwt.user.client.ui.Label;
import com.google.gwt.user.client.ui.RadioButton;
import com.google.gwt.user.client.ui.SimplePanel;
import com.google.gwt.user.client.ui.Widget;
import com.google.gwt.visualization.client.AbstractDataTable.ColumnType;
import com.google.gwt.visualization.client.DataTable;
import com.google.gwt.visualization.client.VisualizationUtils;
import com.google.gwt.visualization.client.visualizations.Table;
import com.google.gwt.visualization.client.visualizations.corechart.BarChart;
import com.google.gwt.visualization.client.visualizations.corechart.Options;
import com.google.gwt.visualization.client.visualizations.corechart.PieChart;
import com.google.gwt.visualization.client.visualizations.corechart.PieChart.PieOptions;

public final class VisualizationPopup extends AbstractPopupBase {

    private static final String TABULATION_TXT = "<h4 id=\"form_name\">Tabulation Method:</h4>";
    private static final String TALLY_EXP_BEGIN = "COUNT: Count occurences of Answer Values from ";
    private static final String TALLY_EXP_END = ". (selected column above)";
    private static final String SUM_COLUMNS_TXT = "SUM: Sum numeric values from column: ";
    private static final String SUM_COLUMNS_BEGIN = " grouped by selected column above [e.g. How many ";
    private static final String SUM_COLUMNS_MIDDLE = " per ";
    private static final String SUM_COLUMNS_END = "?]";
    private static final String GEOPOINT_TOOLTIP = "Geopoint field to map";
    private static final String GEOPOINT_BALLOON = "Choose the geopoint field to map.";

    private static final String TYPE_TXT = "<h2 id=\"form_name\">Type:</h2>";
    private static final String COLUMN_TXT = "<h2 id=\"form_name\">" + HtmlConsts.TAB + "Column to Visualize:</h2>";
    private static final String GPS_TXT = "<h2 id=\"form_name\">" + HtmlConsts.TAB + "GeoPoint to Map:</h2>";

    private static int VIZ_TYPE_TEXT = 0;
    private static int VIZ_TYPE_LIST = 1;
    private static int COLUMN_TEXT = 2;
    private static int COLUMN_LIST = 3;
    private static int BUTTON = 5;

    private static int CLOSE = 4;

    private static int VALUE_TEXT = 0;
    private static int VALUE_LIST = 1;

    private static int TALLY_CHOICE = 0;

    private static int SUM_CHOICE = 0;
    private static int SUM_CHOICE_COLUMN = 1;
    private static int SUM_CHOICE_TXT = 2;

    private static final String RADIO_GROUP = "vizRadioGroup";
    private static final String RESIZE_UNITS = "px";

    private static final String VIZ_TYPE_TOOLTIP = "Type of Visualization";
    private static final String VIZ_TYPE_BALLOON = "Choose whether you would like a pie chart, bar graph, or map.";

    private final ArrayList<Column> headers;
    private final ArrayList<SubmissionUI> submissions;

    private final FlexTable typeControlBar;
    private final EnumListBox<ChartType> chartType;

    private final ColumnListBox columnList;
    private final ColumnListBox dataList;
    private final KmlSettingListBox geoPoints;

    private boolean chartApiLoaded;

    private final String formId;

    private final AggregateButton executeButton;
    private final SimplePanel chartPanel;

    private RadioButton tallyOccurRadio;
    private RadioButton sumColumnsRadio;
    private Label sumRadioTxt;
    private InfoWindow infoWindow = null;

    // track whether the map marker was clicked or not.
    private boolean mapMarkerClicked;

    public VisualizationPopup(FilterSubTab filterSubTab) {
        super();

        formId = filterSubTab.getDisplayedFilterGroup().getFormId();
        headers = filterSubTab.getSubmissionTable().getHeaders();
        submissions = filterSubTab.getSubmissionTable().getSubmissions();

        chartType = new EnumListBox<ChartType>(ChartType.values(), VIZ_TYPE_TOOLTIP, VIZ_TYPE_BALLOON);
        chartType.addChangeHandler(new ChangeHandler() {
            @Override
            public void onChange(ChangeEvent event) {
                updateUIoptions();
            }
        });

        columnList = new ColumnListBox(headers, false, true, "Column to Graph",
                "Select the column you wish to graph.");
        columnList.addChangeHandler(new ColumnChangeHandler());
        dataList = new ColumnListBox(headers, false, true, "Column to get data values from",
                "Select the column to get the numerical values from.");
        dataList.addChangeHandler(new ColumnChangeHandler());
        geoPoints = new KmlSettingListBox(GEOPOINT_TOOLTIP, GEOPOINT_BALLOON);

        // The Maps API is always loaded.

        chartApiLoaded = false;
        VisualizationUtils.loadVisualizationApi(new Runnable() {
            public void run() {
                chartApiLoaded = true;
                updateUIoptions();
            }
        }, PieChart.PACKAGE, Table.PACKAGE);

        SecureGWT.getFormService().getGpsCoordnates(formId, new AsyncCallback<KmlSettings>() {
            public void onFailure(Throwable caught) {
                AggregateUI.getUI().reportError(caught);
            }

            public void onSuccess(KmlSettings result) {
                geoPoints.updateValues(result.getGeopointNodes());
            }
        });

        // create radio button
        // NOTE: need to apply the click handler to both because can't use value
        // change. Because browser limitations prevent ValueChangeEvents from being
        // sent when the radio button is cleared as a side effect of another in the
        // group being clicked.

        FlexTable tallyTable = new FlexTable();
        tallyOccurRadio = new RadioButton(RADIO_GROUP, BasicConsts.EMPTY_STRING);
        tallyOccurRadio.addClickHandler(new RadioChangeClickHandler());
        tallyOccurRadio.setValue(true);
        tallyTable.setWidget(0, TALLY_CHOICE, tallyOccurRadio);

        FlexTable sumTable = new FlexTable();
        sumColumnsRadio = new RadioButton(RADIO_GROUP, SUM_COLUMNS_TXT);
        sumColumnsRadio.addClickHandler(new RadioChangeClickHandler());
        sumRadioTxt = new Label(BasicConsts.EMPTY_STRING);
        sumTable.setWidget(1, SUM_CHOICE, sumColumnsRadio);
        sumTable.setWidget(1, SUM_CHOICE_COLUMN, dataList);
        sumTable.setWidget(1, SUM_CHOICE_TXT, sumRadioTxt);

        executeButton = new AggregateButton(BasicConsts.EMPTY_STRING, "Execute the Vizualization",
                "Create the selected Vizualization.");
        executeButton.addClickHandler(new ExecuteVisualization());

        typeControlBar = new FlexTable();
        typeControlBar.setHTML(0, VIZ_TYPE_TEXT, TYPE_TXT);
        typeControlBar.setWidget(0, VIZ_TYPE_LIST, chartType);
        typeControlBar.setHTML(0, COLUMN_TEXT, COLUMN_TXT);
        typeControlBar.setWidget(0, COLUMN_LIST, columnList);
        typeControlBar.setWidget(0, BUTTON, executeButton);

        FlexTable topSelectionRow = new FlexTable();
        topSelectionRow.addStyleName("stretch_popup_header");
        topSelectionRow.setWidget(0, 0, typeControlBar);
        topSelectionRow.setWidget(0, CLOSE, new ClosePopupButton(this));
        topSelectionRow.getCellFormatter().addStyleName(0, CLOSE, "popup_close_cell");

        FlexTable tabulationBar = new FlexTable();
        tabulationBar.setHTML(0, VALUE_TEXT, TABULATION_TXT);
        tabulationBar.setWidget(0, VALUE_LIST, tallyTable);
        tabulationBar.setWidget(1, VALUE_LIST, sumTable);

        FlexTable bottomSelectionRow = new FlexTable();
        bottomSelectionRow.addStyleName("stretch_popup_header");
        bottomSelectionRow.setWidget(0, 0, tabulationBar);

        // setup the window size
        chartPanel = new SimplePanel();
        Integer height = (Window.getClientHeight() * 5) / 6;
        Integer width = (Window.getClientWidth() * 5) / 6;
        chartPanel.setHeight(height.toString() + RESIZE_UNITS);
        chartPanel.setWidth(width.toString() + RESIZE_UNITS);

        FlowPanel layoutPanel = new FlowPanel();
        layoutPanel.add(topSelectionRow);
        layoutPanel.add(bottomSelectionRow);
        layoutPanel.add(chartPanel);

        setWidget(layoutPanel);
        chartType.setItemSelected(0, true);
        updateUIoptions();
        updateColumnGraphingDesc();
    }

    private void updateUIoptions() {
        ChartType selected = chartType.getSelectedEnumValue();
        executeButton.setHTML(selected.getButtonText());
        if (selected.equals(ChartType.MAP)) {
            typeControlBar.setHTML(0, COLUMN_TEXT, GPS_TXT);
            typeControlBar.setWidget(0, COLUMN_LIST, geoPoints);

            // disable data section
            tallyOccurRadio.setEnabled(false);
            sumColumnsRadio.setEnabled(false);
            dataList.setEnabled(false);
        } else { // must be a chart if not MAP
            typeControlBar.setHTML(0, COLUMN_TEXT, COLUMN_TXT);
            typeControlBar.setWidget(0, COLUMN_LIST, columnList);

            // enable data section
            tallyOccurRadio.setEnabled(true);
            sumColumnsRadio.setEnabled(true);
            dataList.setEnabled(sumColumnsRadio.getValue());
        }
        center();
    }

    private void updateColumnGraphingDesc() {
        String vizColumnTxt = columnList.getSelectedColumn().getDisplayHeader();
        String sumColumnTxt = dataList.getSelectedColumn().getDisplayHeader();

        tallyOccurRadio.setText(TALLY_EXP_BEGIN + vizColumnTxt + TALLY_EXP_END);
        sumRadioTxt.setText(SUM_COLUMNS_BEGIN + sumColumnTxt + SUM_COLUMNS_MIDDLE + vizColumnTxt + SUM_COLUMNS_END);
    }

    private DataTable createDataTable() {
        Column firstDataValue = columnList.getSelectedColumn();
        Column secondDataValue = dataList.getSelectedColumn();

        boolean tally = tallyOccurRadio.getValue();

        DataTable data = DataTable.create();
        data.addColumn(ColumnType.STRING, firstDataValue.getDisplayHeader());
        if (tally) {
            data.addColumn(ColumnType.NUMBER, "Number of Ocurrences");
        } else {
            data.addColumn(ColumnType.NUMBER, "Sum of " + secondDataValue.getDisplayHeader());
        }

        int firstIndex = 0;
        int secondIndex = 0;
        int index = 0;
        for (Column c : headers) {
            if (c.equals(firstDataValue))
                firstIndex = index;
            if (c.equals(secondDataValue))
                secondIndex = index;
            index++;
        }

        HashMap<String, Double> aggregation = new HashMap<String, Double>();
        for (SubmissionUI s : submissions) {
            String label = s.getValues().get(firstIndex);

            // determine submissions value
            double addend = 0;
            if (tally) {
                addend = 1;
            } else {
                try {
                    addend = Double.parseDouble(s.getValues().get(secondIndex));
                } catch (Exception e) {
                    // move on because we couldn't parse the value
                }
            }

            // update running total
            if (aggregation.containsKey(label)) {
                aggregation.put(label, aggregation.get(label) + addend);
            } else {
                aggregation.put(label, addend);
            }
        }

        // output table
        int i = 0;
        for (String s : aggregation.keySet()) {
            data.addRow();
            data.setValue(i, 0, s);
            data.setValue(i, 1, aggregation.get(s));
            i++;
        }

        return data;
    }

    /**
     * Create pie chart
     *
     * @return
     */
    private PieChart createPieChart() {
        DataTable data = createDataTable();
        PieOptions options = PieChart.createPieOptions();
        options.setWidth(chartPanel.getOffsetWidth());
        options.setHeight(chartPanel.getOffsetHeight());
        options.set3D(true);
        return new PieChart(data, options);
    }

    /**
     * Create bar chart
     *
     * @return
     */
    private BarChart createBarChart() {
        DataTable data = createDataTable();
        Options options = Options.create();
        options.setWidth(chartPanel.getOffsetWidth());
        options.setHeight(chartPanel.getOffsetHeight());
        return new BarChart(data, options);
    }

    private int findGpsIndex(String columnElementKey, Integer columnCode) {
        int index = 0;
        Long columnNum = columnCode.longValue();
        for (Column col : headers) {
            if (col.getColumnEncoding().equals(columnElementKey) && col.getGeopointColumnCode().equals(columnNum)) {
                return index;
            }
            index++;
        }
        return -1;
    }

    private LatLng getLatLonFromSubmission(int latIndex, int lonIndex, SubmissionUI sub) {
        LatLng gpsPoint;
        ArrayList<String> values = sub.getValues();
        try {
            Double lat = Double.parseDouble(values.get(latIndex));
            Double lon = Double.parseDouble(values.get(lonIndex));
            gpsPoint = new LatLng(lat, lon);
        } catch (Exception e) {
            // just set the gps point to null, no need to figure out problem
            gpsPoint = null;
        }
        return gpsPoint;
    }

    private MapWidget createMap() {
        int latIndex = findGpsIndex(geoPoints.getElementKey(), GeoPointConsts.GEOPOINT_LATITUDE_ORDINAL_NUMBER);
        int lonIndex = findGpsIndex(geoPoints.getElementKey(), GeoPointConsts.GEOPOINT_LONGITUDE_ORDINAL_NUMBER);

        // check to see if we have lat & long, if not display erro
        if (latIndex < 0 || lonIndex < 0) {
            String error = "ERROR:";
            if (latIndex < 0) {
                error = error + " The Latitude Coordinate is NOT included in the Filter.";
            }
            if (lonIndex < 0) {
                error = error + " The Longitude Coordinate is NOT included in the Filter.";
            }

            Window.alert(error);
            return null;
        }

        // create a center point, stop at the first gps point found
        LatLng center = new LatLng(0.0, 0.0);
        for (SubmissionUI sub : submissions) {
            LatLng gpsPoint = getLatLonFromSubmission(latIndex, lonIndex, sub);
            if (gpsPoint != null) {
                center = gpsPoint;
                break;
            }
        }

        // create mapping area
        final MapOptions options = new MapOptions();
        options.setCenter(center);
        MapTypeId id = new MapTypeId();
        options.setMapTypeId(id.getRoadmap());
        options.setZoom(6);
        options.setMapTypeControl(true);
        options.setNavigationControl(true);
        options.setScaleControl(true);
        final MapWidget mapWidget = new MapWidget(options);
        mapWidget.setSize("100%", "100%");

        final HasMap map = mapWidget.getMap();

        // create the markers
        for (SubmissionUI sub : submissions) {
            LatLng gpsPoint = getLatLonFromSubmission(latIndex, lonIndex, sub);
            if (gpsPoint != null) {
                final Marker marker = new Marker();
                marker.setPosition(gpsPoint);
                marker.setMap(map);

                // marker needs to be added to the map before calling
                // InfoWindow.open(marker, ...)
                final SubmissionUI tmpSub = sub;
                Event.addListener(marker, "mouseover", new MouseEventCallback() {

                    @Override
                    public void callback(HasMouseEvent event) {
                        if (infoWindow != null) {
                            infoWindow.close();
                        }
                        infoWindow = new InfoWindow();
                        InfoContentSubmission w = createInfoWindowWidget(tmpSub);
                        HTMLPanel container = new HTMLPanel("<div></div>");
                        container.add(w);
                        infoWindow.setContent(container.getElement().getInnerHTML());
                        infoWindow.open(map, marker);
                    }
                });

                Event.addListener(marker, "mouseout", new MouseEventCallback() {

                    @Override
                    public void callback(HasMouseEvent event) {
                        if (!mapMarkerClicked) {
                            if (infoWindow != null) {
                                infoWindow.close();
                                infoWindow = null;
                            }
                        }
                        mapMarkerClicked = false;
                    }
                });

                Event.addListener(marker, "click", new MouseEventCallback() {

                    @Override
                    public void callback(HasMouseEvent event) {
                        mapMarkerClicked = true;
                    }

                });
            }
        }
        return mapWidget;
    }

    public class InfoContentSubmission extends FlexTable {

        public InfoContentSubmission(ArrayList<Column> tableHeaders, SubmissionUI row) {

            addStyleName("infoTable");
            getElement().setId("submission_info_table");

            // setup header
            ArrayList<String> values = row.getValues();
            for (int headerIndex = 0; headerIndex < tableHeaders.size(); ++headerIndex) {
                Column column = tableHeaders.get(headerIndex);

                Label txt = new Label(column.getDisplayHeader());
                txt.setStyleName("infoTableLabel");
                setWidget(headerIndex, 0, txt);

                String value = values.get(headerIndex);
                switch (column.getUiDisplayType()) {
                case BINARY:
                    if (value == null) {
                        Label val = new Label(BasicConsts.EMPTY_STRING);
                        setWidget(headerIndex, 1, val);
                    } else {
                        Image image = new Image(value + UIConsts.PREVIEW_SET);
                        image.addClickHandler(new BinaryPopupClickHandler(value, false));
                        image.setStyleName(UIConsts.PREVIEW_IMAGE_STYLENAME);
                        setWidget(headerIndex, 1, image);
                    }
                    break;
                case REPEAT:
                    if (value == null) {
                        Label val = new Label(BasicConsts.EMPTY_STRING);
                        setWidget(headerIndex, 1, val);
                    } else {
                        RepeatViewButton repeat = new RepeatViewButton(value);
                        setWidget(headerIndex, 1, repeat);
                    }
                    break;
                default: {
                    Label val = new Label(value);
                    setWidget(headerIndex, 1, val);
                }
                }
                getWidget(headerIndex, 1).addStyleName("infoTableData");
            }
        }
    }

    private InfoContentSubmission createInfoWindowWidget(SubmissionUI submission) {
        return new InfoContentSubmission(headers, submission);
    }

    private class ExecuteVisualization implements ClickHandler {

        @Override
        public void onClick(ClickEvent event) {

            // verify modules are loaded
            if (!chartApiLoaded) {
                Window.alert("Modules are not loaded yet, please try again!");
                return;
            }

            Widget chart;
            switch (chartType.getSelectedEnumValue()) {
            case MAP:
                chart = createMap();
                break;
            case PIE_CHART:
                chart = createPieChart();
                break;
            case BAR_GRAPH:
                chart = createBarChart();
                break;
            default:
                chart = null;
            }
            chartPanel.clear();
            chartPanel.add(chart);
        }

    }

    private class RadioChangeClickHandler implements ClickHandler {

        @Override
        public void onClick(ClickEvent event) {
            dataList.setEnabled(sumColumnsRadio.getValue());
        }
    }

    private class ColumnChangeHandler implements ChangeHandler {
        @Override
        public void onChange(ChangeEvent event) {
            updateColumnGraphingDesc();
        }
    }
}