Ticket #23481: josm_export_relation_to_gpx_r18721_rev03rc.patch

File josm_export_relation_to_gpx_r18721_rev03rc.patch, 20.9 KB (added by cmuelle8, 20 months ago)

cp. to previous rev02rc adds additional option to sanitize output

  • 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;
     38import org.openstreetmap.josm.data.gpx.GpxConstants;
    2739import org.openstreetmap.josm.data.gpx.GpxData;
    2840import org.openstreetmap.josm.data.gpx.GpxTrack;
    2941import org.openstreetmap.josm.data.gpx.WayPoint;
     
    3143import org.openstreetmap.josm.data.osm.Node;
    3244import org.openstreetmap.josm.data.osm.Relation;
    3345import org.openstreetmap.josm.data.osm.RelationMember;
     46import org.openstreetmap.josm.data.validation.tests.RelationChecker;
     47import org.openstreetmap.josm.gui.ExtendedDialog;
    3448import org.openstreetmap.josm.gui.MainApplication;
    3549import org.openstreetmap.josm.gui.dialogs.relation.sort.WayConnectionType;
    3650import org.openstreetmap.josm.gui.dialogs.relation.sort.WayConnectionTypeCalculator;
     
    3751import org.openstreetmap.josm.gui.layer.GpxLayer;
    3852import org.openstreetmap.josm.gui.layer.Layer;
    3953import org.openstreetmap.josm.gui.layer.OsmDataLayer;
     54import org.openstreetmap.josm.spi.preferences.Config;
     55import org.openstreetmap.josm.tools.GBC;
    4056import org.openstreetmap.josm.tools.SubclassFilteredCollection;
    4157import org.openstreetmap.josm.tools.Utils;
    4258
     
    4965public class ExportRelationToGpxAction extends GpxExportAction
    5066    implements IPrimitiveAction {
    5167
     68    private static final String SETTING_KEY = "gpx.export-from-relation";
     69
    5270    /** Enumeration of export variants */
    5371    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,
     72        /** concatenate members from last to first element, instead of first to last */
     73        LAST_MEMBER_FIRST,
    5874        /** export to GPX layer and add to LayerManager */
    5975        TO_LAYER,
    6076        /** export to GPX file and open FileChooser */
     
    6985
    7086    /** Construct a new ExportRelationToGpxAction with default mode */
    7187    public ExportRelationToGpxAction() {
    72         this(EnumSet.of(FROM_FIRST_MEMBER, TO_FILE));
     88        this(EnumSet.of(TO_FILE));
    7389    }
    7490
     91    /** A flat representation of the input data */
     92    private List<RelationMember> flat;
     93
     94    /** The relations sourced to build {@code ExportRelationToGpxAction.flat} */
     95    private List<Relation> relsFound;
     96
     97    /** Discard relation members with unknown roles if the set is not empty. */
     98    private Set<String> okRoles;
     99
     100    /** Ignore relation members' roles, treat any role as the empty role during export. */
     101    private boolean ignRoles;
     102
     103    /** Ignore node tags and timestamp, export minimal trackpoints of lat, lon, ele */
     104    private boolean minipt;
     105
    75106    /**
    76107     * Constructs a new {@code ExportRelationToGpxAction}
    77108     *
     
    86117
    87118    private static String name(Set<Mode> mode) {
    88119        if (mode.contains(TO_FILE)) {
    89             if (mode.contains(FROM_FIRST_MEMBER)) {
     120            if (!mode.contains(LAST_MEMBER_FIRST)) {
    90121                return tr("Export GPX file starting from first member");
    91122            } else {
    92123                return tr("Export GPX file starting from last member");
    93124            }
    94125        } else {
    95             if (mode.contains(FROM_FIRST_MEMBER)) {
     126            if (!mode.contains(LAST_MEMBER_FIRST)) {
    96127                return tr("Convert to GPX layer starting from first member");
    97128            } else {
    98129                return tr("Convert to GPX layer starting from last member");
     
    101132    }
    102133
    103134    private static String tooltip(Set<Mode> mode) {
    104         if (mode.contains(FROM_FIRST_MEMBER)) {
     135        if (!mode.contains(LAST_MEMBER_FIRST)) {
    105136            return tr("Flatten this relation to a single gpx track recursively, " +
    106137                    "starting with the first member, successively continuing to the last.");
    107138        } else {
     
    110141        }
    111142    }
    112143
    113     @Override
    114     protected Layer getLayer() {
    115         List<RelationMember> flat = new ArrayList<>();
     144    protected void prepareData() {
     145        ignRoles = false;
     146        okRoles = new HashSet<>();
     147        flat = new ArrayList<>();
     148        relsFound = new ArrayList<>();
    116149
    117150        List<RelationMember> init = new ArrayList<>();
    118151        relations.forEach(t -> init.add(new RelationMember("", t)));
     
    119152
    120153        Stack<Iterator<RelationMember>> stack = new Stack<>();
    121154        stack.push(modeAwareIterator(init));
    122 
    123         List<Relation> relsFound = new ArrayList<>();
    124155        do {
    125156            Iterator<RelationMember> i = stack.peek();
    126157            if (!i.hasNext())
     
    128159            while (i.hasNext()) {
    129160                RelationMember m = i.next();
    130161                if (m.isRelation() && !m.getRelation().isIncomplete()) {
    131                     final List<RelationMember> members = m.getRelation().getMembers();
     162                    List<RelationMember> members = m.getRelation().getMembers();
    132163                    stack.push(modeAwareIterator(members));
    133164                    relsFound.add(m.getRelation());
    134165                    break;
     
    138169                }
    139170            }
    140171        } while (!stack.isEmpty());
     172    }
    141173
     174    @Override
     175    protected Layer getLayer() {
    142176        GpxData gpxData = new GpxData();
    143177        final String layerName;
    144178        long time = TimeUnit.MILLISECONDS.toSeconds(System.currentTimeMillis()) - 24*3600;
    145179
     180        if (!okRoles.isEmpty()) {
     181            flat.removeIf(rm -> !okRoles.contains(rm.getRole()));
     182        } else if (ignRoles) {
     183            for (int i = 0; i < flat.size(); i++) {
     184                RelationMember rm = flat.get(i);
     185                if (!rm.getRole().isEmpty())
     186                    flat.set(i, new RelationMember("", rm.getMember()));
     187            }
     188        }
     189
    146190        if (!flat.isEmpty()) {
    147191            Map<String, Object> trkAttr = new HashMap<>();
    148192            Collection<Collection<WayPoint>> trk = new ArrayList<>();
     
    167211                                .findFirst()
    168212                                .ifPresent(r -> {
    169213                                    trkAttr.put("name", r.getName() != null ? r.getName() : Long.toString(r.getId()));
    170                                     trkAttr.put("desc", tr("based on osm route relation data, timestamps are synthetic"));
     214                                    trkAttr.put("desc", tr("based on osm route relation data")
     215                                            + (!minipt ? ", timestamps are synthetic" : ""));
    171216                                });
    172217                        GpxData.ensureUniqueName(trkAttr, names, (String) trkAttr.get("name"));
    173218                    }
     
    175220                    if (wayConnectionType.direction == WayConnectionType.Direction.BACKWARD)
    176221                        Collections.reverse(ln);
    177222                    for (Node n: ln) {
    178                         trkseg.add(OsmDataLayer.nodeToWayPoint(n, TimeUnit.SECONDS.toMillis(time)));
    179                         time += 1;
     223                        WayPoint wpt;
     224                        if (minipt) {
     225                            wpt = new WayPoint(n.getCoor());
     226                            wpt.put(GpxConstants.PT_ELE, n.hasKey(GpxConstants.GPX_PREFIX + GpxConstants.PT_ELE)
     227                                ? n.get(GpxConstants.GPX_PREFIX + GpxConstants.PT_ELE)
     228                                : n.get("ele"));
     229                        } else {
     230                            wpt = OsmDataLayer.nodeToWayPoint(n, TimeUnit.SECONDS.toMillis(time));
     231                            time += 1;
     232                        }
     233                        trkseg.add(wpt);
    180234                    }
     235                } else if (i+1 < flat.size()) {
     236                    WayConnectionType nxt = wct.get(i+1);
     237                    nxt.linkPrev &= wayConnectionType.linkPrev;
    181238                }
    182239            }
    183240            gpxData.addTrack(new GpxTrack(trk, trkAttr));
     
    194251    }
    195252
    196253    private <T> Iterator<T> modeAwareIterator(List<T> list) {
    197         return mode.contains(FROM_FIRST_MEMBER)
    198                 ? list.iterator()
    199                 : new LinkedList<>(list).descendingIterator();
     254        return mode.contains(LAST_MEMBER_FIRST)
     255                ? new LinkedList<>(list).descendingIterator()
     256                : list.iterator();
    200257    }
    201258
     259    private static Set<String> getRolesInPresets(List<Relation> lr) {
     260        return lr.stream().flatMap(n -> RelationChecker.getPresetDefinedRolesFor(n).stream()).collect(Collectors.toSet());
     261    }
     262
    202263    /**
     264     * Shows a dialog asking the user if relation members of
     265     * <li>any role</li>
     266     * <li>a user selection of roles</li>
     267     * <li>roles known/defined in relation presets</li>
     268     * should be considered when exporting.
    203269     *
     270     * The second case allows exclusion of preset-defined roles,
     271     * although this may produce unexpected results. It is an
     272     * expert feature.
     273     *
     274     * @return true if the dialog was confirmed, false if cancelled
     275     */
     276    private boolean askRelationMemberRolesToExport() {
     277        minipt = Config.getPref().getBoolean(SETTING_KEY + ".minipt", false);
     278
     279        // "do not ask again" handled here:
     280        switch (Config.getPref().get(SETTING_KEY, "ask")) {
     281            case "all":
     282                okRoles.clear();
     283                return true;
     284            case "list":
     285                okRoles.addAll(Config.getPref().getList(SETTING_KEY + ".list"));
     286                return true;
     287            case "presets":
     288                okRoles.addAll(getRolesInPresets(relsFound));
     289                return true;
     290            case "treatempty":
     291                ignRoles = true;
     292                return true;
     293        }
     294
     295        String lSel = Config.getPref().get(SETTING_KEY + ".last", "all");
     296        List<String> userRoles = Config.getPref().getList(SETTING_KEY + ".list");
     297        Set<String> rolesInData = flat.stream().map(rm -> rm.getRole()).collect(Collectors.toSet());
     298        Set<String> rolesInPresets = getRolesInPresets(relsFound);
     299
     300        // skip asking if all RelationMembers are assigned the same role
     301        if (rolesInData.size() == 1) {
     302            okRoles.clear();
     303            return true;
     304        }
     305
     306        JPanel p = new JPanel(new GridBagLayout());
     307        ButtonGroup r = new ButtonGroup();
     308
     309        p.add(new JLabel("<html><body style=\"width:404px;\">"
     310          + tr("This converts the relation to GPX format.") + "<br/><br/>"
     311          + tr("Relation members may be filtered out based on their role. Which roles should be considered by export?")
     312          + "</body></html>"), GBC.eol());
     313        JRadioButton rAll = new JRadioButton(tr("Any role"), "all".equals(lSel));
     314        r.add(rAll);
     315        p.add(rAll, GBC.eol());
     316
     317        JRadioButton rList = new JRadioButton(tr("Only selected roles:"), "list".equals(lSel));
     318        rList.setToolTipText("excluding forward / backward will impact WayConnectionTypeCalculator and thus may produce unexpected results");
     319        r.add(rList);
     320        p.add(rList, GBC.eol());
     321
     322        JPanel q = new JPanel();
     323
     324        List<JCheckBox> checkList = new ArrayList<>();
     325        ActionListener ensureAtLeastOneSelected = new ActionListener() {
     326            @Override
     327            public void actionPerformed(ActionEvent e) {
     328                if (checkList.stream().noneMatch(cb -> cb.isSelected()))
     329                    checkList.get(0).setSelected(true);
     330            }
     331        };
     332        for (String role : rolesInData) {
     333            JCheckBox cTmp = new JCheckBox(role, (userRoles.isEmpty() ? rolesInPresets : userRoles).contains(role));
     334            cTmp.addActionListener(ensureAtLeastOneSelected);
     335            checkList.add(cTmp);
     336            q.add(cTmp);
     337        }
     338
     339        q.setBorder(BorderFactory.createEmptyBorder(0, 20, 5, 0));
     340        p.add(q, GBC.eol());
     341
     342        JRadioButton rPre = new JRadioButton(tr("Roles as defined in presets"), "presets".equals(lSel));
     343        r.add(rPre);
     344        p.add(rPre, GBC.eol());
     345
     346        JRadioButton rIgn = new JRadioButton(tr("Ignore roles / treat all as empty"), "treatempty".equals(lSel));
     347        r.add(rIgn);
     348        p.add(rIgn, GBC.eol());
     349
     350        rList.addActionListener(new ActionListener() {
     351            @Override
     352            public void actionPerformed(ActionEvent e) {
     353                for (JCheckBox ch : checkList) {
     354                    ch.setEnabled(true);
     355                }
     356            }
     357        });
     358        ActionListener disabler = new ActionListener() {
     359            @Override
     360            public void actionPerformed(ActionEvent e) {
     361                for (JCheckBox ch : checkList) {
     362                    ch.setEnabled(false);
     363                }
     364            }
     365        };
     366        rAll.addActionListener(disabler);
     367        rPre.addActionListener(disabler);
     368        rIgn.addActionListener(disabler);
     369
     370        if (!"list".equals(lSel)) {
     371            disabler.actionPerformed(null);
     372        }
     373
     374        p.add(new JLabel("<html><body style=\"width:404px;\"><br/>"
     375          + tr("Some node tags are exported to trackpoint attributes by default. Exclude them?")
     376          + "</body></html>"), GBC.eol());
     377        JCheckBox cbMiniPt = new JCheckBox("Write minimal trackpoints", minipt);
     378        cbMiniPt.setToolTipText("export lat, lon and possibly ele; discard other tags and timestamps");
     379        p.add(cbMiniPt, GBC.eol());
     380
     381        ExtendedDialog ed = new ExtendedDialog(MainApplication.getMainFrame(), tr("Options"),
     382                tr("Convert"), tr("Convert and remember selection"), tr("Cancel"))
     383                .setButtonIcons("exportgpx", "exportgpx", "cancel").setContent(p);
     384        int ret = ed.showDialog().getValue();
     385
     386        if (ret == 1 || ret == 2) {
     387            userRoles = checkList.stream().filter(cb -> cb.isSelected()).map(cb -> cb.getText()).collect(Collectors.toList());
     388            String sel = rAll.isSelected() ? "all" : (rIgn.isSelected() ? "treatempty" : (rPre.isSelected() ? "presets" : "list"));
     389            Config.getPref().put(SETTING_KEY + ".last", sel);
     390            Config.getPref().put(SETTING_KEY, (ret == 2) ? sel : "ask");
     391            switch (sel) {
     392            case "all":
     393                okRoles.clear();
     394                break;
     395            case "list":
     396                okRoles.addAll(userRoles);
     397                Config.getPref().putList(SETTING_KEY + ".list", userRoles);
     398                break;
     399            case "presets":
     400                okRoles.addAll(rolesInPresets);
     401                break;
     402            case "treatempty":
     403                ignRoles = true;
     404                break;
     405            }
     406
     407            minipt = cbMiniPt.isSelected();
     408            Config.getPref().putBoolean(SETTING_KEY + ".minipt", minipt);
     409        } else {
     410            return false;
     411        }
     412
     413        return true;
     414    }
     415
     416    /**
     417     *
    204418     * @param e the ActionEvent
    205419     */
    206420    @Override
    207421    public void actionPerformed(ActionEvent e) {
     422        prepareData();
     423        if (!askRelationMemberRolesToExport())
     424            return;
    208425        if (mode.contains(TO_LAYER))
    209426            MainApplication.getLayerManager().addLayer(getLayer());
    210427        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);