From 9fde3fa32034c37f8bb41e396b7c80eabb0a6084 Mon Sep 17 00:00:00 2001 From: simonpoole Date: Thu, 11 Jan 2024 14:47:35 +0100 Subject: [PATCH] Support following a way in PathCreationActionMode Fixes https://github.com/MarcusWolschon/osmeditor4android/issues/1936 --- .../de/blau/android/DisambiguationMenu.java | 15 +- .../PathCreationActionModeCallback.java | 293 ++++++++++++++--- .../java/de/blau/android/util/MathUtil.java | 31 ++ src/main/res/drawable-xhdpi/follow_dark.png | Bin 0 -> 2555 bytes src/main/res/drawable-xhdpi/follow_light.png | Bin 0 -> 4141 bytes src/main/res/values/attrs.xml | 1 + src/main/res/values/shortcuts.xml | 1 + src/main/res/values/strings.xml | 6 +- src/main/res/values/styles.xml | 2 + svg/follow.svg | 300 ++++++++++++++++++ svg/follow_dark.svg | 296 +++++++++++++++++ svg/follow_light.svg | 300 ++++++++++++++++++ 12 files changed, 1198 insertions(+), 47 deletions(-) create mode 100644 src/main/java/de/blau/android/util/MathUtil.java create mode 100644 src/main/res/drawable-xhdpi/follow_dark.png create mode 100644 src/main/res/drawable-xhdpi/follow_light.png create mode 100644 svg/follow.svg create mode 100644 svg/follow_dark.svg create mode 100644 svg/follow_light.svg diff --git a/src/main/java/de/blau/android/DisambiguationMenu.java b/src/main/java/de/blau/android/DisambiguationMenu.java index f7c5701580..4339f3577b 100644 --- a/src/main/java/de/blau/android/DisambiguationMenu.java +++ b/src/main/java/de/blau/android/DisambiguationMenu.java @@ -187,6 +187,19 @@ public void add(int id, @Nullable Type type, @NonNull String text, boolean selec add(new DisambiguationMenuItem(type, s), listener); } + /** + * Add a menu item + * + * @param id an id (unused) + * @param the Type of object + * @param text a descriptive text + * @param listener callback when the menu item is selected + */ + public void add(int id, @Nullable Type type, @NonNull String text, @NonNull final OnMenuItemClickListener listener) { // NOSONAR + SpannableString s = new SpannableString(text); + add(new DisambiguationMenuItem(type, s), listener); + } + /** * Add a menu item * @@ -212,7 +225,7 @@ public void add(int id, @Nullable Type type, @NonNull int textRes, boolean selec public void add(int id, @Nullable Type type, @NonNull SpannableString text, boolean selected, @NonNull final OnMenuItemClickListener listener) { // NOSONAR add(new DisambiguationMenuItem(type, text), listener); } - + /** * Add a menu item * diff --git a/src/main/java/de/blau/android/easyedit/PathCreationActionModeCallback.java b/src/main/java/de/blau/android/easyedit/PathCreationActionModeCallback.java index 2370428549..ba0bf43d50 100644 --- a/src/main/java/de/blau/android/easyedit/PathCreationActionModeCallback.java +++ b/src/main/java/de/blau/android/easyedit/PathCreationActionModeCallback.java @@ -1,6 +1,8 @@ package de.blau.android.easyedit; import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; import java.util.List; import android.content.Context; @@ -17,8 +19,10 @@ import androidx.appcompat.view.ActionMode; import androidx.appcompat.widget.AppCompatCheckBox; import de.blau.android.App; +import de.blau.android.DisambiguationMenu; import de.blau.android.Map; import de.blau.android.R; +import de.blau.android.DisambiguationMenu.Type; import de.blau.android.dialogs.AddressInterpolationDialog; import de.blau.android.dialogs.Tip; import de.blau.android.exception.OsmIllegalOperationException; @@ -29,8 +33,9 @@ import de.blau.android.osm.UndoStorage.UndoElement; import de.blau.android.osm.UndoStorage.UndoWay; import de.blau.android.osm.Way; -import de.blau.android.util.SerializableState; +import de.blau.android.util.MathUtil; import de.blau.android.util.ScreenMessage; +import de.blau.android.util.SerializableState; import de.blau.android.util.Sound; import de.blau.android.util.ThemeUtils; import de.blau.android.util.Util; @@ -44,14 +49,18 @@ public class PathCreationActionModeCallback extends BuilderActionModeCallback { protected static final int MENUITEM_UNDO = 1; private static final int MENUITEM_SNAP = 2; private static final int MENUITEM_NEWWAY_PRESET = 3; - private static final int MENUITEM_ADDRESS = 4; - - private static final String NODE_IDS_KEY = "node ids"; - private static final String EXISTING_NODE_IDS_KEY = "existing node ids"; - private static final String WAY_ID_KEY = "way id"; - private static final String TITLE_KEY = "title"; - private static final String SUBTITLE_KEY = "subtitle"; - private static final String CHECKPOINT_NAME_KEY = "checkpoint name"; + private static final int MENUITEM_FOLLOW_WAY = 4; + private static final int MENUITEM_ADDRESS = 5; + + private static final String NODE_IDS_KEY = "node ids"; + private static final String EXISTING_NODE_IDS_KEY = "existing node ids"; + private static final String WAY_ID_KEY = "way id"; + private static final String TITLE_KEY = "title"; + private static final String SUBTITLE_KEY = "subtitle"; + private static final String CHECKPOINT_NAME_KEY = "checkpoint name"; + private static final String CANDIDATES_FOR_FOLLOWING_IDS_KEY = "candidates for following ids"; + private static final String INITIAL_FOLLOW_NODE_ID_KEY = "initial follow node id"; + private static final String WAY_TO_FOLLOW_ID_KEY = "way to follow id"; /** x coordinate of first node */ private float x; @@ -67,11 +76,15 @@ public class PathCreationActionModeCallback extends BuilderActionModeCallback { private boolean snap = true; /** contains a pointer to the created way if one was created. used to fix selection after undo. */ - private Way createdWay = null; + private Way createdWay = null; /** contains a list of added nodes. used to fix selection after undo. */ - protected List addedNodes = new ArrayList<>(); + protected List addedNodes = new ArrayList<>(); /** nodes we added that already existed */ - private List existingNodes = new ArrayList<>(); + private List existingNodes = new ArrayList<>(); + /** ways that we could potentially follow */ + private List candidatesForFollowing = null; + private Node initialFollowNode = null; + private Way wayToFollow = null; private String savedTitle = null; private String savedSubtitle = null; @@ -87,25 +100,9 @@ public class PathCreationActionModeCallback extends BuilderActionModeCallback { */ public PathCreationActionModeCallback(@NonNull EasyEditManager manager, @NonNull SerializableState state) { super(manager); - List ids = state.getList(NODE_IDS_KEY); StorageDelegator delegator = App.getDelegator(); - for (Long id : ids) { - Node node = (Node) delegator.getOsmElement(Node.NAME, id); - if (node != null) { - addedNodes.add(node); - } else { - throw new IllegalStateException("Failed to find node " + id); - } - } - List existingIds = state.getList(EXISTING_NODE_IDS_KEY); - for (Long id : existingIds) { - Node node = (Node) delegator.getOsmElement(Node.NAME, id); - if (node != null) { - existingNodes.add(node); - } else { - throw new IllegalStateException("Failed to find node " + id); - } - } + getElementsFromIds(state, delegator, NODE_IDS_KEY, addedNodes, Node.NAME); + getElementsFromIds(state, delegator, EXISTING_NODE_IDS_KEY, existingNodes, Node.NAME); if (!addedNodes.isEmpty()) { appendTargetNode = addedNodes.get(addedNodes.size() - 1); } @@ -117,6 +114,40 @@ public PathCreationActionModeCallback(@NonNull EasyEditManager manager, @NonNull savedTitle = state.getString(TITLE_KEY); savedSubtitle = state.getString(SUBTITLE_KEY); checkpointName = state.getInteger(CHECKPOINT_NAME_KEY); + getElementsFromIds(state, delegator, CANDIDATES_FOR_FOLLOWING_IDS_KEY, candidatesForFollowing, Way.NAME); + Long initialFollowNodeId = state.getLong(INITIAL_FOLLOW_NODE_ID_KEY); + if (initialFollowNodeId != null) { + initialFollowNode = (Node) delegator.getOsmElement(Node.NAME, initialFollowNodeId); + } + Long wayToFollowId = state.getLong(WAY_TO_FOLLOW_ID_KEY); + if (wayToFollowId != null) { + wayToFollow = (Way) delegator.getOsmElement(Way.NAME, wayToFollowId); + } + } + + /** + * File List list fith OsmElements from ids in state + * + * @param the OsmElement type + * @param state the saved state + * @param delegator the StorageDelegator instance + * @param key the key for the list of ids + * @param list the target List + * @param elementName the element Name + */ + private void getElementsFromIds(SerializableState state, StorageDelegator delegator, String key, List list, String elementName) { + List ids = state.getList(key); + if (ids != null) { + for (Long id : ids) { + @SuppressWarnings("unchecked") + T element = (T) delegator.getOsmElement(elementName, id); + if (element != null) { + list.add(element); + } else { + throw new IllegalStateException("Failed to find element key " + key + " " + id); + } + } + } } /** @@ -197,6 +228,9 @@ public boolean onPrepareActionMode(ActionMode mode, Menu menu) { }); // menu.add(Menu.NONE, MENUITEM_NEWWAY_PRESET, Menu.NONE, R.string.tag_menu_preset).setIcon(ThemeUtils.getResIdFromAttribute(main, R.attr.menu_preset)); + if (candidatesForFollowing != null && !candidatesForFollowing.isEmpty()) { + menu.add(Menu.NONE, MENUITEM_FOLLOW_WAY, Menu.NONE, R.string.menu_follow_way).setIcon(ThemeUtils.getResIdFromAttribute(main, R.attr.menu_follow)); + } menu.add(Menu.NONE, MENUITEM_ADDRESS, Menu.NONE, R.string.tag_menu_address).setIcon(ThemeUtils.getResIdFromAttribute(main, R.attr.menu_address)); menu.add(GROUP_BASE, MENUITEM_HELP, Menu.CATEGORY_SYSTEM | 10, R.string.menu_help).setIcon(ThemeUtils.getResIdFromAttribute(main, R.attr.menu_help)); arrangeMenu(menu); @@ -244,6 +278,9 @@ static void addSnapCheckBox(@NonNull Context ctx, @NonNull Menu menu, boolean sn @Override public boolean handleClick(float x, float y) { super.handleClick(x, y); + if (logic.getClickableElements() != null) { // way follow + return false; + } try { pathCreateNode(x, y); } catch (OsmIllegalOperationException e) { @@ -252,6 +289,76 @@ public boolean handleClick(float x, float y) { return true; } + @Override + public boolean handleElementClick(OsmElement element) { + // protect against race conditions and other issues + if (!(element instanceof Node) || wayToFollow == null || initialFollowNode == null) { + Log.e(DEBUG_TAG, "handleElementClick " + element + " " + wayToFollow + " " + initialFollowNode); + return false; + } + List followNodes = wayToFollow.getNodes(); + List nodesToAdd = nodesFromFollow(followNodes, initialFollowNode, addedNodes.get(addedNodes.size() - 1), (Node) element, wayToFollow.isClosed()); + existingNodes.addAll(nodesToAdd); + addedNodes.addAll(nodesToAdd); + createdWay.getNodes().addAll(nodesToAdd); // nodes already all exist in storage + createdWay.invalidateBoundingBox(); + logic.setClickableElements(null); + if (createdWay.isClosed()) { + finishPath(createdWay, null); + return true; + } + logic.setSelectedWay(createdWay); + logic.setSelectedNode((Node) element); + mode.setTitle(savedTitle); + mode.setSubtitle(R.string.add_way_node_instruction); + mode.invalidate(); + main.invalidateMap(); + return true; + } + + /** + * Extract the list of Nodes to add to the new way in the correct order + * + * @param followNodes full List of nodes from the original way + * @param initialNode the first of the two nodes that enable following + * @param startNode start Node + * @param endNode end Node + * @param closed true if this was a closed way + * @return a List of Nodes + */ + @NonNull + private List nodesFromFollow(@NonNull List followNodes, @NonNull Node initialNode, @NonNull Node startNode, @NonNull Node endNode, + boolean closed) { + int posStart = followNodes.indexOf(startNode); // positions in the way we are copying + int posEnd = followNodes.indexOf(endNode); + if (!closed) { + if (posEnd > posStart) { + return followNodes.subList(posStart + 1, posEnd + 1); + } + List toReverse = new ArrayList<>(followNodes.subList(posEnd, posStart)); // copy required + Collections.reverse(toReverse); + return toReverse; + } + // closed way slightly complicated + int posInitial = followNodes.indexOf(initialNode); + List result = new ArrayList<>(); + int count = followNodes.size(); + + final int lastNodePos = count - 2; // -2 to skip the closing node + // determine in which direction we are traversing the nodes of the way we are following + int inc = (posStart > posInitial && posInitial != 0 && posStart != lastNodePos) || (posInitial == lastNodePos && posStart == 0) + || (posInitial == 0 && posStart == 1) ? 1 : -1; + + for (int i = MathUtil.floorMod(posStart + inc, count); i != MathUtil.floorMod(posEnd + inc, count); i = MathUtil.floorMod(i + inc, count)) { + final Node next = followNodes.get(i); + // skip the closing node if there is one + if (!next.equals(startNode) && (result.isEmpty() || !result.get(result.size() - 1).equals(next))) { + result.add(next); + } + } + return result; + } + /** * Creates/adds a node into a path during path creation * @@ -278,22 +385,44 @@ private synchronized void pathCreateNode(float x, float y) { if (logic.getSelectedNode() == null) { // user clicked last node again -> finish adding finishPath(lastSelectedWay, lastSelectedNode); - } else { // update cache for undo - createdWay = logic.getSelectedWay(); - if (createdWay == null) { - addedNodes = new ArrayList<>(); - } else { - createdWay.dontValidate(); - } - addedNodes.add(logic.getSelectedNode()); - if (firstNode) { - mode.invalidate(); // activate undo - } - if (clicked != null) { - // node already existed - existingNodes.add(clicked); + return; + } + // update cache for undo + createdWay = logic.getSelectedWay(); + if (createdWay == null) { + addedNodes = new ArrayList<>(); + } else { + createdWay.dontValidate(); + } + addedNodes.add(logic.getSelectedNode()); + if (firstNode) { + mode.invalidate(); // activate undo + } + // node already existed id clicked != null + if (clicked != null) { + existingNodes.add(clicked); + // check if we are potentially following a way + if (lastSelectedNode != null) { + boolean alreadyAvailable = candidatesForFollowing != null && !candidatesForFollowing.isEmpty(); + candidatesForFollowing = logic.getWaysForNode(lastSelectedNode); + candidatesForFollowing.retainAll(logic.getWaysForNode(clicked)); + candidatesForFollowing.remove(createdWay); + // remove any ways that we have "used up" + for (Way candidate : new ArrayList<>(candidatesForFollowing)) { + if (candidate.isEndNode(clicked) && !candidate.isClosed()) { + candidatesForFollowing.remove(candidate); + } + } + initialFollowNode = lastSelectedNode; + if (!alreadyAvailable || (alreadyAvailable && candidatesForFollowing.isEmpty())) { + mode.invalidate(); + } } + } else { + candidatesForFollowing = null; + mode.invalidate(); } + mode.setSubtitle(R.string.add_way_node_instruction); main.invalidateMap(); } @@ -312,7 +441,10 @@ public boolean onActionItemClicked(ActionMode mode, MenuItem item) { switch (itemId) { case MENUITEM_UNDO: handleUndo(); - break; + return true; + case MENUITEM_FOLLOW_WAY: + handleFollow(); + return true; case MENUITEM_NEWWAY_PRESET: case MENUITEM_ADDRESS: Way lastSelectedWay = logic.getSelectedWay(); @@ -334,6 +466,59 @@ public boolean onActionItemClicked(ActionMode mode, MenuItem item) { return false; } + /** + * Setup follow way selection + */ + private void handleFollow() { + if (candidatesForFollowing.size() == 1) { + followWay(mode, candidatesForFollowing.get(0)); + return; + } + DisambiguationMenu menu = new DisambiguationMenu(main.getMap()); + menu.setHeaderTitle(R.string.select_follow_way); + int id = 0; + for (Way w : candidatesForFollowing) { + menu.add(id, Type.WAY, w.getDescription(main), (int position) -> followWay(mode, w)); + id++; + } + menu.show(); + } + + /** + * Follow a way from the last node added to a end node that is selected in the next step + * + * @param mode the current ActionMode + */ + private void followWay(@NonNull ActionMode mode, @NonNull Way follow) { + wayToFollow = follow; + List endNodesCandidates = new ArrayList<>(follow.getNodes()); // copy required!! + // remove nodes that are not "in front of the current node" + final Node current = addedNodes.get(addedNodes.size() - 1); + int posCurrent = endNodesCandidates.indexOf(current); + final Node previous = addedNodes.get(addedNodes.size() - 2); + int posPrevious = endNodesCandidates.indexOf(previous); + if (follow.isClosed()) { + endNodesCandidates.removeAll(addedNodes); + final Node firstAdded = addedNodes.get(0); + if (follow.hasNode(firstAdded)) { + endNodesCandidates.add(firstAdded); + } + } else { + endNodesCandidates = posPrevious < posCurrent ? endNodesCandidates.subList(posCurrent + 1, endNodesCandidates.size()) + : endNodesCandidates.subList(0, posCurrent); + } + logic.setSelectedWay(null); + logic.setSelectedNode(null); + logic.setClickableElements(new HashSet<>(endNodesCandidates)); + logic.setReturnRelations(false); + if (savedTitle == null) { + savedTitle = mode.getTitle().toString(); + } + mode.setTitle(R.string.actionmode_createpath_follow_way); + mode.setSubtitle(R.string.actionmode_createpath_follow_way_select_end_node); + main.invalidateMap(); + } + /** * Handle presses on the undo button, this does not invoke the normal undo mechanism but simply removes the * non-saved nodes one by one @@ -429,6 +614,11 @@ protected void delayedResetHasProblem(@Nullable final Way way) { public boolean processShortcut(Character c) { if (c == Util.getShortCut(main, R.string.shortcut_copy)) { handleUndo(); + return true; + } + if (c == Util.getShortCut(main, R.string.shortcut_follow)) { + handleFollow(); + return true; } return super.processShortcut(c); } @@ -473,6 +663,19 @@ public void saveState(SerializableState state) { state.putString(TITLE_KEY, mode.getTitle().toString()); state.putString(SUBTITLE_KEY, mode.getSubtitle().toString()); state.putInteger(CHECKPOINT_NAME_KEY, checkpointName); + if (candidatesForFollowing != null) { + List candidatesForFollowingIds = new ArrayList<>(); + for (Way w : candidatesForFollowing) { + candidatesForFollowingIds.add(w.getOsmId()); + } + state.putList(CANDIDATES_FOR_FOLLOWING_IDS_KEY, candidatesForFollowingIds); + } + if (initialFollowNode != null) { + state.putLong(INITIAL_FOLLOW_NODE_ID_KEY, initialFollowNode.getOsmId()); + } + if (wayToFollow != null) { + state.putLong(WAY_TO_FOLLOW_ID_KEY, wayToFollow.getOsmId()); + } } @Override diff --git a/src/main/java/de/blau/android/util/MathUtil.java b/src/main/java/de/blau/android/util/MathUtil.java new file mode 100644 index 0000000000..e064b0b394 --- /dev/null +++ b/src/main/java/de/blau/android/util/MathUtil.java @@ -0,0 +1,31 @@ +package de.blau.android.util; + +/** + * Android doesn't have floor mode before API 24 + */ +public final class MathUtil { + + /** + * Private constructor + */ + private MathUtil() { + // empty + } + + /** + * Floor modulus + * + * See Math.floorMod + * + * @param x dividend + * @param y dividor + * @return the floor modulus of x and y + */ + public static int floorMod(int x, int y) { + int r = x / y; + if ((x ^ y) < 0 && (r * y != x)) { + r--; + } + return x - r * y; + } +} diff --git a/src/main/res/drawable-xhdpi/follow_dark.png b/src/main/res/drawable-xhdpi/follow_dark.png new file mode 100644 index 0000000000000000000000000000000000000000..7fc109924720bf3d01b3d1fdc60cede6e0a273bb GIT binary patch literal 2555 zcmV+HRzy&mkxe!Qz;&0r#_Uv%4HW?jwT+Y{*B zBtFLgzsYVS0c;0$htCe66L=#G=>QbQbcXA#Kvx*E9k{(G6xkF7nAt)}PXVU`L-2_x z`~}z({>IN0e*ZvR30uOm&A?6j(kcQ#($SKhmb62SV}qpWlA5(LYoeuDjLhr>U@ows zK^C1r8*r_e?Wr}mS!kL90)C4t;LQeTw*wED+2W!Bl3FDlFX=={BP6}IQ9&~_b*Y(I zo1_`Q0ANHNq;CLE12^Qq2;1Q-U_5SYW9*c)0Jzi4mNq1FBSh68=@8&Az{ha|R|{Rh z3gB`xo1Nn|0=O19GJ(%r;1_1LqaJyix&^}9Iu$sy4zf02A+EHT*BIb-(k2gZ8Zbpt z-#UJ~c`HEDNx;Rpz%9UV;2@w^6|(KXa^M?g_EN-a5O62Z8jXDw_%pB+k0S;HCj(!k zd@Da{W|ySO-EA8nX*6&V@R8_o7cOugpqimt3rveAAMiuUC;td=m6^rH^+jNwq(*KH?ZIAdK3EW3%dp-kPP|Y<4 zS?-`@b5@$!gp_&IZ385=NV*!hm*UC41WW|J2mG5-M3`X@C7-`N_ua`kxqewIE-rwFe{8RUIHrW?27K&fm?tx&1_~C$D5geU*n3&-eL#7Dy}c!Js`SXU589-C|_v< ziU!7LNC9TH0pF)PfN8)PW;V^tc2yX=2zVTS8^R6W@6Bve0sGDdsJ{5#ohTD%KydGWG6HN}MTmdO^H693=S$pE-F|!x(J-P{)Y-XMLF_QX8 znurHdnOsLoIzCaPaWOmK*%YE4OFMD9(QanT;)fxj`6_S@@IE|n>Ib|QhFmJ?yOp+A z#N#;NOVOic$uB2a{2GvSFz{xzLWZQ4?E4Tlj|a{N-k%$K7+xUB4CeyiXJ)pw0?#pc zz7PjaKQOZeDROrw5NZX^#QpYg-~fD&Zvy56_nX?JpH;@mS#NzzGFoRs&Pa?BU$+9RW<{t#yDrx_}wL z4Q95x4%zBfz=gQ6im?j#2kz$&#wYn{N|wAA@Buvf%iD+bxN-bjk?n}K%=o}N_FV12 z6Tn@;)6|V*1q4ewEuz^4OalHIBEER+hr@Rt3XcW^y3OH!3!d)YQ_Z?pkfAT02KU9i z#O6va^=*Nqn$J2MmM9;XzNz6p#oAGc|PwrQWbDOblpWMLQ6sHliyPZU5_AD0k7t0h7=8~fDab%*(;E$fP7WvvnfO& zkU2Ws9#DbLJB}m;nAs}4ij!f2q{FJwgmT!$xc_efjsnI@8W)0_y#n*@)up@DQ*(f~ZdO9^VW0Y5jh z`I7nopU2M%jwl%aI$mtO8@Cy^;;SkZOMwgDwsbf{Se9o1-=gy28u$;!iy@~_zAfgO+2yHo>2^sh97~PB%Uav;eZ8#KX--h$ zsd!0jXzuF)_-e>9y(jUN)LwzlXnaM+C9#QS_GF6uP45q=7eNtYf%9;~*$4MBSq`xh zIIl)g^cdVgXA$c%Gdnls_Z!^;qj4X*eGC(!>G zI6J!q#+ljXDkMPTW%1?sTN;b+(@wm0*M_&Pv{O<{8zlv8#1-^fo?%Fw=1E$FZw-L8 zDU%O)fRX~Tv%!aphdUZlKsflk4>*=`BiM>-e;4lav%fnjHBIi6w2P8LHUKZg z)HwUi1MyR!4!ouqH%G-8W8OPd!%)iSlazUOHz~OrZ&hiet+fsB4!bt@JU%)b8Z3MY zz7Sn2$EOh8Qh=GQ!F_bmQFjrx0qcODQ}^(0{2*jfrCPq3t;4rghS9*}qV~72LT;wq=KLX1mZn64EqI7JmbCuBb-+|J zyQT`yfxumqM|WACuo&+w9fGIZr%_(8eZ7p2L^XZ%}%E6wbmRrvPCkKs?p1Fu--oxoFgtIjix$lXi@ z37O&lTDVXDw?f; z$MEv`Ry^K+8SfBTlMOJlEqHVF^}IEV?oZnP80@`}bgrZqB>hX$6*b0=ijL41NxG<~ z5$L|FkMp3w;9IYBxzc$kawSfq{}2FXL6EE<^O%UZ)0yd`r3~g_#dHs>Ym@` RhhhK#002ovPDHLkV1hF~x-$R( literal 0 HcmV?d00001 diff --git a/src/main/res/drawable-xhdpi/follow_light.png b/src/main/res/drawable-xhdpi/follow_light.png new file mode 100644 index 0000000000000000000000000000000000000000..ffc71c3221af6cd40ad070dabbecf78336886c8f GIT binary patch literal 4141 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 literal 0 HcmV?d00001 diff --git a/src/main/res/values/attrs.xml b/src/main/res/values/attrs.xml index bf03d6c690..68ea602928 100644 --- a/src/main/res/values/attrs.xml +++ b/src/main/res/values/attrs.xml @@ -50,6 +50,7 @@ + diff --git a/src/main/res/values/shortcuts.xml b/src/main/res/values/shortcuts.xml index 298dda76e2..5b85b0a570 100644 --- a/src/main/res/values/shortcuts.xml +++ b/src/main/res/values/shortcuts.xml @@ -17,4 +17,5 @@ m s r + f \ No newline at end of file diff --git a/src/main/res/values/strings.xml b/src/main/res/values/strings.xml index 76d1635367..37782d953c 100755 --- a/src/main/res/values/strings.xml +++ b/src/main/res/values/strings.xml @@ -1081,6 +1081,7 @@ Node selected Relation selected Creating path + Follow way Multiselect New Note selected Add segment @@ -1104,11 +1105,15 @@ Split \"via\" segment Select segment Set tags + Select end node 1 object %1$d objects Rotate way + Snap + Follow way + Select way to follow Add node Tap the screen position @@ -1125,7 +1130,6 @@ Tap the screen position Paste multiple times Tap the screen position - Snap Map background The tile based map background. diff --git a/src/main/res/values/styles.xml b/src/main/res/values/styles.xml index 3c0cec38a3..898be7bb9c 100644 --- a/src/main/res/values/styles.xml +++ b/src/main/res/values/styles.xml @@ -185,6 +185,7 @@ @drawable/bridge_dark @drawable/culvert_dark @drawable/steps_dark + @drawable/follow_dark @drawable/undolist_undo_dark @drawable/undolist_redo_dark @drawable/ic_action_warning_holo_dark @@ -263,6 +264,7 @@ @drawable/bridge_light @drawable/culvert_light @drawable/steps_light + @drawable/follow_light @drawable/undolist_undo_light @drawable/undolist_redo_light @drawable/ic_action_warning_holo_light diff --git a/svg/follow.svg b/svg/follow.svg new file mode 100644 index 0000000000..b79c0b7dd7 --- /dev/null +++ b/svg/follow.svg @@ -0,0 +1,300 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/svg/follow_dark.svg b/svg/follow_dark.svg new file mode 100644 index 0000000000..bcd3115dad --- /dev/null +++ b/svg/follow_dark.svg @@ -0,0 +1,296 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/svg/follow_light.svg b/svg/follow_light.svg new file mode 100644 index 0000000000..dcc0044899 --- /dev/null +++ b/svg/follow_light.svg @@ -0,0 +1,300 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +