diff --git a/src/androidTest/java/de/blau/android/propertyeditor/PropertyEditorTest.java b/src/androidTest/java/de/blau/android/propertyeditor/PropertyEditorTest.java index 5ed4c00513..9557892944 100644 --- a/src/androidTest/java/de/blau/android/propertyeditor/PropertyEditorTest.java +++ b/src/androidTest/java/de/blau/android/propertyeditor/PropertyEditorTest.java @@ -322,7 +322,7 @@ public void nodeWithDirection1() { fail(); } assertNotNull(direction); - assertEquals("Type or tap for values", direction.getText()); + assertEquals(main.getString(R.string.tag_dialog_value_hint), direction.getText()); direction.clickAndWait(Until.newWindow(), 2000); TestUtils.clickText(device, true, main.getString(R.string.save), true, false); TestUtils.clickHome(device, true); @@ -371,12 +371,12 @@ public void nodeWithDirection2() { fail(); } assertNotNull(direction); - assertEquals("Type or tap for values", direction.getText()); + assertEquals(main.getString(R.string.tag_dialog_value_hint), direction.getText()); direction.clickAndWait(Until.newWindow(), 2000); TestUtils.clickText(device, true, "Forward", false, false); TestUtils.clickText(device, true, main.getString(R.string.save), true, false); TestUtils.clickHome(device, true); - assertEquals("forward", n.getTagWithKey("direction")); + assertEquals("forward", n.getTagWithKey("direction").toLowerCase()); } @Test @@ -410,7 +410,7 @@ public void nodeWithDirection3() { fail(); } assertNotNull(direction); - assertEquals("forward", direction.getText()); + assertEquals("forward", direction.getText().toLowerCase()); direction.clickAndWait(Until.newWindow(), 2000); TestUtils.clickText(device, true, "Forward", false, false); TestUtils.clickText(device, true, main.getString(R.string.save), true, false); diff --git a/src/main/java/de/blau/android/Logic.java b/src/main/java/de/blau/android/Logic.java index 6affec140d..c1f219c834 100644 --- a/src/main/java/de/blau/android/Logic.java +++ b/src/main/java/de/blau/android/Logic.java @@ -55,7 +55,6 @@ import androidx.appcompat.app.AlertDialog; import androidx.appcompat.app.AppCompatActivity; import androidx.fragment.app.FragmentActivity; -import ch.poole.osm.josmfilterparser.Nodes; import de.blau.android.contract.HttpStatusCodes; import de.blau.android.contract.Urls; import de.blau.android.dialogs.AttachedObjectWarning; @@ -110,6 +109,7 @@ import de.blau.android.osm.ViewBox; import de.blau.android.osm.Way; import de.blau.android.prefs.Preferences; +import de.blau.android.presets.Preset; import de.blau.android.resources.DataStyle; import de.blau.android.resources.DataStyle.FeatureStyle; import de.blau.android.tasks.Note; @@ -1577,7 +1577,7 @@ synchronized void handleTouchEventMove(@NonNull Main main, final float absoluteX /** * Rotate selected objects * - * Note that this needs to rotate all the nodes of all objects at once to avoid rotating the same ome multiple + * Note that this needs to rotate all the nodes of all objects at once to avoid rotating the same one multiple * times, special cases exactly one node selected. * * @param activity the current Activity @@ -1597,7 +1597,7 @@ private void rotateSelection(@NonNull FragmentActivity activity, @NonNull final List selectedNodes = selection.getNodes(); if (selectedNodes != null) { if (selection.count() == 1) { - updateDirection((float) Math.toDegrees(angle), direction, selectedNodes.get(0)); + updateDirection(activity, (float) Math.toDegrees(angle), direction, selectedNodes.get(0)); return; } nodes.addAll(selectedNodes); @@ -1625,8 +1625,9 @@ private void rotateSelection(@NonNull FragmentActivity activity, @NonNull final * @param direction rotation direction (+ == clockwise) * @param node the Node */ - private void updateDirection(float angle, int direction, @NonNull final Node node) { - String directionKey = Tags.getDirectionKey(node); + private void updateDirection(@NonNull Context context, float angle, int direction, @NonNull final Node node) { + // this is obviously quiet expensive bit avoids having state somewhere else + String directionKey = Tags.getDirectionKey(Preset.findBestMatch(App.getCurrentPresets(context), node.getTags(), null, null), node); if (directionKey != null) { java.util.Map tags = new HashMap<>(node.getTags()); Float currentAngle = Tags.parseDirection(tags.get(directionKey)); @@ -2808,8 +2809,11 @@ private Node findTargetNode(@NonNull List targetNodes, double newLon, doub for (Node target : targetNodes) { double distance = GeoMath.haversineDistance(target.getLon() / 1E7D, target.getLat() / 1E7D, newLon, newLat); if (distance < bestDistance) { - if (target.hasTags() && prefs != null && distance > prefs.getReplaceTolerance()) { // only use tagged nodes if they are really close to new - // position + if (target.hasTags() && prefs != null && distance > prefs.getReplaceTolerance()) { // only use tagged + // nodes if they are + // really close to + // new + // position continue; } bestDistance = distance; diff --git a/src/main/java/de/blau/android/easyedit/NodeSelectionActionModeCallback.java b/src/main/java/de/blau/android/easyedit/NodeSelectionActionModeCallback.java index f6f89926db..613b41c727 100644 --- a/src/main/java/de/blau/android/easyedit/NodeSelectionActionModeCallback.java +++ b/src/main/java/de/blau/android/easyedit/NodeSelectionActionModeCallback.java @@ -24,6 +24,7 @@ import androidx.appcompat.app.AlertDialog.Builder; import androidx.appcompat.app.AppCompatDialog; import androidx.appcompat.view.ActionMode; +import de.blau.android.App; import de.blau.android.DisambiguationMenu; import de.blau.android.R; import de.blau.android.dialogs.ElementIssueDialog; @@ -36,6 +37,7 @@ import de.blau.android.osm.Result; import de.blau.android.osm.Tags; import de.blau.android.osm.Way; +import de.blau.android.presets.Preset; import de.blau.android.util.GeoMath; import de.blau.android.util.ScreenMessage; import de.blau.android.util.Sound; @@ -67,6 +69,7 @@ public class NodeSelectionActionModeCallback extends ElementSelectionActionModeC private MenuItem restrictionItem; private MenuItem rotateItem; private int action; + private Preset[] presets; /** * Construct a callback for Node selection @@ -86,9 +89,9 @@ public boolean onCreateActionMode(ActionMode mode, Menu menu) { main.invalidateMap(); mode.setTitle(R.string.actionmode_nodeselect); mode.setSubtitle(null); - + presets = App.getCurrentPresets(main); menu = replaceMenu(menu, mode, this); - SortedMap tags = ((Node) element).getTags(); + SortedMap tags = element.getTags(); if (!tags.containsKey(Tags.KEY_ADDR_HOUSENUMBER) && !tags.containsKey(Tags.KEY_HIGHWAY)) { // exclude some stuff that typically doesn't have an address menu.add(Menu.NONE, MENUITEM_ADDRESS, Menu.NONE, R.string.tag_menu_address).setIcon(ThemeUtils.getResIdFromAttribute(main, R.attr.menu_address)); @@ -137,7 +140,7 @@ public boolean onPrepareActionMode(ActionMode mode, Menu menu) { } updated |= setItemVisibility(highways.size() >= 2, restrictionItem, false); - updated |= setItemVisibility(Tags.getDirectionKey(element) != null, rotateItem, false); + updated |= setItemVisibility(Tags.getDirectionKey(Preset.findBestMatch(presets, element.getTags(), null, null), element) != null, rotateItem, false); if (updated) { arrangeMenu(menu); diff --git a/src/main/java/de/blau/android/easyedit/ReplaceGeometryActionModeCallback.java b/src/main/java/de/blau/android/easyedit/ReplaceGeometryActionModeCallback.java index d1e45dbd60..a7e2467607 100644 --- a/src/main/java/de/blau/android/easyedit/ReplaceGeometryActionModeCallback.java +++ b/src/main/java/de/blau/android/easyedit/ReplaceGeometryActionModeCallback.java @@ -7,14 +7,11 @@ import java.util.HashSet; import java.util.List; -import android.app.Activity; import android.app.AlertDialog; import android.content.DialogInterface; -import android.content.DialogInterface.OnDismissListener; import android.util.Log; import android.view.Menu; import androidx.annotation.NonNull; -import androidx.annotation.Nullable; import androidx.appcompat.view.ActionMode; import de.blau.android.App; import de.blau.android.Logic; @@ -22,12 +19,10 @@ import de.blau.android.dialogs.ElementIssueDialog; import de.blau.android.exception.OsmIllegalOperationException; import de.blau.android.exception.StorageException; -import de.blau.android.osm.GeoPoint; import de.blau.android.osm.MergeAction; import de.blau.android.osm.Node; import de.blau.android.osm.OsmElement; import de.blau.android.osm.Result; -import de.blau.android.osm.StorageDelegator; import de.blau.android.osm.Way; import de.blau.android.util.SerializableState; diff --git a/src/main/java/de/blau/android/layer/data/MapOverlay.java b/src/main/java/de/blau/android/layer/data/MapOverlay.java index 7d3cc1bf4f..de94140160 100644 --- a/src/main/java/de/blau/android/layer/data/MapOverlay.java +++ b/src/main/java/de/blau/android/layer/data/MapOverlay.java @@ -187,6 +187,11 @@ public class MapOverlay extends MapViewLayer */ private final WeakHashMap, Float> directionCache = new WeakHashMap<>(); + /** + * Cache for any preset matching during one draw pass + */ + private final HashMap, PresetItem> matchCache = new HashMap<>(); + /** * Stores custom icons */ @@ -477,6 +482,7 @@ protected void onDraw(Canvas canvas, IMapView osmv) { download.setBox(viewBox); map.getRootView().postDelayed(download, 100); } + matchCache.clear(); paintOsmData(canvas); } @@ -1141,9 +1147,12 @@ private Float getDirection(@NonNull final Node node) { Float direction = node.getFromCache(directionCache); if (direction == null) { direction = Float.NaN; - String key = Tags.getDirectionKey(node); - if (key != null) { - direction = Tags.parseDirection(node.getTagWithKey(key)); + PresetItem match = getMatch(node); + if (match != null) { + String key = Tags.getDirectionKey(match, node); + if (key != null) { + direction = Tags.parseDirection(node.getTagWithKey(key)); + } } synchronized (directionCache) { node.addToCache(directionCache, direction); @@ -1152,6 +1161,23 @@ private Float getDirection(@NonNull final Node node) { return direction; } + /** + * Get the best preset match for an element and cache it during this draw pass + * + * @param e the OsmElement + * @return a PresetItem or null + */ + @Nullable + private PresetItem getMatch(@NonNull OsmElement e) { + java.util.Map tags = e.getTags(); + PresetItem match = matchCache.get(tags); + if (match == null && !matchCache.containsKey(tags)) { + match = Preset.findBestMatch(tmpPresets, tags, null, null); + matchCache.put(tags, match); + } + return match; + } + /** * Draw a circle with center at x,y with the house number in it * @@ -1190,7 +1216,7 @@ private void paintLabel(final float x, final float y, @NonNull final Canvas canv } FeatureStyle style = styles.matchStyle(e); if (style.usePresetLabel() && tmpPresets != null) { - PresetItem match = Preset.findBestMatch(tmpPresets, e.getTags(), null, null); + PresetItem match = getMatch(e); if (match != null) { String template = e.nameFromTemplate(context, match); label = template != null ? template : match.getTranslatedName(); @@ -1265,7 +1291,7 @@ private void retrieveIcon(@NonNull OsmElement element, boolean isWay, @NonNull W iconDrawable = retrieveCustomIcon(iconPath); } } else if (tmpPresets != null) { - PresetItem match = !isWay || usePresetIcon ? Preset.findBestMatch(tmpPresets, element.getTags(), null, null) : null; + PresetItem match = !isWay || usePresetIcon ? getMatch(element) : null; if (match != null) { iconDrawable = match.getMapIcon(context); } diff --git a/src/main/java/de/blau/android/osm/Tags.java b/src/main/java/de/blau/android/osm/Tags.java index b7bd34565e..d34ee551e6 100644 --- a/src/main/java/de/blau/android/osm/Tags.java +++ b/src/main/java/de/blau/android/osm/Tags.java @@ -11,6 +11,10 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import de.blau.android.presets.PresetField; +import de.blau.android.presets.PresetItem; +import de.blau.android.presets.PresetTagField; +import de.blau.android.presets.ValueType; /** * Key and value constants for tags that are used in the code @@ -281,19 +285,20 @@ public static boolean isSpeedKey(@Nullable final String key) { public static final String KEY_CAMERA_DIRECTION = "camera:direction"; public static final String KEY_LIGHT_DIRECTION = "light:direction"; - public static final List DIRECTION_KEYS = Collections.unmodifiableList(Arrays.asList(KEY_DIRECTION, KEY_CAMERA_DIRECTION, KEY_LIGHT_DIRECTION)); - /** * Get a direction key if any are present from the OSM element * + * @param presetItem matching preset item * @param e the OSM element * @return the key or null */ @Nullable - public static String getDirectionKey(@NonNull OsmElement e) { - for (String key : Tags.DIRECTION_KEYS) { - if (e.hasTagKey(key)) { - return key; + public static String getDirectionKey(@Nullable PresetItem presetItem, @NonNull OsmElement e) { + if (presetItem != null) { + for (PresetField field : presetItem.getFields().values()) { + if (field instanceof PresetTagField && ((PresetTagField) field).getValueType() == ValueType.CARDINAL_DIRECTION) { + return ((PresetTagField) field).getKey(); + } } } return null; diff --git a/src/main/java/de/blau/android/presets/ValueType.java b/src/main/java/de/blau/android/presets/ValueType.java index f222f9d401..ad9744c5f3 100644 --- a/src/main/java/de/blau/android/presets/ValueType.java +++ b/src/main/java/de/blau/android/presets/ValueType.java @@ -5,7 +5,7 @@ import androidx.annotation.Nullable; public enum ValueType { - OPENING_HOURS, OPENING_HOURS_MIXED, DIMENSION_HORIZONTAL, DIMENSION_VERTICAL, INTEGER, WEBSITE, PHONE, WIKIPEDIA, WIKIDATA; + OPENING_HOURS, OPENING_HOURS_MIXED, DIMENSION_HORIZONTAL, DIMENSION_VERTICAL, INTEGER, WEBSITE, PHONE, WIKIPEDIA, WIKIDATA, CARDINAL_DIRECTION; /** * Get a ValueType corresponding to the input String @@ -44,6 +44,9 @@ static ValueType fromString(@NonNull String typeString) { case "wikidata": type = WIKIDATA; break; + case "cardinal_direction": + type = CARDINAL_DIRECTION; + break; default: Log.e(ValueType.class.getSimpleName(), "Unknown value type string " + typeString); } diff --git a/src/main/java/de/blau/android/propertyeditor/tagform/IntegerValueFragment.java b/src/main/java/de/blau/android/propertyeditor/tagform/IntegerValueFragment.java index d22a530e26..427efaef91 100644 --- a/src/main/java/de/blau/android/propertyeditor/tagform/IntegerValueFragment.java +++ b/src/main/java/de/blau/android/propertyeditor/tagform/IntegerValueFragment.java @@ -125,7 +125,7 @@ class IntegerWidget implements ValueWidget { if (editView instanceof EditText) { // Remove default input filter ((EditText) editView).setFilters(new InputFilter[0]); - ((EditText) editView).setFocusable(false); + editView.setFocusable(false); } } diff --git a/src/main/java/de/blau/android/propertyeditor/tagform/TagFormFragment.java b/src/main/java/de/blau/android/propertyeditor/tagform/TagFormFragment.java index 490e360c67..cbf5d5d0f0 100644 --- a/src/main/java/de/blau/android/propertyeditor/tagform/TagFormFragment.java +++ b/src/main/java/de/blau/android/propertyeditor/tagform/TagFormFragment.java @@ -993,7 +993,7 @@ void addRow(@Nullable final LinearLayout rowLayout, @NonNull final PresetTagFiel ValueType valueType = field.getValueType(); if (field instanceof PresetTextField || key.startsWith(Tags.KEY_ADDR_BASE) || (isComboField && ((PresetComboField) field).isEditable() && ValueType.OPENING_HOURS_MIXED != valueType) || Tags.isConditional(key) - || ValueType.INTEGER == valueType || Tags.DIRECTION_KEYS.contains(key)) { + || ValueType.INTEGER == valueType || ValueType.CARDINAL_DIRECTION == valueType) { if (Tags.isConditional(key)) { rowLayout.addView(getConditionalRestrictionDialogRow(rowLayout, preset, hint, key, value, values, allTags)); return; @@ -1014,6 +1014,10 @@ void addRow(@Nullable final LinearLayout rowLayout, @NonNull final PresetTagFiel rowLayout.addView(LongTextDialogRow.getRow(this, inflater, rowLayout, preset, (PresetTextField) field, value, maxStringLength)); return; } + if ( ValueType.INTEGER == valueType || ValueType.CARDINAL_DIRECTION == valueType) { + rowLayout.addView(ValueWidgetRow.getRow(this, inflater, rowLayout, preset, field, value, values, allTags)); + return; + } rowLayout.addView(TextRow.getRow(this, inflater, rowLayout, preset, field, value, values, allTags)); return; } diff --git a/src/main/java/de/blau/android/propertyeditor/tagform/TextRow.java b/src/main/java/de/blau/android/propertyeditor/tagform/TextRow.java index 9e18b10b7e..8362d7f6da 100644 --- a/src/main/java/de/blau/android/propertyeditor/tagform/TextRow.java +++ b/src/main/java/de/blau/android/propertyeditor/tagform/TextRow.java @@ -178,7 +178,6 @@ static TextRow getRow(@NonNull final TagFormFragment caller, @NonNull final Layo final TextRow row = (TextRow) inflater.inflate(R.layout.tag_form_text_row, rowLayout, false); final String key = field.getKey(); final String hint = preset != null ? field.getHint() : null; - final String defaultValue = field.getDefaultValue(); row.valueType = preset != null ? preset.getValueType(key) : null; final boolean isName = Tags.isLikeAName(key); final Context context = rowLayout.getContext(); @@ -218,25 +217,6 @@ static TextRow getRow(@NonNull final TagFormFragment caller, @NonNull final Layo dialog.show(); }); } - if (ValueType.INTEGER == valueType) { - ourValueView.setFocusable(false); - ourValueView.setFocusableInTouchMode(false); - ourValueView.setOnClickListener(v -> { - final View finalView = v; - finalView.setEnabled(false); // debounce - IntegerValueFragment.show(caller, hint != null ? hint : key, key, ((TextView) v).getText().toString(), values, preset, allTags); - }); - } - if (Tags.DIRECTION_KEYS.contains(key) - && (preset == null || !(preset.hasKeyValue(Tags.KEY_HIGHWAY, Tags.VALUE_MINI_ROUNDABOUT) && Tags.KEY_DIRECTION.equals(key)))) { - ourValueView.setFocusable(false); - ourValueView.setFocusableInTouchMode(false); - ourValueView.setOnClickListener(v -> { - final View finalView = v; - finalView.setEnabled(false); // debounce - DirectionValueFragment.show(caller, hint != null ? hint : key, key, ((TextView) v).getText().toString(), values, preset, allTags); - }); - } ourValueView.setOnFocusChangeListener((v, hasFocus) -> { Log.d(DEBUG_TAG, "onFocusChange"); String rowValue = row.getValue(); diff --git a/src/main/java/de/blau/android/propertyeditor/tagform/ValueWidgetRow.java b/src/main/java/de/blau/android/propertyeditor/tagform/ValueWidgetRow.java new file mode 100644 index 0000000000..b4c1707a3e --- /dev/null +++ b/src/main/java/de/blau/android/propertyeditor/tagform/ValueWidgetRow.java @@ -0,0 +1,109 @@ +package de.blau.android.propertyeditor.tagform; + +import java.util.List; +import java.util.Map; + +import android.content.Context; +import android.util.AttributeSet; +import android.view.LayoutInflater; +import android.view.View; +import android.widget.LinearLayout; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import de.blau.android.R; +import de.blau.android.presets.PresetComboField; +import de.blau.android.presets.PresetItem; +import de.blau.android.presets.PresetTagField; +import de.blau.android.presets.ValueType; +import de.blau.android.propertyeditor.TagChanged; +import de.blau.android.util.StringWithDescription; + +public class ValueWidgetRow extends DialogRow implements TagChanged { + + private ValueType valueType; + private PresetTagField field; + + /** + * Construct a row that will display a Dialog when clicked + * + * @param context Android Context + */ + public ValueWidgetRow(@NonNull Context context) { + super(context); + } + + /** + * Construct a row that will display a Dialog when clicked + * + * @param context Android Context + * @param attrs an AttributeSet + */ + public ValueWidgetRow(@NonNull Context context, AttributeSet attrs) { + super(context, attrs); + } + + /** + * Add a row that displays a dialog for selecting a single when clicked + * + * @param caller the calling TagFormFragment instance + * @param inflater the inflater to use + * @param rowLayout the Layout holding the roes + * @param preset the relevant PresetItem + * @param field the current PresetTagField + * @param value the current value + * @param values all relevant values (preset MRU etc) + * @param allTags all current tags + * @return a ValueWidgetRow + */ + static ValueWidgetRow getRow(@NonNull final TagFormFragment caller, @NonNull final LayoutInflater inflater, @NonNull final LinearLayout rowLayout, + @NonNull final PresetItem preset, @NonNull final PresetTagField field, @Nullable final String value, @Nullable final List values, + @NonNull final Map allTags) { + final ValueWidgetRow row = (ValueWidgetRow) inflater.inflate(R.layout.tag_form_value_widget_row, rowLayout, false); + final String key = field.getKey(); + final String hint = field.getHint(); + row.keyView.setText(hint); + row.keyView.setTag(key); + row.setPreset(preset); + row.valueType = preset.getValueType(key); + row.field = field; + + row.valueView.setHint(R.string.tag_dialog_value_hint); + row.setValue(value == null ? "" : value); + + row.valueView.setFocusable(false); + row.valueView.setFocusableInTouchMode(false); + row.setOnClickListener(v -> { + final View finalView = v; + finalView.setEnabled(false); // debounce + if (ValueType.INTEGER == row.valueType) { + IntegerValueFragment.show(caller, hint, key, row.getValue(), values, preset, allTags); + } + if (ValueType.CARDINAL_DIRECTION == row.valueType) { + DirectionValueFragment.show(caller, hint, key, row.getValue(), values, preset, allTags); + } + }); + return row; + } + + @Override + public void changed(String key, String value) { + if (key.equals(this.getKey())) { + valueView.setEnabled(true); + setValue(value); + } + } + + @Override + public void setValue(@NonNull String value) { + // it might be simpler to simply use the normal autocomplete adapter here + if (field instanceof PresetComboField) { + for (StringWithDescription swd : ((PresetComboField) field).getValues()) { + if (value.equals(swd.getValue())) { + setValue(swd); + return; + } + } + } + super.setValue(value); + } +} diff --git a/src/main/res/layout/tag_form_value_widget_row.xml b/src/main/res/layout/tag_form_value_widget_row.xml new file mode 100644 index 0000000000..8c085094cb --- /dev/null +++ b/src/main/res/layout/tag_form_value_widget_row.xml @@ -0,0 +1,57 @@ + + + + + + + + + + + + \ No newline at end of file