Ticket #23481: josm_export_relation_to_gpx_r18721_rev02rc.patch

File josm_export_relation_to_gpx_r18721_rev02rc.patch, 18.8 KB (added by cmuelle8, 20 months ago)

revamp export relation to gpx feature

  • src/org/openstreetmap/josm/actions/relation/ExportRelationToGpxAction.java

     
    11// License: GPL. For details, see LICENSE file.
    22package org.openstreetmap.josm.actions.relation;
    33
    4 import static org.openstreetmap.josm.actions.relation.ExportRelationToGpxAction.Mode.FROM_FIRST_MEMBER;
     4import static org.openstreetmap.josm.actions.relation.ExportRelationToGpxAction.Mode.LAST_MEMBER_FIRST;
    55import static org.openstreetmap.josm.actions.relation.ExportRelationToGpxAction.Mode.TO_FILE;
    66import static org.openstreetmap.josm.actions.relation.ExportRelationToGpxAction.Mode.TO_LAYER;
    77import static org.openstreetmap.josm.gui.help.HelpUtil.ht;
    88import static org.openstreetmap.josm.tools.I18n.tr;
    99
     10import java.awt.GridBagLayout;
    1011import java.awt.event.ActionEvent;
     12import java.awt.event.ActionListener;
    1113import java.util.ArrayList;
    1214import java.util.Arrays;
    1315import java.util.Collection;
     
    1416import java.util.Collections;
    1517import java.util.EnumSet;
    1618import java.util.HashMap;
     19import java.util.HashSet;
    1720import java.util.Iterator;
    1821import java.util.LinkedList;
    1922import java.util.List;
     
    2124import java.util.Set;
    2225import java.util.Stack;
    2326import java.util.concurrent.TimeUnit;
     27import java.util.stream.Collectors;
    2428
     29import javax.swing.BorderFactory;
     30import javax.swing.ButtonGroup;
     31import javax.swing.JCheckBox;
     32import javax.swing.JLabel;
     33import javax.swing.JPanel;
     34import javax.swing.JRadioButton;
     35
    2536import org.openstreetmap.josm.actions.GpxExportAction;
    2637import org.openstreetmap.josm.actions.IPrimitiveAction;
    2738import org.openstreetmap.josm.data.gpx.GpxData;
     
    3142import org.openstreetmap.josm.data.osm.Node;
    3243import org.openstreetmap.josm.data.osm.Relation;
    3344import org.openstreetmap.josm.data.osm.RelationMember;
     45import org.openstreetmap.josm.data.validation.tests.RelationChecker;
     46import org.openstreetmap.josm.gui.ExtendedDialog;
    3447import org.openstreetmap.josm.gui.MainApplication;
    3548import org.openstreetmap.josm.gui.dialogs.relation.sort.WayConnectionType;
    3649import org.openstreetmap.josm.gui.dialogs.relation.sort.WayConnectionTypeCalculator;
     
    3750import org.openstreetmap.josm.gui.layer.GpxLayer;
    3851import org.openstreetmap.josm.gui.layer.Layer;
    3952import org.openstreetmap.josm.gui.layer.OsmDataLayer;
     53import org.openstreetmap.josm.spi.preferences.Config;
     54import org.openstreetmap.josm.tools.GBC;
    4055import org.openstreetmap.josm.tools.SubclassFilteredCollection;
    4156import org.openstreetmap.josm.tools.Utils;
    4257
     
    4964public class ExportRelationToGpxAction extends GpxExportAction
    5065    implements IPrimitiveAction {
    5166
     67    private static final String SETTING_KEY = "gpx.export-from-relation";
     68
    5269    /** Enumeration of export variants */
    5370    public enum Mode {
    54         /** concatenate members from first to last element */
    55         FROM_FIRST_MEMBER,
    56         /** concatenate members from last to first element */
    57         FROM_LAST_MEMBER,
     71        /** concatenate members from last to first element, instead of first to last */
     72        LAST_MEMBER_FIRST,
    5873        /** export to GPX layer and add to LayerManager */
    5974        TO_LAYER,
    6075        /** export to GPX file and open FileChooser */
     
    6984
    7085    /** Construct a new ExportRelationToGpxAction with default mode */
    7186    public ExportRelationToGpxAction() {
    72         this(EnumSet.of(FROM_FIRST_MEMBER, TO_FILE));
     87        this(EnumSet.of(TO_FILE));
    7388    }
    7489
     90    /** A flat representation of the input data */
     91    private List<RelationMember> flat;
     92
     93    /** The relations sourced to build {@code ExportRelationToGpxAction.flat} */
     94    private List<Relation> relsFound;
     95
     96    /** Discard relation members with unknown roles if the set is not empty. */
     97    private Set<String> okRoles;
     98
     99    /** Ignore relation members' roles, treat any role as the empty role during export. */
     100    private List<Boolean> ignRoles;
     101
    75102    /**
    76103     * Constructs a new {@code ExportRelationToGpxAction}
    77104     *
     
    86113
    87114    private static String name(Set<Mode> mode) {
    88115        if (mode.contains(TO_FILE)) {
    89             if (mode.contains(FROM_FIRST_MEMBER)) {
     116            if (!mode.contains(LAST_MEMBER_FIRST)) {
    90117                return tr("Export GPX file starting from first member");
    91118            } else {
    92119                return tr("Export GPX file starting from last member");
    93120            }
    94121        } else {
    95             if (mode.contains(FROM_FIRST_MEMBER)) {
     122            if (!mode.contains(LAST_MEMBER_FIRST)) {
    96123                return tr("Convert to GPX layer starting from first member");
    97124            } else {
    98125                return tr("Convert to GPX layer starting from last member");
     
    101128    }
    102129
    103130    private static String tooltip(Set<Mode> mode) {
    104         if (mode.contains(FROM_FIRST_MEMBER)) {
     131        if (!mode.contains(LAST_MEMBER_FIRST)) {
    105132            return tr("Flatten this relation to a single gpx track recursively, " +
    106133                    "starting with the first member, successively continuing to the last.");
    107134        } else {
     
    110137        }
    111138    }
    112139
    113     @Override
    114     protected Layer getLayer() {
    115         List<RelationMember> flat = new ArrayList<>();
     140    protected void prepareData() {
     141        ignRoles = new ArrayList<>();
     142        okRoles = new HashSet<>();
     143        flat = new ArrayList<>();
     144        relsFound = new ArrayList<>();
    116145
    117146        List<RelationMember> init = new ArrayList<>();
    118147        relations.forEach(t -> init.add(new RelationMember("", t)));
     
    119148
    120149        Stack<Iterator<RelationMember>> stack = new Stack<>();
    121150        stack.push(modeAwareIterator(init));
    122 
    123         List<Relation> relsFound = new ArrayList<>();
    124151        do {
    125152            Iterator<RelationMember> i = stack.peek();
    126153            if (!i.hasNext())
     
    128155            while (i.hasNext()) {
    129156                RelationMember m = i.next();
    130157                if (m.isRelation() && !m.getRelation().isIncomplete()) {
    131                     final List<RelationMember> members = m.getRelation().getMembers();
     158                    List<RelationMember> members = m.getRelation().getMembers();
    132159                    stack.push(modeAwareIterator(members));
    133160                    relsFound.add(m.getRelation());
    134161                    break;
     
    138165                }
    139166            }
    140167        } while (!stack.isEmpty());
     168    }
    141169
     170    @Override
     171    protected Layer getLayer() {
    142172        GpxData gpxData = new GpxData();
    143173        final String layerName;
    144174        long time = TimeUnit.MILLISECONDS.toSeconds(System.currentTimeMillis()) - 24*3600;
    145175
     176        if (!okRoles.isEmpty()) {
     177            flat.removeIf(rm -> !okRoles.contains(rm.getRole()));
     178        } else if (!ignRoles.isEmpty()) {
     179            for (int i = 0; i < flat.size(); i++) {
     180                RelationMember rm = flat.get(i);
     181                if (!rm.getRole().isEmpty())
     182                    flat.set(i, new RelationMember("", rm.getMember()));
     183            }
     184        }
     185
    146186        if (!flat.isEmpty()) {
    147187            Map<String, Object> trkAttr = new HashMap<>();
    148188            Collection<Collection<WayPoint>> trk = new ArrayList<>();
     
    178218                        trkseg.add(OsmDataLayer.nodeToWayPoint(n, TimeUnit.SECONDS.toMillis(time)));
    179219                        time += 1;
    180220                    }
     221                } else if (i+1 < flat.size()) {
     222                    WayConnectionType nxt = wct.get(i+1);
     223                    nxt.linkPrev &= wayConnectionType.linkPrev;
    181224                }
    182225            }
    183226            gpxData.addTrack(new GpxTrack(trk, trkAttr));
     
    194237    }
    195238
    196239    private <T> Iterator<T> modeAwareIterator(List<T> list) {
    197         return mode.contains(FROM_FIRST_MEMBER)
    198                 ? list.iterator()
    199                 : new LinkedList<>(list).descendingIterator();
     240        return mode.contains(LAST_MEMBER_FIRST)
     241                ? new LinkedList<>(list).descendingIterator()
     242                : list.iterator();
    200243    }
    201244
     245    private static Set<String> getRolesInPresets(List<Relation> lr) {
     246        return lr.stream().flatMap(n -> RelationChecker.getPresetDefinedRolesFor(n).stream()).collect(Collectors.toSet());
     247    }
     248
    202249    /**
     250     * Shows a dialog asking the user if relation members of
     251     * <li>any role</li>
     252     * <li>a user selection of roles</li>
     253     * <li>roles known/defined in relation presets<li>
     254     * should be considered when exporting.
    203255     *
     256     * The second case allows exclusion of preset-defined roles,
     257     * although this may produce unexpected results. It is an
     258     * expert feature.
     259     *
     260     * @param lrm a flat list of RelationMembers about to be exported
     261     * @param lr a list of relations lrm was sourced from
     262     * @param oR okRoles, possibly modified by the method
     263     *
     264     * @return true if the dialog was confirmed, false if cancelled
     265     */
     266    private static boolean askRelationMemberRolesToExport(List<RelationMember> lrm, List<Relation> lr, Set<String> oR, List<Boolean> iR) {
     267        // "do not ask again" handled here:
     268        switch (Config.getPref().get(SETTING_KEY, "ask")) {
     269            case "all":
     270                oR.clear();
     271                return true;
     272            case "list":
     273                oR.addAll(Config.getPref().getList(SETTING_KEY + ".list"));
     274                return true;
     275            case "presets":
     276                oR.addAll(getRolesInPresets(lr));
     277                return true;
     278            case "treatempty":
     279                iR.add(true);
     280                return true;
     281        }
     282
     283        String lSel = Config.getPref().get(SETTING_KEY + ".last", "all");
     284        List<String> userRoles = Config.getPref().getList(SETTING_KEY + ".list");
     285        Set<String> rolesInData = lrm.stream().map(rm -> rm.getRole()).collect(Collectors.toSet());
     286        Set<String> rolesInPresets = getRolesInPresets(lr);
     287
     288        // skip asking if all RelationMembers are assigned the same role
     289        if (rolesInData.size() == 1) {
     290            oR.clear();
     291            return true;
     292        }
     293
     294        JPanel p = new JPanel(new GridBagLayout());
     295        ButtonGroup r = new ButtonGroup();
     296
     297        p.add(new JLabel("<html><body style=\"width:404px;\">"
     298          + tr("This converts the relation to GPX format.") + "<br><br>"
     299          + tr("Relation members may be filtered out based on their role. Which roles should be considered by export?")
     300          + "</body></html>"), GBC.eol());
     301        JRadioButton rAll = new JRadioButton(tr("Any role"), "all".equals(lSel));
     302        r.add(rAll);
     303        p.add(rAll, GBC.eol());
     304
     305        JRadioButton rList = new JRadioButton(tr("Only selected roles:"), "list".equals(lSel));
     306        rList.setToolTipText("excluding forward / backward will impact WayConnectionTypeCalculator and thus may produce unexpected results");
     307        r.add(rList);
     308        p.add(rList, GBC.eol());
     309
     310        JPanel q = new JPanel();
     311
     312        List<JCheckBox> checkList = new ArrayList<>();
     313        ActionListener ensureAtLeastOneSelected = new ActionListener() {
     314            @Override
     315            public void actionPerformed(ActionEvent e) {
     316                if (checkList.stream().noneMatch(cb -> cb.isSelected()))
     317                    checkList.get(0).setSelected(true);
     318            }
     319        };
     320        for (String role : rolesInData) {
     321            JCheckBox cTmp = new JCheckBox(role, (userRoles.isEmpty() ? rolesInPresets : userRoles).contains(role));
     322            cTmp.addActionListener(ensureAtLeastOneSelected);
     323            checkList.add(cTmp);
     324            q.add(cTmp);
     325        }
     326
     327        q.setBorder(BorderFactory.createEmptyBorder(0, 20, 5, 0));
     328        p.add(q, GBC.eol());
     329
     330        JRadioButton rPre = new JRadioButton(tr("Roles as defined in presets"), "presets".equals(lSel));
     331        r.add(rPre);
     332        p.add(rPre, GBC.eol());
     333
     334        JRadioButton rIgn = new JRadioButton(tr("Ignore roles / treat all as empty"), "treatempty".equals(lSel));
     335        r.add(rIgn);
     336        p.add(rIgn, GBC.eol());
     337
     338        rList.addActionListener(new ActionListener() {
     339            @Override
     340            public void actionPerformed(ActionEvent e) {
     341                for (JCheckBox ch : checkList) {
     342                    ch.setEnabled(true);
     343                }
     344            }
     345        });
     346        ActionListener disabler = new ActionListener() {
     347            @Override
     348            public void actionPerformed(ActionEvent e) {
     349                for (JCheckBox ch : checkList) {
     350                    ch.setEnabled(false);
     351                }
     352            }
     353        };
     354        rAll.addActionListener(disabler);
     355        rPre.addActionListener(disabler);
     356        rIgn.addActionListener(disabler);
     357
     358        if (!"list".equals(lSel)) {
     359            disabler.actionPerformed(null);
     360        }
     361
     362        ExtendedDialog ed = new ExtendedDialog(MainApplication.getMainFrame(), tr("Options"),
     363                tr("Convert"), tr("Convert and remember selection"), tr("Cancel"))
     364                .setButtonIcons("exportgpx", "exportgpx", "cancel").setContent(p);
     365        int ret = ed.showDialog().getValue();
     366
     367        if (ret == 1 || ret == 2) {
     368            userRoles = checkList.stream().filter(cb -> cb.isSelected()).map(cb -> cb.getText()).collect(Collectors.toList());
     369            String sel = rAll.isSelected() ? "all" : (rIgn.isSelected() ? "treatempty" : (rPre.isSelected() ? "presets" : "list"));
     370            Config.getPref().put(SETTING_KEY + ".last", sel);
     371            Config.getPref().put(SETTING_KEY, (ret == 2) ? sel : "ask");
     372            switch (sel) {
     373            case "all":
     374                oR.clear();
     375                break;
     376            case "list":
     377                oR.addAll(userRoles);
     378                Config.getPref().putList(SETTING_KEY + ".list", userRoles);
     379                break;
     380            case "presets":
     381                oR.addAll(rolesInPresets);
     382                break;
     383            case "treatempty":
     384                iR.add(true);
     385                break;
     386            }
     387        } else {
     388            return false;
     389        }
     390
     391        return true;
     392    }
     393
     394    /**
     395     *
    204396     * @param e the ActionEvent
    205397     */
    206398    @Override
    207399    public void actionPerformed(ActionEvent e) {
     400        prepareData();
     401        if (!askRelationMemberRolesToExport(flat, relsFound, okRoles, ignRoles))
     402            return;
    208403        if (mode.contains(TO_LAYER))
    209404            MainApplication.getLayerManager().addLayer(getLayer());
    210405        if (mode.contains(TO_FILE))
  • src/org/openstreetmap/josm/data/osm/Relation.java

     
    556556        return Stream.of(members).map(RelationMember::getRole).filter(role -> !role.isEmpty()).collect(Collectors.toSet());
    557557    }
    558558
     559    public List<? extends OsmPrimitive> findRelationMembersWithUnknownRole(Set<String> knownRoles) {
     560        return Stream.of(members)
     561                .filter(m -> knownRoles.isEmpty() || knownRoles.stream().noneMatch(kr -> kr.equals(m.getRole())))
     562                .map(RelationMember::getMember).collect(Collectors.toList());
     563    }
     564
    559565    @Override
    560566    public List<? extends OsmPrimitive> findRelationMembers(String role) {
    561567        return IRelation.super.findRelationMembers(role).stream()
  • src/org/openstreetmap/josm/data/validation/tests/RelationChecker.java

     
    1515import java.util.LinkedList;
    1616import java.util.List;
    1717import java.util.Map;
     18import java.util.Set;
    1819import java.util.stream.Collectors;
    1920
    2021import org.openstreetmap.josm.command.ChangeMembersCommand;
     
    480481            return Collections.unmodifiableList(test.loops.iterator().next());
    481482    }
    482483
     484    /**
     485     * Check RelationMember roles to be known in presets, report members with unknown roles.
     486     * @param n the relation to check
     487     * @return An empty list if all members have known roles, or a list of members with preset-wise unknown roles.
     488     */
     489    public static List<? extends OsmPrimitive> checkMembersForRoleUnknownInPresets(Relation n) {
     490        return n.findRelationMembersWithUnknownRole(getPresetDefinedRolesFor(n));
     491    }
     492
     493    /**
     494     * Reply a set of preset-defined "known" roles for the type of relation passed.
     495     *
     496     * The data of the relation instance' members is irrelevant for this call.
     497     * Preset data is looked up based on the relation tags, in particular its
     498     * type tag.  Thus, the returned set may very well contain roles not as-
     499     * signed to any of the passed instance' members.
     500     *
     501     * @param n the relation whose type the preset-defined roles should be determined for
     502     * @return a set of known roles for the relation's type.
     503     */
     504    public static Set<String> getPresetDefinedRolesFor(Relation n) {
     505        initializePresets();
     506        Map<Role, String> allroles = buildAllRoles(n);
     507        return allroles.keySet().stream().map(r -> r.key).collect(Collectors.toSet());
     508    }
     509
    483510}
  • src/org/openstreetmap/josm/gui/dialogs/RelationListDialog.java

     
    129129
    130130    /** export relation to GPX track action */
    131131    private final ExportRelationToGpxAction exportRelationFromFirstAction =
    132             new ExportRelationToGpxAction(EnumSet.of(Mode.FROM_FIRST_MEMBER, Mode.TO_FILE));
     132            new ExportRelationToGpxAction(EnumSet.of(Mode.TO_FILE));
    133133    private final ExportRelationToGpxAction exportRelationFromLastAction =
    134             new ExportRelationToGpxAction(EnumSet.of(Mode.FROM_LAST_MEMBER, Mode.TO_FILE));
     134            new ExportRelationToGpxAction(EnumSet.of(Mode.LAST_MEMBER_FIRST, Mode.TO_FILE));
    135135    private final ExportRelationToGpxAction exportRelationFromFirstToLayerAction =
    136             new ExportRelationToGpxAction(EnumSet.of(Mode.FROM_FIRST_MEMBER, Mode.TO_LAYER));
     136            new ExportRelationToGpxAction(EnumSet.of(Mode.TO_LAYER));
    137137    private final ExportRelationToGpxAction exportRelationFromLastToLayerAction =
    138             new ExportRelationToGpxAction(EnumSet.of(Mode.FROM_LAST_MEMBER, Mode.TO_LAYER));
     138            new ExportRelationToGpxAction(EnumSet.of(Mode.LAST_MEMBER_FIRST, Mode.TO_LAYER));
    139139
    140140    private final transient HighlightHelper highlightHelper = new HighlightHelper();
    141141    private final boolean highlightEnabled = Config.getPref().getBoolean("draw.target-highlight", true);