package de.blau.android.propertyeditor; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Set; import java.util.TreeMap; import java.util.TreeSet; import android.content.Context; import android.content.DialogInterface; import android.os.Build; import android.os.Bundle; import android.support.v4.app.Fragment; import android.support.v4.app.FragmentManager; import android.support.v4.app.FragmentTransaction; import android.support.v7.app.AlertDialog; import android.support.v7.app.AlertDialog.Builder; import android.support.v7.app.AppCompatActivity; import android.text.Editable; import android.text.InputType; import android.text.TextUtils.TruncateAt; import android.text.TextWatcher; import android.text.style.CharacterStyle; import android.text.style.MetricAffectingSpan; import android.util.AttributeSet; import android.util.Log; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; import android.widget.AdapterView; import android.widget.AdapterView.OnItemClickListener; import android.widget.ArrayAdapter; import android.widget.AutoCompleteTextView; import android.widget.CheckBox; import android.widget.CompoundButton; import android.widget.CompoundButton.OnCheckedChangeListener; import android.widget.EditText; import android.widget.LinearLayout; import android.widget.ScrollView; import de.blau.android.App; import de.blau.android.HelpViewer; import de.blau.android.R; import de.blau.android.javascript.EvalCallback; import de.blau.android.names.Names; import de.blau.android.names.Names.NameAndTags; import de.blau.android.osm.OsmElement; import de.blau.android.osm.OsmElement.ElementType; import de.blau.android.osm.Tags; import de.blau.android.prefs.Preferences; import de.blau.android.presets.Preset; import de.blau.android.presets.Preset.PresetItem; import de.blau.android.presets.Preset.PresetKeyType; import de.blau.android.presets.ValueWithCount; import de.blau.android.util.BaseFragment; import de.blau.android.util.ClipboardUtils; import de.blau.android.util.KeyValue; import de.blau.android.util.NetworkStatus; import de.blau.android.util.SavingHelper; import de.blau.android.util.Snack; import de.blau.android.util.StreetTagValueAdapter; import de.blau.android.util.StringWithDescription; import de.blau.android.util.Util; import de.blau.android.views.CustomAutoCompleteTextView; public class TagEditorFragment extends BaseFragment implements PropertyRows, EditorUpdate { private static final String RECENTPRESETS_FRAGMENT = "recentpresets_fragment"; private static final String SAVEDTAGS = "SAVEDTAGS"; private static final String ELEMENTS = "elements"; private static final String DISPLAY_MR_UPRESETS = "displayMRUpresets"; private static final String FOCUS_ON_KEY = "focusOnKey"; private static final String APPLY_LAST_ADDRESS_TAGS = "applyLastAddressTags"; private static final String EXTRA_TAGS = "extraTags"; private static final String HTTP_PREFIX = "http://"; private static final String DEBUG_TAG = TagEditorFragment.class.getSimpleName(); private SavingHelper<LinkedHashMap<String,String>> savingHelper = new SavingHelper<LinkedHashMap<String,String>>(); private static SelectedRowsActionModeCallback tagSelectedActionModeCallback = null; private static final Object actionModeCallbackLock = new Object(); private PresetItem autocompletePresetItem = null; private Names names = null; private boolean loaded = false; private String[] types; private long[] osmIds; private Preferences prefs = null; private OsmElement[] elements = null; private LayoutInflater inflater = null; private NameAdapters nameAdapters = null; /** * saves any changed fields on onPause */ private LinkedHashMap<String,ArrayList<String>> savedTags = null; /** * per tag preset association */ private HashMap<String,PresetItem> tags2Preset = new HashMap<String,PresetItem>(); /** * per tag preset association */ private ArrayList<PresetItem> secondaryPresets = new ArrayList<PresetItem>(); /** * selective copy of tags */ Map<String, String> copiedTags = null; private FormUpdate formUpdate; private PresetFilterUpdate presetFilterUpdate; /** * Interface for handling the key:value pairs in the TagEditor. * @author Andrew Gregory */ interface KeyValueHandler { void handleKeyValue(final EditText keyEdit, final EditText valueEdit, final ArrayList<String> tagValues); } /** * Perform some processing for each key:value pair in the TagEditor. * @param handler The handler that will be called for each key:value pair. */ private void processKeyValues(final KeyValueHandler handler) { LinearLayout rowLayout = (LinearLayout) getOurView(); processKeyValues(rowLayout, handler); } /** * Perform some processing for each key:value pair in the TagEditor. * @param handler The handler that will be called for each key:value pair. */ private void processKeyValues(LinearLayout rowLayout, final KeyValueHandler handler) { final int size = rowLayout.getChildCount(); for (int i = 0; i < size; ++i) { View view = rowLayout.getChildAt(i); TagEditRow row = (TagEditRow)view; handler.handleKeyValue(row.keyEdit, row.valueEdit, row.tagValues); } } /** * @param applyLastAddressTags * @param focusOnKey * @param displayMRUpresets */ static public TagEditorFragment newInstance(OsmElement[] elements, ArrayList<LinkedHashMap<String,String>> tags, boolean applyLastAddressTags, String focusOnKey, boolean displayMRUpresets, HashMap<String,String> extraTags) { TagEditorFragment f = new TagEditorFragment(); Bundle args = new Bundle(); args.putSerializable(ELEMENTS, elements); args.putSerializable("tags", tags); args.putSerializable(APPLY_LAST_ADDRESS_TAGS, Boolean.valueOf(applyLastAddressTags)); args.putSerializable(FOCUS_ON_KEY, focusOnKey); args.putSerializable(DISPLAY_MR_UPRESETS, Boolean.valueOf(displayMRUpresets)); args.putSerializable(EXTRA_TAGS, extraTags); f.setArguments(args); // f.setShowsDialog(true); return f; } @Override public void onAttachToContext(Context context) { Log.d(DEBUG_TAG, "onAttachToContext"); try { nameAdapters = (NameAdapters) context; formUpdate = (FormUpdate) context; presetFilterUpdate = (PresetFilterUpdate) context; } catch (ClassCastException e) { throw new ClassCastException(context.toString() + " must implement NameAdapters. FormUpdate and PresetFilterUpdate"); } setHasOptionsMenu(true); getActivity().supportInvalidateOptionsMenu(); } @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); Log.d(DEBUG_TAG, "onCreate"); } /** * display member elements of the relation if any * @param members */ @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { ScrollView rowLayout = null; boolean applyLastAddressTags = false; String focusOnKey = null; boolean displayMRUpresets = false; if (savedInstanceState == null) { // No previous state to restore - get the state from the intent Log.d(DEBUG_TAG, "Initializing from original arguments"); elements = (OsmElement[]) getArguments().getSerializable(ELEMENTS); applyLastAddressTags = ((Boolean) getArguments().getSerializable(APPLY_LAST_ADDRESS_TAGS)).booleanValue(); focusOnKey = (String) getArguments().getSerializable(FOCUS_ON_KEY); displayMRUpresets = ((Boolean) getArguments().getSerializable(DISPLAY_MR_UPRESETS)).booleanValue(); } else { // Restore activity from saved state Log.d(DEBUG_TAG, "Restoring from savedInstanceState"); Object[] tempElements = (Object[]) savedInstanceState.getSerializable(ELEMENTS); elements = new OsmElement[tempElements.length]; for (int i=0;i<tempElements.length;i++) { elements[i] = (OsmElement) tempElements[i]; } @SuppressWarnings("unchecked") Map<String, ArrayList<String>> temp = (Map<String, ArrayList<String>>) savedInstanceState.getSerializable(SAVEDTAGS); savedTags = new LinkedHashMap<String, ArrayList<String>>(); savedTags.putAll(temp); } prefs = new Preferences(getActivity()); if (prefs.getEnableNameSuggestions()) { names = App.getNames(getActivity()); } this.inflater = inflater; rowLayout = (ScrollView) inflater.inflate(R.layout.taglist_view, container, false); LinearLayout editRowLayout = (LinearLayout) rowLayout.findViewById(R.id.edit_row_layout); // editRowLayout.setSaveFromParentEnabled(false); editRowLayout.setSaveEnabled(false); types = new String[elements.length]; osmIds = new long[elements.length]; for (int i=0;i< elements.length;i++) { types[i] = elements[i].getName(); osmIds[i] = elements[i].getOsmId(); } LinkedHashMap<String,ArrayList<String>> tags; if (savedTags != null) { // view was destroyed and needs to be recreated with current state Log.d(DEBUG_TAG,"Restoring from instance variable"); tags = savedTags; } else { tags = buildEdits(); } // Log.d(DEBUG_TAG,"element " + element + " tags " + tags); loaded = false; // rowLayout.removeAllViews(); for (Entry<String, ArrayList<String>> pair : tags.entrySet()) { insertNewEdit(editRowLayout, pair.getKey(), pair.getValue(), -1); } loaded = true; TagEditRow row = ensureEmptyRow(editRowLayout); if (getUserVisibleHint()) { // don't request focus if we are not visible Log.d(DEBUG_TAG,"is visible"); row.keyEdit.requestFocus(); row.keyEdit.dismissDropDown(); if (focusOnKey != null) { focusOnValue(editRowLayout,focusOnKey); } else { focusOnEmptyValue(editRowLayout); // probably never actually works } } // if (applyLastAddressTags) { loadEdits(editRowLayout,Address.predictAddressTags(getActivity(), getType(),getOsmId(), ((StreetTagValueAdapter)nameAdapters.getStreetNameAdapter(null)).getElementSearch(), getKeyValueMap(editRowLayout,false), Address.DEFAULT_HYSTERESIS)); if (getUserVisibleHint()) { if (!focusOnValue(editRowLayout,Tags.KEY_ADDR_HOUSENUMBER)) { focusOnValue(editRowLayout,Tags.KEY_ADDR_STREET); } // this could be a bit more refined } } // Add any extra tags that were supplied HashMap<String, String> extraTags = (HashMap<String,String>) getArguments().getSerializable(EXTRA_TAGS); if (extraTags != null) { for (Entry<String,String>e:extraTags.entrySet()) { addTag(editRowLayout,e.getKey(),e.getValue(), true, false); } } updateAutocompletePresetItem(editRowLayout, null); // set preset from initial tags if (displayMRUpresets) { Log.d(DEBUG_TAG,"Adding MRU prests"); FragmentManager fm = getChildFragmentManager(); FragmentTransaction ft = fm.beginTransaction(); Fragment recentPresetsFragment = fm.findFragmentByTag(RECENTPRESETS_FRAGMENT); if (recentPresetsFragment != null) { ft.remove(recentPresetsFragment); } recentPresetsFragment = RecentPresetsFragment.newInstance(elements[0]); // FIXME ft.add(R.id.tag_mru_layout,recentPresetsFragment,RECENTPRESETS_FRAGMENT); ft.commit(); } CheckBox headerCheckBox = (CheckBox) rowLayout.findViewById(R.id.header_tag_selected); headerCheckBox.setOnCheckedChangeListener(new OnCheckedChangeListener() { @Override public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { if (isChecked) { selectAllTags(); } else { deselectAllTags(); } } }); Log.d(DEBUG_TAG,"onCreateView returning"); return rowLayout; } /** * Build the data structure we use to build the edit display * @return */ private LinkedHashMap<String,ArrayList<String>> buildEdits() { @SuppressWarnings("unchecked") ArrayList<LinkedHashMap<String,String>> originalTags = (ArrayList<LinkedHashMap<String,String>>)getArguments().getSerializable("tags"); // LinkedHashMap<String,ArrayList<String>> tags = new LinkedHashMap<String,ArrayList<String>>(); for (LinkedHashMap<String,String>map:originalTags) { for (String key:map.keySet()) { String value = map.get(key); if (!tags.containsKey(key)) { tags.put(key,new ArrayList<String>()); } tags.get(key).add(value); } } // for those keys that don't have a value for each element add an empty string int l = originalTags.size(); for (ArrayList<String>v:tags.values()) { if (v.size() != l) { v.add(""); } } return tags; } @Override public void onStart() { super.onStart(); Log.d(DEBUG_TAG, "onStart"); } @Override public void onResume() { super.onResume(); Log.d(DEBUG_TAG, "onResume"); formUpdate.tagsUpdated(); // kick the form } @Override public void onSaveInstanceState(Bundle outState) { super.onSaveInstanceState(outState); Log.d(DEBUG_TAG, "onSaveInstanceState"); outState.putSerializable(ELEMENTS, elements); outState.putSerializable(SAVEDTAGS, savedTags); } @Override public void onPause() { super.onPause(); Log.d(DEBUG_TAG, "onPause"); savedTags = getKeyValueMap(true); } @Override public void onStop() { super.onStop(); Log.d(DEBUG_TAG, "onStop"); } @Override public void onDestroy() { super.onDestroy(); Log.d(DEBUG_TAG, "onDestroy"); } @Override public void onDestroyView() { super.onDestroyView(); Log.d(DEBUG_TAG, "onDestroyView"); } /** * Creates edits from a SortedMap containing tags (as sequential key-value pairs) * Backwards compatible version */ private void loadEditsSingle(final Map<String, String> tags) { LinearLayout rowLayout = (LinearLayout) getOurView(); LinkedHashMap<String,ArrayList<String>> convertedTags = new LinkedHashMap<String,ArrayList<String>>(); for (String key:tags.keySet()) { ArrayList<String> v = new ArrayList<String>(); v.add(tags.get(key)); convertedTags.put(key, v); } loadEdits(rowLayout, convertedTags); } /** * Creates edits from a SortedMap containing tags (as sequential key-value pairs) */ private void loadEdits(final Map<String, ArrayList<String>> tags) { LinearLayout rowLayout = (LinearLayout) getOurView(); loadEdits(rowLayout, tags); } /** * Creates edits from a SortedMap containing tags (as sequential key-value pairs) */ private void loadEdits(LinearLayout rowLayout, final Map<String, ArrayList<String>> tags) { if (rowLayout == null) { Log.e(DEBUG_TAG, "loadEdits rowLayout null"); return; } loaded = false; rowLayout.removeAllViews(); for (Entry<String, ArrayList<String>> pair : tags.entrySet()) { insertNewEdit(rowLayout, pair.getKey(), pair.getValue(), -1); } loaded = true; ensureEmptyRow(rowLayout); } /** * If tags have changed the autocomplete adapters need to be recalculated on what is the current preset * @param rowLayout * @param preset if null determine best preset from existing tags */ private void updateAutocompletePresetItem(LinearLayout rowLayout, PresetItem preset) { Log.d(DEBUG_TAG,"setting new autocompletePresetItem"); clearPresets(); clearSecondaryPresets(); LinkedHashMap<String, String> allTags = getKeyValueMapSingle(rowLayout,true); if (preset == null) { autocompletePresetItem = Preset.findBestMatch(((PropertyEditor)getActivity()).presets, allTags, true); // FIXME multiselect } else { autocompletePresetItem = preset; } Map<String, String> nonAssigned = addPresetsToTags(autocompletePresetItem, allTags); int nonAssignedCount = nonAssigned.size(); while (nonAssignedCount > 0) { PresetItem nonAssignedPreset = Preset.findBestMatch(((PropertyEditor)getActivity()).presets, nonAssigned, true); if (nonAssignedPreset==null) { // no point in continuing break; } addSecondaryPreset(nonAssignedPreset); nonAssigned = addPresetsToTags(nonAssignedPreset, (LinkedHashMap<String, String>) nonAssigned); nonAssignedCount = nonAssigned.size(); } // update hints for (int i = 0; i < rowLayout.getChildCount()-1; i++) { // don't update empty row at end setHint((TagEditRow) rowLayout.getChildAt(i)); } if (elements.length == 1) { // update element type for preset filter if necessary ElementType newType = elements[0].getType(allTags); ElementType oldType = elements[0].getType(); if (newType != oldType) { presetFilterUpdate.typeUpdated(newType); } } } private void setHint(TagEditRow row) { String aTagKey = row.getKey(); PresetItem preset = getPreset(aTagKey); if (preset != null && aTagKey != null && !aTagKey.equals("")) { // set hints even if value isen't empty String hint = preset.getHint(aTagKey); if (hint != null) { row.valueEdit.setHint(hint); } else if (preset.getKeyType(aTagKey) != PresetKeyType.TEXT) { row.valueEdit.setHint(R.string.tag_autocomplete_value_hint); } else { row.valueEdit.setHint(R.string.tag_value_hint); } if (row.getValue().length() == 0) { String defaultValue = preset.getDefault(aTagKey); if (defaultValue != null) { // row.valueEdit.setText(defaultValue); } } if (!row.same) { row.valueEdit.setHint(R.string.tag_multi_value_hint); // overwrite the above } } } /** * Don't call with null preset * @param preset * @param tags * @return */ private Map<String, String> addPresetsToTags(PresetItem preset, LinkedHashMap<String, String> tags) { LinkedHashMap<String,String> leftOvers = new LinkedHashMap<String,String>(); if (preset!=null) { List<PresetItem> linkedPresetList = preset.getLinkedPresets(true); for (String key:tags.keySet()) { if ( preset.hasKeyValue(key, tags.get(key))) { storePreset(key, preset); } else { boolean found = false; if (linkedPresetList != null){ for (PresetItem linkedPreset:linkedPresetList) { if (linkedPreset.hasKeyValue(key, tags.get(key))) { storePreset(key, linkedPreset); found = true; break; } } } if (!found) { leftOvers.put(key, tags.get(key)); } } } } else { Log.e(DEBUG_TAG,"addPresetsToTags called with null preset"); } return leftOvers; } private PresetItem getPreset(String key) { return tags2Preset.get(key); } private void storePreset(String key, PresetItem preset) { tags2Preset.put(key,preset); } private void clearPresets() { tags2Preset.clear(); } @Override public Map<String,PresetItem> getAllPresets() { return tags2Preset; } private void addSecondaryPreset(PresetItem nonAssignedPreset) { secondaryPresets.add(nonAssignedPreset); } private void clearSecondaryPresets() { secondaryPresets.clear(); } @Override public List<PresetItem> getSecondaryPresets() { return secondaryPresets; } /** * Edits may change the best fitting preset * @param presetItem if null determine best preset from existing tags */ void updateAutocompletePresetItem(PresetItem presetItem) { LinearLayout rowLayout = (LinearLayout) getOurView(); if (rowLayout != null) { updateAutocompletePresetItem(rowLayout, presetItem); } else { Log.d(DEBUG_TAG,"updateAutocompletePresetItem rowLayout null"); } } @Override public void updatePresets() { updateAutocompletePresetItem(null); } @Override public PresetItem getBestPreset() { return autocompletePresetItem; } @Override public void predictAddressTags(boolean allowBlanks) { loadEdits(Address.predictAddressTags(getActivity(), getType(),getOsmId(), ((StreetTagValueAdapter)nameAdapters.getStreetNameAdapter(null)).getElementSearch(), getKeyValueMap(allowBlanks), Address.DEFAULT_HYSTERESIS)); updateAutocompletePresetItem(null); } private ArrayAdapter<String> getKeyAutocompleteAdapter(PresetItem preset, LinearLayout rowLayout, AutoCompleteTextView keyEdit) { // Use a set to prevent duplicate keys appearing Set<String> keys = new HashSet<String>(); if (preset == null && ((PropertyEditor)getActivity()).presets != null) { updateAutocompletePresetItem(rowLayout, null); } if (preset != null) { keys.addAll(preset.getFixedTags().keySet()); keys.addAll(preset.getRecommendedTags().keySet()); keys.addAll(preset.getOptionalTags().keySet()); } if (((PropertyEditor)getActivity()).presets != null && elements[0] != null) { // FIXME multiselect keys.addAll(Preset.getAutocompleteKeys(((PropertyEditor)getActivity()).presets, elements[0].getType())); // FIXME multiselect } keys.removeAll(getUsedKeys(rowLayout,keyEdit)); List<String> result = new ArrayList<String>(keys); Collections.sort(result); return new ArrayAdapter<String>(getActivity(), R.layout.autocomplete_row, result); } /** * Return true if the edited object has an address or is a "highway" * @param key * @param usedKeys * @return */ public static boolean isStreetName(String key, Set<String> usedKeys) { return (Tags.KEY_ADDR_STREET.equalsIgnoreCase(key) || (Tags.KEY_NAME.equalsIgnoreCase(key) && usedKeys.contains(Tags.KEY_HIGHWAY))); } /** * Return true if the edited object has an address or is a "place" * @param key * @param usedKeys * @return */ public static boolean isPlaceName(String key, Set<String> usedKeys) { return (Tags.KEY_ADDR_PLACE.equalsIgnoreCase(key) || (Tags.KEY_NAME.equalsIgnoreCase(key) && usedKeys.contains(Tags.KEY_PLACE))); } /** * Return true if the edited object could have a name in the name index * @param usedKeys * @return */ public static boolean useNameSuggestions(Set<String> usedKeys) { return !(usedKeys.contains(Tags.KEY_HIGHWAY) || usedKeys.contains(Tags.KEY_WATERWAY) || usedKeys.contains(Tags.KEY_LANDUSE) || usedKeys.contains(Tags.KEY_NATURAL) || usedKeys.contains(Tags.KEY_RAILWAY)); } private ArrayAdapter<?> getValueAutocompleteAdapter(PresetItem preset, LinearLayout rowLayout, TagEditRow row) { ArrayAdapter<?> adapter = null; String key = row.getKey(); if (key != null && key.length() > 0) { HashSet<String> usedKeys = (HashSet<String>) getUsedKeys(rowLayout,null); boolean hasTagValues = row.tagValues != null && row.tagValues.size() > 1; if (isStreetName(key, usedKeys)) { adapter = nameAdapters.getStreetNameAdapter(hasTagValues ? row.tagValues : null); } else if (isPlaceName(key, usedKeys)) { adapter = nameAdapters.getPlaceNameAdapter(hasTagValues ? row.tagValues : null); } else if (key.equals(Tags.KEY_NAME) && (names != null) && useNameSuggestions(usedKeys)) { Log.d(DEBUG_TAG,"generate suggestions for name from name suggestion index"); ArrayList<NameAndTags> values = (ArrayList<NameAndTags>) names.getNames(new TreeMap<String,String>(getKeyValueMapSingle(rowLayout,true))); // FIXME if (values != null && !values.isEmpty()) { Collections.sort(values); adapter = new ArrayAdapter<NameAndTags>(getActivity(), R.layout.autocomplete_row, values); } } else { HashMap<String, Integer> counter = new HashMap<String, Integer>(); ArrayAdapter<ValueWithCount> adapter2 = new ArrayAdapter<ValueWithCount>(getActivity(), R.layout.autocomplete_row); if (hasTagValues) { for(String t:row.tagValues) { if (t.equals("")) { continue; } if (counter.containsKey(t)) { counter.put(t, Integer.valueOf(counter.get(t).intValue()+1)); } else { counter.put(t, Integer.valueOf(1)); } } ArrayList<String> keys = new ArrayList<String>(counter.keySet()); Collections.sort(keys); for (String t:keys) { ValueWithCount v = new ValueWithCount(t,counter.get(t).intValue()); // FIXME determine description in some way adapter2.add(v); } } if (preset != null) { // note this will use the last applied preset which may be wrong FIXME Collection<StringWithDescription> values = preset.getAutocompleteValues(key); Log.d(DEBUG_TAG,"setting autocomplete adapter for values " + values + " based on " + preset.getName()); if (values != null && !values.isEmpty()) { ArrayList<StringWithDescription> result = new ArrayList<StringWithDescription>(values); if (preset.sortIt(key)) { Collections.sort(result); } for (StringWithDescription s:result) { if (counter != null && counter.containsKey(s.getValue())) { continue; // skip stuff that is already listed } adapter2.add(new ValueWithCount(s.getValue(), s.getDescription())); } Log.d(DEBUG_TAG,"key " + key + " type " + preset.getKeyType(key)); } else if (preset.isFixedTag(key)) { for (StringWithDescription s:Preset.getAutocompleteValues(((PropertyEditor)getActivity()).presets,elements[0].getType(), key)) { adapter2.add(new ValueWithCount(s.getValue(), s.getDescription())); } } } else if (((PropertyEditor)getActivity()).presets != null && elements[0] != null) { Log.d(DEBUG_TAG,"generate suggestions for >" + key + "< from presets"); // only do this if there is no other source of suggestions for (StringWithDescription s:Preset.getAutocompleteValues(((PropertyEditor)getActivity()).presets,elements[0].getType(), key)) { adapter2.add(new ValueWithCount(s.getValue(), s.getDescription())); } } else if (adapter2.getCount() == 0) { // FIXME shouldn't happen but seems to Log.d(DEBUG_TAG,"no suggestions for values for >" + key + "<"); } if (adapter2.getCount() > 0) { return adapter2; } } } return adapter; } /** * Insert a new row with one key and one value to edit. * @param aTagKey the key-value to start with * @param aTagValue the value to start with. * @param position the position where this should be inserted. set to -1 to insert at end, or 0 to insert at beginning. * @returns The new TagEditRow. */ private TagEditRow insertNewEdit(final LinearLayout rowLayout, final String aTagKey, final ArrayList<String> tagValues, final int position) { final TagEditRow row = (TagEditRow)inflater.inflate(R.layout.tag_edit_row, rowLayout, false); if (Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB) { // stop Hint from wrapping row.valueEdit.setEllipsize(TruncateAt.END); } boolean same = true; if (tagValues.size() > 1) { for (int i=1;i<tagValues.size();i++) { if (!tagValues.get(i-1).equals(tagValues.get(i))) { same = false; break; } } } row.setValues(aTagKey, tagValues, same); // If the user selects addr:street from the menu, auto-fill a suggestion row.keyEdit.setOnItemClickListener(new OnItemClickListener() { @Override public void onItemClick(AdapterView<?> parent, View view, int position, long id) { if (Tags.KEY_ADDR_STREET.equals(parent.getItemAtPosition(position)) && row.getValue().length() == 0) { ArrayAdapter<ValueWithCount> adapter = nameAdapters.getStreetNameAdapter(tagValues); if (adapter != null && adapter.getCount() > 0) { row.valueEdit.setText(adapter.getItem(0).getValue()); } } else if (Tags.KEY_ADDR_PLACE.equals(parent.getItemAtPosition(position)) && row.getValue().length() == 0) { ArrayAdapter<ValueWithCount> adapter = nameAdapters.getPlaceNameAdapter(tagValues); if (adapter != null && adapter.getCount() > 0) { row.valueEdit.setText(adapter.getItem(0).getValue()); } } else{ if (autocompletePresetItem != null) { String hint = autocompletePresetItem.getHint(parent.getItemAtPosition(position).toString()); if (hint != null) { // row.valueEdit.setHint(hint); } else if (autocompletePresetItem.getRecommendedTags().keySet().size() > 0 || autocompletePresetItem.getOptionalTags().keySet().size() > 0) { row.valueEdit.setHint(R.string.tag_value_hint); } if (row.getValue().length() == 0) { String defaultValue = autocompletePresetItem.getDefault(parent.getItemAtPosition(position).toString()); if (defaultValue != null) { // row.valueEdit.setText(defaultValue); } } } // set focus on value row.valueEdit.requestFocus(); } } }); row.keyEdit.setOnFocusChangeListener(new View.OnFocusChangeListener() { String originalKey; @Override public void onFocusChange(View v, boolean hasFocus) { PresetItem preset = getPreset(aTagKey); if (hasFocus) { // Log.d(DEBUG_TAG,"got focus"); originalKey = row.getKey(); row.keyEdit.setAdapter(getKeyAutocompleteAdapter(preset, rowLayout, row.keyEdit)); if (PropertyEditor.running && row.getKey().length() == 0) row.keyEdit.showDropDown(); } else { String newKey = row.getValue(); if (!newKey.equals(originalKey)) { // our preset may have changed re-calc updateAutocompletePresetItem(rowLayout, null); } } } }); row.valueEdit.setOnFocusChangeListener(new View.OnFocusChangeListener() { String originalValue; @Override public void onFocusChange(View v, boolean hasFocus) { if (hasFocus) { originalValue = row.getValue(); String key = row.getKey(); PresetItem preset = getPreset(key); row.valueEdit.setAdapter(getValueAutocompleteAdapter(preset, rowLayout, row)); if (preset != null && preset.getKeyType(key)==PresetKeyType.MULTISELECT) { // FIXME this should be somewhere better obvious since it creates a non obvious side effect row.valueEdit.setTokenizer(new CustomAutoCompleteTextView.SingleCharTokenizer(preset.getDelimiter(key))); } if (Tags.isWebsiteKey(key)) { initWebsite(row.valueEdit); } if (PropertyEditor.running) { if (row.valueEdit.getText().length() == 0) row.valueEdit.showDropDown(); // try { // hack to display numeric keyboard for numeric tag values // // unluckily there is then no way to get an alpha-numeric keyboard // int number = Integer.parseInt(valueEdit.getText().toString()); // valueEdit.setInputType(InputType.TYPE_CLASS_NUMBER); // } catch (NumberFormatException nfe) { // // do nothing // } } } else { // our preset may have changed re-calc String newValue = row.getValue(); if (!newValue.equals(originalValue)) { // Log.d(DEBUG_TAG,"lost focus"); // potentially we should update tagValues here updateAutocompletePresetItem(rowLayout, null); } } } }); // This TextWatcher reacts to previously empty cells being filled to add additional rows where needed TextWatcher emptyWatcher = new TextWatcher() { private boolean wasEmpty; @Override public void onTextChanged(CharSequence s, int start, int before, int count) { // nop } @Override public void beforeTextChanged(CharSequence s, int start, int count, int after) { wasEmpty = row.isEmpty(); } @Override public void afterTextChanged(Editable s) { if (wasEmpty == (s.length() > 0)) { // changed from empty to not-empty or vice versa row.enableCheckBox(); ensureEmptyRow(rowLayout); } // remove formating from pastes etc CharacterStyle[] toBeRemovedSpans = s.getSpans(0, s.length(), MetricAffectingSpan.class); for (int i = 0; i < toBeRemovedSpans.length; i++) s.removeSpan(toBeRemovedSpans[i]); } }; row.keyEdit.addTextChangedListener(emptyWatcher); row.valueEdit.addTextChangedListener(emptyWatcher); row.valueEdit.setOnItemClickListener(new OnItemClickListener() { @Override public void onItemClick(AdapterView<?> parent, View view, int position, long id) { Log.d("TagEdit","onItemClicked value"); Object o = parent.getItemAtPosition(position); if (o instanceof Names.NameAndTags) { row.valueEdit.setOrReplaceText(((NameAndTags)o).getName()); applyTagSuggestions(((NameAndTags)o).getTags()); } else if (o instanceof ValueWithCount) { row.valueEdit.setOrReplaceText(((ValueWithCount)o).getValue()); } else if (o instanceof StringWithDescription) { row.valueEdit.setOrReplaceText(((StringWithDescription)o).getValue()); } else if (o instanceof String) { row.valueEdit.setOrReplaceText((String)o); } } }); row.selected.setOnCheckedChangeListener(new OnCheckedChangeListener() { @Override public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { if (!row.isEmpty()) { if (isChecked) { tagSelected(); } else { deselectRow(); } } if (row.isEmpty()) { row.deselect(); } } }); if (row.isEmpty()) { row.disableCheckBox(); } rowLayout.addView(row, (position == -1) ? rowLayout.getChildCount() : position); // return row; } /** * A row representing an editable tag, consisting of edits for key and value, labels and a delete button. * Needs to be static, otherwise the inflater will not find it. * @author Jan */ public static class TagEditRow extends LinearLayout implements SelectedRowsActionModeCallback.Row { private PropertyEditor owner; private AutoCompleteTextView keyEdit; private CustomAutoCompleteTextView valueEdit; private CheckBox selected; private ArrayList<String> tagValues; private boolean same = true; public TagEditRow(Context context) { super(context); owner = (PropertyEditor) (isInEditMode() ? null : context); // Can only be instantiated inside TagEditor or in Eclipse } public TagEditRow(Context context, AttributeSet attrs) { super(context, attrs); owner = (PropertyEditor) (isInEditMode() ? null : context); // Can only be instantiated inside TagEditor or in Eclipse } // public TagEditRow(Context context, AttributeSet attrs, int defStyle) { // super(context, attrs, defStyle); // owner = (TagEditor) (isInEditMode() ? null : context); // Can only be instantiated inside TagEditor or in Eclipse // } @Override protected void onFinishInflate() { super.onFinishInflate(); if (isInEditMode()) return; // allow visual editor to work keyEdit = (AutoCompleteTextView)findViewById(R.id.editKey); keyEdit.setOnKeyListener(owner.myKeyListener); //lastEditKey.setSingleLine(true); valueEdit = (CustomAutoCompleteTextView)findViewById(R.id.editValue); valueEdit.setOnKeyListener(owner.myKeyListener); selected = (CheckBox) findViewById(R.id.tagSelected); OnClickListener autocompleteOnClick = new OnClickListener() { @Override public void onClick(View v) { if (v.hasFocus()) { ((AutoCompleteTextView)v).showDropDown(); } } }; // set an empty adapter on both views to be on the safe side ArrayAdapter<String> empty = new ArrayAdapter<String>(owner, R.layout.autocomplete_row, new String[0]); keyEdit.setAdapter(empty); valueEdit.setAdapter(empty); keyEdit.setOnClickListener(autocompleteOnClick); valueEdit.setOnClickListener(autocompleteOnClick); } @Override public void onSizeChanged(int w, int h, int oldw, int oldh) { super.onSizeChanged(w, h, oldw, oldh); // Log.d(DEBUG_TAG, "onSizeChanged"); if (w == 0 && h == 0) { return; } // Log.d(DEBUG_TAG,"w=" + w +" h="+h); // this is not really satisfactory keyEdit.setDropDownAnchor(valueEdit.getId()); // keyEdit.setDropDownVerticalOffset(-h); // valueEdit.setDropDownVerticalOffset(-h); valueEdit.setParentWidth(w); // } /** * Sets key and value values * @param aTagKey the key value to set * @param aTagValue the value value to set * @return the TagEditRow object for convenience */ public TagEditRow setValues(String aTagKey, ArrayList<String> tagValues, boolean same) { Log.d(DEBUG_TAG, "key " + aTagKey + " value " + tagValues); keyEdit.setText(aTagKey); this.tagValues = tagValues; this.same = same; if (same) { if (tagValues != null && tagValues.size() > 0) { valueEdit.setText(tagValues.get(0)); } else { valueEdit.setText(""); } } else { valueEdit.setHint(R.string.tag_multi_value_hint); } return this; } public String getKey() { return keyEdit.getText().toString(); } public String getValue() { // FIXME check if returning the textedit value is actually ok return valueEdit.getText().toString(); } /** * Deletes this row */ @Override public void delete() { //FIXME the references to owner.tagEditorFragemnt are likely suspect deleteRow((LinearLayout)owner.tagEditorFragment.getOurView()); } /** * Deletes this row */ public void deleteRow(LinearLayout rowLayout) { //FIXME the references to owner.tagEditorFragemnt are likely suspect View cf = owner.getCurrentFocus(); if (cf == keyEdit || cf == valueEdit) { // about to delete the row that has focus! // try to move the focus to the next row or failing that to the previous row int current = owner.tagEditorFragment.rowIndex(this); if (!owner.tagEditorFragment.focusRow(current + 1)) owner.tagEditorFragment.focusRow(current - 1); } rowLayout.removeView(this); if (isEmpty() && owner.tagEditorFragment != null) { owner.tagEditorFragment.ensureEmptyRow(); } } /** * Checks if the fields in this row are empty * @return true if both fields are empty, false if at least one is filled */ public boolean isEmpty() { return keyEdit.getText().toString().trim().equals("") && valueEdit.getText().toString().trim().equals(""); } // return the status of the checkbox @Override public boolean isSelected() { return selected.isChecked(); } @Override public void deselect() { selected.setChecked(false); } public void disableCheckBox() { selected.setEnabled(false); } void enableCheckBox() { selected.setEnabled(true); } } /** * Appy tags from name suggestion list, ask if overwriting * @param tags */ public void applyTagSuggestions(Names.TagMap tags) { final LinkedHashMap<String, ArrayList<String>> currentValues = getKeyValueMap(true); boolean replacedValue = false; // Fixed tags, always have a value. We overwrite mercilessly. for (Entry<String, String> tag : tags.entrySet()) { ArrayList<String> oldValue = currentValues.put(tag.getKey(), Util.getArrayList(tag.getValue())); if (oldValue != null && oldValue.size() > 0 && !oldValue.contains(tag.getValue())) replacedValue = true; } if (replacedValue) { Builder dialog = new AlertDialog.Builder(getActivity()); dialog.setTitle(R.string.tag_editor_name_suggestion); dialog.setMessage(R.string.tag_editor_name_suggestion_overwrite_message); dialog.setPositiveButton(R.string.replace, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { loadEdits(currentValues);// FIXME } }); dialog.setNegativeButton(R.string.cancel, null); dialog.create().show(); } else loadEdits(currentValues);// FIXME // TODO while applying presets automatically seems like a good idea, it needs some further thought if (prefs.enableAutoPreset()) { PresetItem p = Preset.findBestMatch(((PropertyEditor)getActivity()).presets,getKeyValueMapSingle(false)); // FIXME if (p!=null) { applyPreset(p, false, false); } } } private void tagSelected() { LinearLayout rowLayout = (LinearLayout) getOurView(); synchronized (actionModeCallbackLock) { if (tagSelectedActionModeCallback == null) { tagSelectedActionModeCallback = new TagSelectedActionModeCallback(this, rowLayout); ((AppCompatActivity)getActivity()).startSupportActionMode(tagSelectedActionModeCallback); } } } @Override public void deselectRow() { synchronized (actionModeCallbackLock) { if (tagSelectedActionModeCallback != null) { if (tagSelectedActionModeCallback.rowsDeselected(false)) { tagSelectedActionModeCallback = null; } } } } private void selectAllTags() { LinearLayout rowLayout = (LinearLayout) getOurView(); if (loaded) { int i = rowLayout.getChildCount(); while (--i >= 0) { TagEditRow row = (TagEditRow)rowLayout.getChildAt(i); if (row.selected.isEnabled()) { row.selected.setChecked(true); } } } } private void deselectAllTags() { LinearLayout rowLayout = (LinearLayout) getOurView(); if (loaded) { int i = rowLayout.getChildCount(); while (--i >= 0) { TagEditRow row = (TagEditRow)rowLayout.getChildAt(i); if (row.selected.isEnabled()) { row.selected.setChecked(false); } } } } /** * Ensures that at least one empty row exists (creating one if needed) * @return the first empty row found (or the one created), or null if loading was not finished (loaded == false) */ private TagEditRow ensureEmptyRow() { LinearLayout rowLayout = (LinearLayout) getOurView(); return ensureEmptyRow(rowLayout); } /** * Ensures that at least one empty row exists (creating one if needed) * @return the first empty row found (or the one created), or null if loading was not finished (loaded == false) */ private TagEditRow ensureEmptyRow(LinearLayout rowLayout) { TagEditRow ret = null; if (loaded) { int i = rowLayout.getChildCount(); while (--i >= 0) { TagEditRow row = (TagEditRow)rowLayout.getChildAt(i); boolean isEmpty = row.isEmpty(); if (ret == null) ret = isEmpty ? row : insertNewEdit(rowLayout,"", new ArrayList<String>(), -1); else if (isEmpty) row.deleteRow(rowLayout); } if (ret == null) ret = insertNewEdit(rowLayout,"", new ArrayList<String>(), -1); } return ret; } /** * Focus on the value field of the first tag with non empty key and empty value * @param key * @return */ private boolean focusOnEmptyValue() { LinearLayout rowLayout = (LinearLayout) getOurView(); return focusOnEmptyValue(rowLayout); } private boolean focusOnEmptyValue(LinearLayout rowLayout) { boolean found = false; for (int i = 0; i < rowLayout.getChildCount(); i++) { TagEditRow ter = (TagEditRow)rowLayout.getChildAt(i); if (ter.getKey() != null && !ter.getKey().equals("") && ter.getValue().equals("")) { focusRowValue(rowLayout, rowIndex(rowLayout,ter)); found = true; break; } } return found; } /** * Move the focus to the key field of the specified row. * @param index The index of the row to move to, counting from 0. * @return true if the row was successfully focussed, false otherwise. */ private boolean focusRow(int index) { LinearLayout rowLayout = (LinearLayout) getOurView(); TagEditRow row = (TagEditRow)rowLayout.getChildAt(index); return row != null && row.keyEdit.requestFocus(); } /** * Move the focus to the value field of the specified row. * @param index The index of the row to move to, counting from 0. * @return true if the row was successfully focussed, false otherwise. */ private boolean focusRowValue(LinearLayout rowLayout, int index) { TagEditRow row = (TagEditRow)rowLayout.getChildAt(index); return row != null && row.valueEdit.requestFocus(); } /** * Given a tag edit row, calculate its position. * @param row The tag edit row to find. * @return The position counting from 0 of the given row, or -1 if it couldn't be found. */ private int rowIndex(TagEditRow row) { LinearLayout rowLayout = (LinearLayout) getOurView(); return rowIndex(rowLayout, row); } private int rowIndex(LinearLayout rowLayout,TagEditRow row) { for (int i = rowLayout.getChildCount() - 1; i >= 0; --i) { if (rowLayout.getChildAt(i) == row) return i; } return -1; } /** * Focus on the value field of a tag with key "key" * @param key * @return */ private boolean focusOnValue(LinearLayout rowLayout, String key) { boolean found = false; for (int i = rowLayout.getChildCount() - 1; i >= 0; --i) { TagEditRow ter = (TagEditRow)rowLayout.getChildAt(i); if (ter.getKey().equals(key)) { focusRowValue(rowLayout, rowIndex(rowLayout, ter)); found = true; break; } } return found; } /** * Applies a preset (e.g. selected from the dialog or MRU), i.e. adds the tags from the preset to the current tag set * @param item the preset to apply */ void applyPreset(PresetItem item) { applyPreset(item, false, true); } /** * Applies a preset (e.g. selected from the dialog or MRU), i.e. adds the tags from the preset to the current tag set * @param item the preset to apply * @param addOptional TODO */ void applyPreset(PresetItem item, boolean addOptional, boolean addToMRU) { LinkedHashMap<String, ArrayList<String>> currentValues = getKeyValueMap(true); boolean wasEmpty = currentValues.size() == 0; boolean replacedValue = false; // remove everything that doesn't have a value // given that these are likely leftovers from a previous preset Set<String> keySet = new HashSet<String>(currentValues.keySet()); // shallow copy for (String key:keySet) { ArrayList<String>list = currentValues.get(key); if (list == null || list.size() == 0) { currentValues.remove(key); } } // Fixed tags, always have a value. We overwrite mercilessly. for (Entry<String, StringWithDescription> tag : item.getFixedTags().entrySet()) { String v = tag.getValue().getValue(); ArrayList<String> oldValue = currentValues.put(tag.getKey(), Util.getArrayList(v)); if (oldValue != null && oldValue.size() > 0 && !oldValue.contains(v)) { replacedValue = true; } } // Recommended tags, no fixed value is given. We add only those that do not already exist. for (Entry<String, StringWithDescription[]> tag : item.getRecommendedTags().entrySet()) { addTagFromPreset(item, currentValues, tag.getKey()); } // Optional tags, no fixed value is given. We add only those that do not already exist. if (addOptional) { for (Entry<String, StringWithDescription[]> tag : item.getOptionalTags().entrySet()) { addTagFromPreset(item, currentValues, tag.getKey()); } } loadEdits(currentValues); if (replacedValue) { Snack.barWarning(getActivity(), R.string.toast_preset_overwrote_tags); } if (wasEmpty || getBestPreset() == null) { // preset is what we just applied updateAutocompletePresetItem(item); } else { // re-determine best preset updateAutocompletePresetItem(null); } // if (addToMRU) { Preset[] presets = App.getCurrentPresets(getActivity()); if (presets != null) { for (Preset p:presets) { if (p.contains(item)) { p.putRecentlyUsed(item); break; } } } recreateRecentPresetView(); } focusOnEmptyValue(); } /** * Add tag from preset if the tag doesn't exist, execute JS if present * * If evaluating the JS returns null, the key is removed * @param item current Preset * @param tags map of current tags * @param key the key we are processing */ private void addTagFromPreset(PresetItem item, Map<String, ArrayList<String>> tags, String key) { String value = item.getDefault(key) == null ? "" : item.getDefault(key); if (!tags.containsKey(key)) { String script = item.getJavaScript(key); if (script!=null) { try { value = de.blau.android.javascript.Utils.evalString(getActivity(), " " + key, script, buildEdits(), tags, value); if (value==null) { tags.remove(key); return; } } catch (Exception ex) { Snack.barError(getActivity(), ex.getLocalizedMessage()); } } tags.put(key, Util.getArrayList(value)); } } /** * Update the MRU preset view */ void recreateRecentPresetView() { Log.d(DEBUG_TAG,"Updating MRU prests"); FragmentManager fm = getChildFragmentManager(); Fragment recentPresetsFragment = fm.findFragmentByTag(PropertyEditor.RECENTPRESETS_FRAGMENT); if (recentPresetsFragment != null) { ((RecentPresetsFragment)recentPresetsFragment).recreateRecentPresetView(); } } /** * Merge a set of tags in to the current ones * @param newTags */ private void mergeTags(Map<String, String> newTags) { LinkedHashMap<String, ArrayList<String>> currentValues = getKeyValueMap(true); boolean replacedValue = false; // Fixed tags, always have a value. We overwrite mercilessly. for (Entry<String, String> tag : newTags.entrySet()) { ArrayList<String> oldValue = currentValues.put(tag.getKey(), Util.getArrayList(tag.getValue())); if (oldValue != null && oldValue.size() > 0 && !oldValue.contains(tag.getValue())) { replacedValue = true; } } loadEdits(currentValues); if (replacedValue) { Snack.barWarning(getActivity(), R.string.toast_preset_overwrote_tags); } focusOnEmptyValue(); } /** * Merge a set of tags in to the current ones, with potentially empty keys * @param newTags * @param replace */ private void mergeTags(ArrayList<KeyValue> newTags, boolean replace) { LinkedHashMap<String, ArrayList<String>> currentValues = getKeyValueMap(true); HashMap<String,KeyValue> keyIndex = new HashMap<String, KeyValue>(); // needed for de-duping ArrayList<KeyValue> keysAndValues = new ArrayList<KeyValue>(); for (String key:currentValues.keySet()) { KeyValue keyValue = new KeyValue(key, currentValues.get(key)); keysAndValues.add(keyValue); keyIndex.put(key, keyValue); } boolean replacedValue = false; // for (KeyValue tag : newTags) { KeyValue keyValue = keyIndex.get(tag.getKey()); if (keyValue != null && replace) { // exists keyValue.setValue(tag.getValue()); replacedValue = true; } else { keysAndValues.add(new KeyValue(tag.getKey(),tag.getValue())); } } // this code needs to be duplicated because we can't use a map here LinearLayout rowLayout = (LinearLayout) getOurView(); loaded = false; rowLayout.removeAllViews(); for (KeyValue keyValue:keysAndValues) { insertNewEdit(rowLayout, keyValue.getKey(), keyValue.getValues(), -1); } loaded = true; ensureEmptyRow(rowLayout); if (replacedValue) { Snack.barWarning(getActivity(), R.string.toast_preset_overwrote_tags); } focusOnEmptyValue(); } @Override public void onCreateOptionsMenu(final Menu menu, final MenuInflater inflater) { // final MenuInflater inflater = getSupportMenuInflater(); super.onCreateOptionsMenu(menu, inflater); inflater.inflate(R.menu.tag_menu, menu); menu.findItem(R.id.tag_menu_mapfeatures).setEnabled(NetworkStatus.isConnected(getActivity())); menu.findItem(R.id.tag_menu_paste).setVisible(pasteIsPossible()); menu.findItem(R.id.tag_menu_paste_from_clipboard).setVisible(pasteFromClipboardIsPossible()); } @Override public void onPrepareOptionsMenu(Menu menu) { super.onPrepareOptionsMenu(menu); // disable address tagging for stuff that won't have an address // menu.findItem(R.id.tag_menu_address).setVisible(!type.equals(Way.NAME) || element.hasTagKey(Tags.KEY_BUILDING)); } @Override public boolean onOptionsItemSelected(final MenuItem item) { switch (item.getItemId()) { case android.R.id.home: ((PropertyEditor)getActivity()).sendResultAndFinish(); return true; case R.id.tag_menu_address: predictAddressTags(false); return true; case R.id.tag_menu_sourcesurvey: doSourceSurvey(); return true; case R.id.tag_menu_apply_preset: PresetItem pi = Preset.findBestMatch(((PropertyEditor)getActivity()).presets,getKeyValueMapSingle(false)); // FIXME if (pi!=null) { applyPreset(pi, true, false); } return true; case R.id.tag_menu_paste: paste(true); return true; case R.id.tag_menu_paste_from_clipboard: pasteFromClipboard(true); return true; case R.id.tag_menu_revert: doRevert(); return true; case R.id.tag_menu_mapfeatures: startActivity(Preset.getMapFeaturesIntent(getActivity(),getBestPreset())); return true; case R.id.tag_menu_resetMRU: for (Preset p:((PropertyEditor)getActivity()).presets) p.resetRecentlyUsed(); ((PropertyEditor)getActivity()).recreateRecentPresetView(); return true; case R.id.tag_menu_reset_address_prediction: // simply overwrite with an empty file Address.resetLastAddresses(getActivity()); return true; case R.id.tag_menu_js_console: de.blau.android.javascript.Utils.jsConsoleDialog(getActivity(), R.string.js_console_msg_debug, new EvalCallback() { @Override public String eval(String input) { return de.blau.android.javascript.Utils.evalString(getActivity(), "JS Preset Test", input, buildEdits(), getKeyValueMap(true), "test"); } }); return true; case R.id.tag_menu_help: HelpViewer.start(getActivity(), R.string.help_propertyeditor); return true; } return false; } /** * Collect all key-value pairs into a LinkedHashMap<String,String> * * @param allowBlanks If true, includes key-value pairs where one or the other is blank. * @return The LinkedHashMap<String,String> of key-value pairs. */ private LinkedHashMap<String,ArrayList<String>> getKeyValueMap(final boolean allowBlanks) { LinearLayout rowLayout = (LinearLayout) getOurView(); return getKeyValueMap(rowLayout, allowBlanks); } private LinkedHashMap<String,ArrayList<String>> getKeyValueMap(LinearLayout rowLayout, final boolean allowBlanks) { final LinkedHashMap<String,ArrayList<String>> tags = new LinkedHashMap<String, ArrayList<String>>(); if (rowLayout == null && savedTags != null) { return savedTags; } if (rowLayout != null) { processKeyValues(rowLayout, new KeyValueHandler() { @Override public void handleKeyValue(final EditText keyEdit, final EditText valueEdit, final ArrayList<String> tagValues) { String key = keyEdit.getText().toString().trim(); String value = valueEdit.getText().toString().trim(); boolean valueBlank = "".equals(value); boolean bothBlank = "".equals(key) && valueBlank; boolean neitherBlank = !"".equals(key) && !valueBlank; if (!bothBlank) { // both blank is never acceptable if (neitherBlank || allowBlanks || (valueBlank && tagValues != null && tagValues.size()>0)) { if (valueBlank) { tags.put(key, tagValues.size()==1?Util.getArrayList(""):tagValues); } else { tags.put(key, Util.getArrayList(value)); } } } } }); } else { Log.e(DEBUG_TAG,"rowLayout null in getKeyValueMapSingle"); } // for (String key:tags.keySet()) { // Log.d(DEBUG_TAG,"getKeyValueMap Key " + key + " " + tags.get(key)); // } return tags; } /** * Version of above that ignores multiple values * @param allowBlanks * @return */ public LinkedHashMap<String,String> getKeyValueMapSingle(final boolean allowBlanks) { LinearLayout rowLayout = (LinearLayout) getOurView(); return getKeyValueMapSingle(rowLayout, allowBlanks); } private LinkedHashMap<String,String> getKeyValueMapSingle(LinearLayout rowLayout, final boolean allowBlanks) { final LinkedHashMap<String,String> tags = new LinkedHashMap<String, String>(); if (rowLayout == null && savedTags != null) { for (Entry<String,ArrayList<String>>entry:savedTags.entrySet()) { String key = entry.getKey().trim(); ArrayList<String> tagValues = entry.getValue(); String value = tagValues != null && tagValues.size() > 0 ? (tagValues.get(0)!=null?tagValues.get(0):""):""; boolean valueBlank = "".equals(value); boolean bothBlank = "".equals(key) && valueBlank; boolean neitherBlank = !"".equals(key) && !valueBlank; if (!bothBlank) { // both blank is never acceptable if (neitherBlank || allowBlanks || valueBlank) { if (valueBlank) { tags.put(key, tagValues==null || tagValues.size()==1?"":tagValues.get(0)); // FIXME if multi-select } else { tags.put(key, value); } } } } } if (rowLayout != null) { processKeyValues(rowLayout, new KeyValueHandler() { @Override public void handleKeyValue(final EditText keyEdit, final EditText valueEdit, final ArrayList<String> tagValues) { String key = keyEdit.getText().toString().trim(); String value = valueEdit.getText().toString().trim(); boolean valueBlank = "".equals(value); boolean bothBlank = "".equals(key) && valueBlank; boolean neitherBlank = !"".equals(key) && !valueBlank; if (!bothBlank) { // both blank is never acceptable boolean hasValues = tagValues != null && tagValues.size()>0; if (neitherBlank || allowBlanks || (valueBlank && hasValues)) { if (valueBlank) { tags.put(key, ""); } else { tags.put(key, value); } } } } }); } else { Log.e(DEBUG_TAG,"rowLayout null in getKeyValueMapSingle"); } return tags; } /** * Given an edit field of an OSM key value, determine it's corresponding source key. * For example, the source of "name" is "source:name". The source of "source" is * "source". The source of "mf:name" is "mf.source:name". * @param keyEdit The edit field of the key to be sourced. * @return The source key for the given key. */ private static String sourceForKey(final String key) { String result = "source"; if (key != null && !key.equals("") && !key.equals("source")) { // key is neither blank nor "source" // check if it's namespaced int i = key.indexOf(':'); if (i == -1) { result = "source:" + key; } else { // handle already namespaced keys as per // http://wiki.openstreetmap.org/wiki/Key:source result = key.substring(0, i) + ".source" + key.substring(i); } } return result; } private void doSourceSurvey() { // FIXME // determine the key (if any) that has the current focus in the key or its value final String[] focusedKey = new String[]{null}; // array to work around unsettable final processKeyValues(new KeyValueHandler() { @Override public void handleKeyValue(final EditText keyEdit, final EditText valueEdit, final ArrayList<String> tagValues) { if (keyEdit.isFocused() || valueEdit.isFocused()) { focusedKey[0] = keyEdit.getText().toString().trim(); } } }); // ensure source(:key)=survey is tagged final String sourceKey = sourceForKey(focusedKey[0]); final boolean[] sourceSet = new boolean[]{false}; // array to work around unsettable final processKeyValues(new KeyValueHandler() { @Override public void handleKeyValue(final EditText keyEdit, final EditText valueEdit, final ArrayList<String> tagValues) { if (!sourceSet[0]) { String key = keyEdit.getText().toString().trim(); String value = valueEdit.getText().toString().trim(); // if there's a blank row - use them if (key.equals("") && value.equals("")) { key = sourceKey; keyEdit.setText(key); } if (key.equals(sourceKey)) { valueEdit.setText(Tags.VALUE_SURVEY); sourceSet[0] = true; } } } }); if (!sourceSet[0]) { // source wasn't set above - add a new pair ArrayList<String> v = new ArrayList<String>(); v.add(Tags.VALUE_SURVEY); insertNewEdit((LinearLayout) getOurView(),sourceKey, v, -1); } } @Override public boolean pasteIsPossible() { if (copiedTags == null) { copiedTags = savingHelper.load(getActivity(),PropertyEditor.COPIED_TAGS_FILE, false); } return copiedTags != null; } @Override public boolean paste(boolean replace) { if (copiedTags != null) { mergeTags(copiedTags); } else { copiedTags = savingHelper.load(getActivity(),PropertyEditor.COPIED_TAGS_FILE, false); if (copiedTags != null) { mergeTags(copiedTags); } } updateAutocompletePresetItem(null); return copiedTags != null; } @Override public boolean pasteFromClipboardIsPossible() { return ClipboardUtils.checkForText(getActivity()); } @Override public boolean pasteFromClipboard(boolean replace) { ArrayList<KeyValue> paste = ClipboardUtils.getKeyValues(getActivity()); if (paste != null) { mergeTags(paste, replace); } updateAutocompletePresetItem(null); return paste != null; } @Override public void copyTags(Map<String,String>tags) { copiedTags = new LinkedHashMap<String,String>(tags); ClipboardUtils.copyTags(getActivity(), copiedTags); } /** * reload original arguments */ void doRevert() { loadEdits(buildEdits()); updateAutocompletePresetItem(null); } /** * @return the OSM ID of the element currently edited by the editor */ public long getOsmId() { // FIXME return osmIds[0]; } public String getType() {// FIXME return types[0]; } /** * Get all key values currently in the editor, optionally skipping one field. * @param ignoreEdit optional - if not null, this key field will be skipped, * i.e. the key in it will not be included in the output * @return the set of all (or all but one) keys currently entered in the edit boxes */ private Set<String> getUsedKeys(LinearLayout rowLayout, final EditText ignoreEdit) { final HashSet<String> keys = new HashSet<String>(); processKeyValues(rowLayout,new KeyValueHandler() { @Override public void handleKeyValue(final EditText keyEdit, final EditText valueEdit, final ArrayList<String> tagValues) { if (!keyEdit.equals(ignoreEdit)) { String key = keyEdit.getText().toString().trim(); if (key.length() > 0) { keys.add(key); } } } }); return keys; } @Override public void deselectHeaderCheckBox() { CheckBox headerCheckBox = (CheckBox) getView().findViewById(R.id.header_tag_selected); headerCheckBox.setChecked(false); } /** * Return the view we have our rows in and work around some android craziness * @return */ private View getOurView() { // android.support.v4.app.NoSaveStateFrameLayout View v = getView(); if (v != null) { if ( v.getId() == R.id.edit_row_layout) { Log.d(DEBUG_TAG,"got correct view in getView"); return v; } else { v = v.findViewById(R.id.edit_row_layout); if (v == null) { Log.d(DEBUG_TAG,"didn't find R.id.edit_row_layout"); } else { Log.d(DEBUG_TAG,"Found R.id.edit_row_layout"); } return v; } } else { Log.d(DEBUG_TAG,"got null view in getView"); } return null; } /** * Return tags copied or cut * @return */ public LinkedHashMap<String,String> getCopiedTags() { return (LinkedHashMap<String, String>) copiedTags; } public void enableRecentPresets() { FragmentManager fm = getChildFragmentManager(); Fragment recentPresetsFragment = fm.findFragmentByTag(PropertyEditor.RECENTPRESETS_FRAGMENT); if (recentPresetsFragment != null) { ((RecentPresetsFragment)recentPresetsFragment).enable(); } } public void disableRecentPresets() { FragmentManager fm = getChildFragmentManager(); Fragment recentPresetsFragment = fm.findFragmentByTag(PropertyEditor.RECENTPRESETS_FRAGMENT); if (recentPresetsFragment != null) { ((RecentPresetsFragment)recentPresetsFragment).disable(); } } /** * update the original list of tags to reflect edits * @return */ public ArrayList<LinkedHashMap<String, String>> getUpdatedTags() { @SuppressWarnings("unchecked") ArrayList<Map<String,String>> oldTags = (ArrayList<Map<String,String>>)getArguments().getSerializable("tags"); // make a (nearly) full copy ArrayList<LinkedHashMap<String,String>> newTags = new ArrayList<LinkedHashMap<String,String>>(); for (Map<String,String> map:oldTags) { newTags.add(new LinkedHashMap<String, String>(map)); } LinkedHashMap<String,ArrayList<String>> edits = getKeyValueMap(true); if (edits == null) { // if we didn't get a LinkedHashMap as input we need to copy ArrayList<LinkedHashMap<String,String>> newOldTags = new ArrayList<LinkedHashMap<String,String>>(); for (Map<String,String> map:oldTags) { newOldTags.add(new LinkedHashMap<String, String>(map)); } return newOldTags; } for (LinkedHashMap<String,String> map:newTags) { for (String key:new TreeSet<String>(map.keySet())) { if (edits.containsKey(key)) { if (edits.get(key).size()==1) { String value = edits.get(key).get(0).trim(); if (saveTag(key, value)) { addTagToMap(map, key, value); } else { map.remove(key); // zap stuff with empty values or just the HTTP prefix } } } else { // key deleted map.remove(key); } } // check for new tags for (String editsKey:edits.keySet()) { if (editsKey != null && !"".equals(editsKey) && !map.containsKey(editsKey) && edits.get(editsKey).size()==1) { // zap empty stuff or just the HTTP prefix String value = edits.get(editsKey).get(0).trim(); if (saveTag(editsKey,value)) { addTagToMap(map, editsKey, value); } } } } return newTags; } /** * @param key * @param value * @return true is value isn't empty and isn't the HTTP prefix */ private boolean saveTag(String key, String value) { return !"".equals(value) && !(Tags.isWebsiteKey(key) && HTTP_PREFIX.equals(value)); } /** * Add tag if it doesn't exist * @param layout * @param key * @param value */ private void addTag(LinearLayout rowLayout, String key, String value, boolean replace, boolean update) { Log.d(DEBUG_TAG,"adding tag " + key + "=" + value); LinkedHashMap<String, ArrayList<String>> currentValues = getKeyValueMap(rowLayout,true); if (!currentValues.containsKey(key) || replace) { currentValues.put(key, Util.getArrayList(value)); loadEdits(rowLayout,currentValues); if (update) { updateAutocompletePresetItem(null); } } } @Override public void updateSingleValue(String key, String value) { LinkedHashMap<String, ArrayList<String>> currentValues = getKeyValueMap(true); currentValues.put(key, Util.getArrayList(value)); loadEdits(currentValues); updateAutocompletePresetItem(null); } @Override public void updateTags(Map<String, String> tags, boolean flush) { loadEditsSingle(tags); updateAutocompletePresetItem(null); } @Override public void revertTags() { doRevert(); } @Override public void deleteTag(final String key) { LinearLayout l = (LinearLayout) getOurView(); if (l!=null) { for (int i = l.getChildCount() - 1; i >= 0; --i) { TagEditRow ter = (TagEditRow)l.getChildAt(i); if (ter.getKey().equals(key)) { ter.delete(); break; } } } } /** * Added key-value to a map stripping training list separator * @param map * @param key * @param value */ private void addTagToMap(Map<String,String>map, String key, String value) { if (autocompletePresetItem != null && autocompletePresetItem.getKeyType(key)==PresetKeyType.MULTISELECT) { // trim potential trailing separators if (value.endsWith(String.valueOf(autocompletePresetItem.getDelimiter(key)))) { value = value.substring(0, value.length()-1); } } map.put(key, value); } /** * Add http:// to empty EditTexts that are supposed to contain a website and set input mode * @param valueEdit */ public static void initWebsite(final EditText valueEdit) { valueEdit.setInputType(InputType.TYPE_CLASS_TEXT|InputType.TYPE_TEXT_VARIATION_URI); if (valueEdit.getText().length() == 0) { valueEdit.setText(HTTP_PREFIX); valueEdit.setSelection(HTTP_PREFIX.length()); } } @Override public void applyPreset(PresetItem preset, boolean addOptional) { applyPreset(preset, addOptional, true); } }