package org.azavea.otm.fields; import android.app.Activity; import android.content.Intent; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.Button; import android.widget.LinearLayout; import android.widget.TextView; import android.widget.Toast; import org.azavea.helpers.JSONHelper; import org.azavea.helpers.Logger; import org.azavea.otm.App; import org.azavea.otm.R; import org.azavea.otm.data.Plot; import org.azavea.otm.data.UDFCollectionDefinition; import org.azavea.otm.ui.TreeEditDisplay; import org.azavea.otm.ui.UDFCollectionCreateActivity; import org.azavea.otm.ui.UDFCollectionEditActivity; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import static com.google.common.collect.Collections2.filter; import static com.google.common.collect.Collections2.transform; import static com.google.common.collect.Iterables.concat; import static com.google.common.collect.Iterables.partition; import static com.google.common.collect.Lists.newArrayList; /** * UDF Collections are odd and don't really fit into the ecosystem of other fields. * <p> * A UDF collection has one or more keys, and each entry on a Model has a list of zero or more * entries per UDF collection field * <p> * Each UDFCollectionFieldGroup can have multiple UDF collection fields that are shown in that group * All collection entries are merged together by a sort key, which *must* exist on every collection * UDF in the group. * <p> * It isn't known until we pull the values from the Plot and Tree models how many rows we * will need to display. * As such, it doesn't make it's Field(s) until it is rendered, and the number of Fields may change */ public class UDFCollectionFieldGroup extends FieldGroup { private static final int NUM_FIELDS_PER_CLICK = 3; private final String title; private final String sortKey; private final LinkedHashMap<String, UDFCollectionDefinition> udfDefinitions = new LinkedHashMap<>(); private final LinkedHashMap<String, UDFCollectionDefinition> editableUdfDefinitions = new LinkedHashMap<>(); private final List<String> fieldKeys; private ViewGroup fieldContainer; private List<UDFCollectionValueField> fields; public UDFCollectionFieldGroup(JSONObject groupDefinition, Map<String, JSONObject> fieldDefinitions) throws JSONException { title = groupDefinition.optString("header"); sortKey = groupDefinition.optString("sort_key"); fieldKeys = JSONHelper.jsonStringArrayToList(groupDefinition.getJSONArray("collection_udf_keys")); for (String key : fieldKeys) { if (fieldDefinitions.containsKey(key)) { final UDFCollectionDefinition udfDef = new UDFCollectionDefinition(fieldDefinitions.get(key)); udfDefinitions.put(key, udfDef); if (udfDef.isWritable()) { editableUdfDefinitions.put(key, udfDef); } } } } /** * Render a field group and its child fields for viewing */ @Override public View renderForDisplay(LayoutInflater inflater, Plot plot, Activity activity, ViewGroup parent) { return render(inflater, plot, activity, parent, DisplayMode.VIEW); } /** * Render a field group and its child fields for editing */ @Override public View renderForEdit(LayoutInflater inflater, Plot plot, Activity activity, ViewGroup parent) { return render(inflater, plot, activity, parent, DisplayMode.EDIT); } @Override public void receiveActivityResult(int resultCode, Intent data, Activity activity) { boolean shouldUpdate = false; for (String key : editableUdfDefinitions.keySet()) { if (data.getExtras().containsKey(key)) { final String json = data.getStringExtra(key); final UDFCollectionDefinition udfDef = editableUdfDefinitions.get(key); final JSONObject value; try { value = new JSONObject(json); } catch (JSONException e) { Logger.error("Error parsing JSON passed as activity result", e); continue; } shouldUpdate = true; // The presence of a tag tells us if this is an edit to an existing field or an add final UDFCollectionValueField field = new UDFCollectionValueField(udfDef, sortKey, value); if (data.getExtras().containsKey(UDFCollectionEditActivity.TAG)) { final int tag = data.getIntExtra(UDFCollectionEditActivity.TAG, -1); if (tag == -1 || fields.isEmpty()) { Logger.warning("Invalid tag for UDF, ignoring it"); } else { // If we received a modified field, remove the old field and add it back with new data fields = newArrayList(concat(filter(fields, f -> f.getTag() != tag), newArrayList(field))); } } else { fields.add(field); } } } if (shouldUpdate) { rerenderEditFields(activity); } } @Override public void update(Plot plot) { Map<String, JSONArray> collectionUdfArrays = new HashMap<>(fieldKeys.size()); for (String collectionKey : fieldKeys) { collectionUdfArrays.put(collectionKey, new JSONArray()); } if (fields != null) { for (Field field : fields) { String collectionKey = field.key; if (collectionUdfArrays.containsKey(collectionKey)) { JSONArray udfData = collectionUdfArrays.get(collectionKey); udfData.put(field.getEditedValue()); } else { Logger.warning("Impossible state - UDFCollectionGroup has a field not in it's fieldKeys"); } } } for (Map.Entry<String, JSONArray> entry : collectionUdfArrays.entrySet()) { JSONArray collectionValues = entry.getValue(); if (collectionValues.length() > 0) { try { plot.setValueForKey(entry.getKey(), collectionValues); } catch (Exception e) { Logger.error(e); Toast.makeText(App.getAppInstance(), R.string.udf_save_error, Toast.LENGTH_SHORT).show(); } } } } /** * Handles the common functionality between rendering collection UDFs for edit and for display * Dispatches to helpers for those parts which are different based on DisplayMode */ private View render(LayoutInflater inflater, Plot plot, Activity activity, ViewGroup parent, DisplayMode mode) { if (getCurrentUdfDefinitions(mode).isEmpty()) { // If there are no udfDefinitions, we shouldn't show the group at all return null; } final View groupContainer = inflater.inflate(R.layout.collection_udf_field_group, parent, false); final TextView groupLabel = (TextView) groupContainer.findViewById(R.id.group_name); groupLabel.setText(title); final View buttonContainer = groupContainer.findViewById(R.id.udf_button_container); final Button button = (Button) groupContainer.findViewById(R.id.udf_button); fields = getFields(plot, mode); fieldContainer = (LinearLayout) groupContainer.findViewById(R.id.fields); final Collection<View> fieldViews = getViewsForFields(inflater, activity, mode); if (mode == DisplayMode.VIEW) { setupFieldsForDisplay(inflater, buttonContainer, button, fieldViews); } else { setupFieldsForEdit(button, fieldViews, activity); } return groupContainer; } private void setupFieldsForDisplay(LayoutInflater inflater, View buttonContainer, Button button, Collection<View> fieldViews) { button.setText(R.string.load_more_collection_udf); if (fieldViews.isEmpty()) { inflater.inflate(R.layout.collection_udf_empty, fieldContainer); buttonContainer.setVisibility(View.GONE); } else { // We only want to show so many fields at a time, and add more when a "Show More" button is clicked final List<List<View>> fieldViewGroups = newArrayList(partition(fieldViews, NUM_FIELDS_PER_CLICK)); addFieldsToGroup(fieldViewGroups.remove(0), fieldContainer); if (fieldViewGroups.isEmpty()) { buttonContainer.setVisibility(View.GONE); } else { button.setOnClickListener(v -> { addFieldsToGroup(fieldViewGroups.remove(0), fieldContainer); if (fieldViewGroups.isEmpty()) { buttonContainer.setVisibility(View.GONE); } }); } } } private void setupFieldsForEdit(Button button, Collection<View> fieldViews, Activity activity) { button.setText(R.string.udf_create_new); for (View view : fieldViews) { fieldContainer.addView(view); } button.setOnClickListener(v -> { Intent udfCreator = new Intent(App.getAppInstance(), UDFCollectionCreateActivity.class); udfCreator.putParcelableArrayListExtra(UDFCollectionCreateActivity.UDF_DEFINITIONS, newArrayList(editableUdfDefinitions.values())); activity.startActivityForResult(udfCreator, TreeEditDisplay.FIELD_ACTIVITY_REQUEST_CODE); }); } private void rerenderEditFields(Activity activity) { fieldContainer.removeAllViews(); Collections.sort(fields); LayoutInflater inflater = activity.getLayoutInflater(); Collection<View> fieldViews = getViewsForFields(inflater, activity, DisplayMode.EDIT); for (View view : fieldViews) { fieldContainer.addView(view); } } private List<UDFCollectionValueField> getFields(Plot plot, DisplayMode mode) { final List<UDFCollectionValueField> fieldsList = newArrayList(); for (UDFCollectionDefinition udfDef : getCurrentUdfDefinitions(mode).values()) { JSONArray collectionValues = (JSONArray) plot.getValueForKey(udfDef.getCollectionKey()); if (!JSONObject.NULL.equals(collectionValues)) { for (int i = 0; i < collectionValues.length(); i++) { JSONObject value = collectionValues.optJSONObject(i); fieldsList.add(new UDFCollectionValueField(udfDef, sortKey, value)); } } } return fieldsList; } private Map<String, UDFCollectionDefinition> getCurrentUdfDefinitions(DisplayMode mode) { return (mode == DisplayMode.VIEW) ? udfDefinitions : editableUdfDefinitions; } private Collection<View> getViewsForFields(LayoutInflater inflater, Activity activity, DisplayMode mode) { Collections.sort(fields); return filter(transform(fields, field -> { try { if (mode == DisplayMode.VIEW) { return field.renderForDisplay(inflater, activity, fieldContainer); } else { return field.renderForEdit(inflater, activity, fieldContainer); } } catch (JSONException e) { Logger.error("Error creating collection UDF view", e); return null; } }), view -> view != null); } private void addFieldsToGroup(List<View> fieldGroup, ViewGroup parent) { for (View fieldView : fieldGroup) { parent.addView(fieldView); } } }