Changeset 18365 in josm


Ignore:
Timestamp:
2022-01-25T19:05:27+01:00 (4 months ago)
Author:
Don-vip
Message:

see #15182 - make JOSM callable as standalone validator (patch by taylor.smock)

Location:
trunk
Files:
4 added
9 edited

Legend:

Unmodified
Added
Removed
  • trunk/src/org/openstreetmap/josm/data/validation/tests/MapCSSTagChecker.java

    r18275 r18365  
    255255
    256256    /**
    257      * A handler for assertion error messages (for not fulfilled "assertMatch", "assertNoMatch").
    258      */
    259     @FunctionalInterface
    260     interface AssertionConsumer extends Consumer<String> {
    261     }
    262 
    263     /**
    264257     * Adds a new MapCSS config file from the given URL.
    265258     * @param url The unique URL of the MapCSS config file
     
    275268    }
    276269
    277     synchronized ParseResult addMapCSS(String url, AssertionConsumer assertionConsumer) throws ParseException, IOException {
     270    /**
     271     * Adds a new MapCSS config file from the given URL. <br />
     272     * NOTE: You should prefer {@link #addMapCSS(String)} unless you <i>need</i> to know what the assertions return.
     273     *
     274     * @param url The unique URL of the MapCSS config file
     275     * @param assertionConsumer A string consumer for error messages.
     276     * @return List of tag checks and parsing errors, or null
     277     * @throws ParseException if the config file does not match MapCSS syntax
     278     * @throws IOException if any I/O error occurs
     279     * @since 18365 (public, primarily for ValidatorCLI)
     280     */
     281    public synchronized ParseResult addMapCSS(String url, Consumer<String> assertionConsumer) throws ParseException, IOException {
    278282        CheckParameterUtil.ensureParameterNotNull(url, "url");
    279283        ParseResult result;
  • trunk/src/org/openstreetmap/josm/data/validation/tests/MapCSSTagCheckerAsserts.java

    r17981 r18365  
    1111import java.util.Optional;
    1212import java.util.Set;
     13import java.util.function.Consumer;
    1314import java.util.stream.Collectors;
    1415
     
    4748     */
    4849    static void checkAsserts(final MapCSSTagCheckerRule check, final Map<String, Boolean> assertions,
    49                              final MapCSSTagChecker.AssertionConsumer assertionConsumer) {
     50                             final Consumer<String> assertionConsumer) {
    5051        final DataSet ds = new DataSet();
    5152        Logging.debug("Check: {0}", check);
  • trunk/src/org/openstreetmap/josm/data/validation/tests/MapCSSTagCheckerRule.java

    r18193 r18365  
    1616import java.util.Optional;
    1717import java.util.Set;
     18import java.util.function.Consumer;
    1819import java.util.function.Predicate;
    1920import java.util.regex.Matcher;
     
    3233import org.openstreetmap.josm.data.validation.Test;
    3334import org.openstreetmap.josm.data.validation.TestError;
    34 import org.openstreetmap.josm.data.validation.tests.MapCSSTagChecker.AssertionConsumer;
    3535import org.openstreetmap.josm.gui.mappaint.Environment;
    3636import org.openstreetmap.josm.gui.mappaint.Keyword;
     
    107107    private static final String POSSIBLE_THROWS = "throwError/throwWarning/throwOther";
    108108
    109     static MapCSSTagCheckerRule ofMapCSSRule(final MapCSSRule rule, AssertionConsumer assertionConsumer) throws IllegalDataException {
     109    static MapCSSTagCheckerRule ofMapCSSRule(final MapCSSRule rule, Consumer<String> assertionConsumer) throws IllegalDataException {
    110110        final MapCSSTagCheckerRule check = new MapCSSTagCheckerRule(rule);
    111111        final Map<String, Boolean> assertions = new HashMap<>();
     
    186186    }
    187187
    188     static MapCSSTagChecker.ParseResult readMapCSS(Reader css, AssertionConsumer assertionConsumer) throws ParseException {
     188    static MapCSSTagChecker.ParseResult readMapCSS(Reader css, Consumer<String> assertionConsumer) throws ParseException {
    189189        CheckParameterUtil.ensureParameterNotNull(css, "css");
    190190
  • trunk/src/org/openstreetmap/josm/gui/MainApplication.java

    r18361 r18365  
    9999import org.openstreetmap.josm.data.projection.datum.NTV2GridShiftFileWrapper;
    100100import org.openstreetmap.josm.data.projection.datum.NTV2Proj4DirGridShiftFileSource;
     101import org.openstreetmap.josm.data.validation.ValidatorCLI;
    101102import org.openstreetmap.josm.data.validation.tests.MapCSSTagChecker;
    102103import org.openstreetmap.josm.gui.ProgramArguments.Option;
     
    312313        registerCLIModule(ProjectionCLI.INSTANCE);
    313314        registerCLIModule(RenderingCLI.INSTANCE);
     315        registerCLIModule(ValidatorCLI.INSTANCE);
    314316    }
    315317
     
    661663                "\trunjosm     "+tr("launch JOSM (default, performed when no command is specified)")+'\n'+
    662664                "\trender      "+tr("render data and save the result to an image file")+'\n'+
    663                 "\tproject     "+tr("convert coordinates from one coordinate reference system to another")+"\n\n"+
     665                "\tproject     " + tr("convert coordinates from one coordinate reference system to another")+ '\n' +
     666                "\tvalidate    " + tr("validate data") + "\n\n" +
    664667                tr("For details on the {0} and {1} commands, run them with the {2} option.", "render", "project", "--help")+'\n'+
    665668                tr("The remainder of this help page documents the {0} command.", "runjosm")+"\n\n"+
  • trunk/src/org/openstreetmap/josm/gui/io/importexport/OsmImporter.java

    r14717 r18365  
    9898                associatedFile == null ? OsmDataLayer.createNewName() : associatedFile.getName(), pm);
    9999
     100        final OsmDataLayer layer = data.getLayer();
     101        // Note: addLayer calls GuiHelper.runInEDTAndWaitWithException
     102        MainApplication.getLayerManager().addLayer(layer);
    100103        // FIXME: remove UI stuff from IO subsystem
    101104        GuiHelper.runInEDT(() -> {
    102             OsmDataLayer layer = data.getLayer();
    103             MainApplication.getLayerManager().addLayer(layer);
    104105            data.getPostLayerTask().run();
    105106            data.getLayer().onPostLoadFromFile();
  • trunk/src/org/openstreetmap/josm/gui/preferences/projection/CustomProjectionChoice.java

    r18211 r18365  
    2424import org.openstreetmap.josm.data.projection.Projections;
    2525import org.openstreetmap.josm.gui.ExtendedDialog;
     26import org.openstreetmap.josm.gui.tagging.ac.AutoCompTextField;
    2627import org.openstreetmap.josm.gui.widgets.AbstractTextComponentValidator;
    2728import org.openstreetmap.josm.gui.widgets.HistoryComboBox;
     
    5354    private static class PreferencePanel extends JPanel {
    5455
    55         public JosmTextField input;
     56        public AutoCompTextField<String> input;
    5657        private HistoryComboBox cbInput;
    5758
     
    6162
    6263        private void build(String initialText, final ActionListener listener) {
    63             input = new JosmTextField(30);
     64            input = new AutoCompTextField<>(30);
    6465            cbInput = new HistoryComboBox();
    6566            cbInput.setEditor(new BasicComboBoxEditor() {
  • trunk/src/org/openstreetmap/josm/gui/progress/AbstractProgressMonitor.java

    r12675 r18365  
    234234    ==================*/
    235235
     236    /**
     237     * Update progress message
     238     * @param value The percentage of completion (this and child progress)
     239     */
    236240    protected abstract void updateProgress(double value);
    237241
  • trunk/src/org/openstreetmap/josm/gui/util/GuiHelper.java

    r17090 r18365  
    289289     */
    290290    public static void assertCallFromEdt() {
    291         if (!SwingUtilities.isEventDispatchThread()) {
     291        if (!SwingUtilities.isEventDispatchThread() && !GraphicsEnvironment.isHeadless()) {
    292292            throw new IllegalStateException(
    293293                    "Needs to be called from the EDT thread, not from " + Thread.currentThread().getName());
  • trunk/src/org/openstreetmap/josm/io/GeoJSONWriter.java

    r17570 r18365  
    77import java.math.BigDecimal;
    88import java.math.RoundingMode;
     9import java.time.Instant;
     10import java.util.ArrayList;
     11import java.util.Arrays;
    912import java.util.Collection;
    1013import java.util.Collections;
     14import java.util.EnumSet;
    1115import java.util.HashSet;
    1216import java.util.Iterator;
    1317import java.util.List;
    1418import java.util.Map;
    15 import java.util.Map.Entry;
    1619import java.util.Set;
     20import java.util.stream.Collectors;
    1721import java.util.stream.Stream;
    1822
     
    3135import org.openstreetmap.josm.data.coor.LatLon;
    3236import org.openstreetmap.josm.data.osm.DataSet;
     37import org.openstreetmap.josm.data.osm.INode;
     38import org.openstreetmap.josm.data.osm.IWay;
    3339import org.openstreetmap.josm.data.osm.MultipolygonBuilder;
    34 import org.openstreetmap.josm.data.osm.MultipolygonBuilder.JoinedPolygon;
    3540import org.openstreetmap.josm.data.osm.Node;
    3641import org.openstreetmap.josm.data.osm.OsmPrimitive;
    3742import org.openstreetmap.josm.data.osm.Relation;
     43import org.openstreetmap.josm.data.osm.RelationMember;
    3844import org.openstreetmap.josm.data.osm.Way;
    3945import org.openstreetmap.josm.data.osm.visitor.OsmPrimitiveVisitor;
     
    4248import org.openstreetmap.josm.data.projection.Projections;
    4349import org.openstreetmap.josm.gui.mappaint.ElemStyles;
     50import org.openstreetmap.josm.tools.Geometry;
    4451import org.openstreetmap.josm.tools.Logging;
    4552import org.openstreetmap.josm.tools.Pair;
     53import org.openstreetmap.josm.tools.Utils;
    4654
    4755/**
     
    5260public class GeoJSONWriter {
    5361
     62    enum Options {
     63        /** If using the right hand rule, we have to ensure that the "right" side is the interior of the object. */
     64        RIGHT_HAND_RULE,
     65        /** Write OSM information to the feature properties field. This tries to follow the Overpass turbo format. */
     66        WRITE_OSM_INFORMATION,
     67        /** Skip empty nodes */
     68        SKIP_EMPTY_NODES
     69    }
     70
    5471    private final DataSet data;
    55     private final Projection projection;
     72    private static final Projection projection = Projections.getProjectionByCode("EPSG:4326"); // WGS 84
    5673    private static final BooleanProperty SKIP_EMPTY_NODES = new BooleanProperty("geojson.export.skip-empty-nodes", true);
    5774    private static final BooleanProperty UNTAGGED_CLOSED_IS_POLYGON = new BooleanProperty("geojson.export.untagged-closed-is-polygon", false);
    5875    private static final Set<Way> processedMultipolygonWays = new HashSet<>();
     76    private EnumSet<Options> options = EnumSet.noneOf(Options.class);
    5977
    6078    /**
     
    7896    public GeoJSONWriter(DataSet ds) {
    7997        this.data = ds;
    80         this.projection = Projections.getProjectionByCode("EPSG:4326"); // WGS 84
     98        if (Boolean.TRUE.equals(SKIP_EMPTY_NODES.get())) {
     99            this.options.add(Options.SKIP_EMPTY_NODES);
     100        }
     101    }
     102
     103    /**
     104     * Set the options for this writer. See {@link Options}.
     105     * @param options The options to set.
     106     */
     107    void setOptions(final Options... options) {
     108        this.options.clear();
     109        this.options.addAll(Arrays.asList(options));
    81110    }
    82111
     
    118147    }
    119148
     149    /**
     150     * Convert a primitive to a json object
     151     */
    120152    private class GeometryPrimitiveVisitor implements OsmPrimitiveVisitor {
    121153
     
    142174                    return;
    143175                }
    144                 final JsonArrayBuilder array = getCoorsArray(w.getNodes());
    145176                boolean writeAsPolygon = w.isClosed() && ((!w.isTagged() && UNTAGGED_CLOSED_IS_POLYGON.get())
    146177                        || ElemStyles.hasAreaElemStyle(w, false));
     178                final List<Node> nodes = w.getNodes();
     179                if (writeAsPolygon && options.contains(Options.RIGHT_HAND_RULE) && Geometry.isClockwise(nodes)) {
     180                    Collections.reverse(nodes);
     181                }
     182                final JsonArrayBuilder array = getCoorsArray(nodes);
    147183                if (writeAsPolygon) {
    148184                    geomObj.add("type", "Polygon");
     
    160196                return;
    161197            }
    162             try {
    163                 final Pair<List<JoinedPolygon>, List<JoinedPolygon>> mp = MultipolygonBuilder.joinWays(r);
     198            if (r.isMultipolygon()) {
     199                try {
     200                    this.visitMultipolygon(r);
     201                    return;
     202                } catch (MultipolygonBuilder.JoinedPolygonCreationException ex) {
     203                    Logging.warn("GeoJSON: Failed to export multipolygon {0}, falling back to other multi geometry types", r.getUniqueId());
     204                    Logging.warn(ex);
     205                }
     206            }
     207            // These are run if (a) r is not a multipolygon or (b) r is not a well-formed multipolygon.
     208            if (r.getMemberPrimitives().stream().allMatch(IWay.class::isInstance)) {
     209                this.visitMultiLineString(r);
     210            } else if (r.getMemberPrimitives().stream().allMatch(INode.class::isInstance)) {
     211                this.visitMultiPoints(r);
     212            } else {
     213                this.visitMultiGeometry(r);
     214            }
     215        }
     216
     217        /**
     218         * Visit a multi-part geometry.
     219         * Note: Does not currently recurse down relations. RFC 7946 indicates that we
     220         * should avoid nested geometry collections. This behavior may change any time in the future!
     221         * @param r The relation to visit.
     222         */
     223        private void visitMultiGeometry(final Relation r) {
     224            final JsonArrayBuilder jsonArrayBuilder = Json.createArrayBuilder();
     225            r.getMemberPrimitives().stream().filter(p -> !(p instanceof Relation))
     226                    .map(p -> {
     227                        final JsonObjectBuilder tempGeomObj = Json.createObjectBuilder();
     228                        p.accept(new GeometryPrimitiveVisitor(tempGeomObj));
     229                        return tempGeomObj.build();
     230                    }).forEach(jsonArrayBuilder::add);
     231            geomObj.add("type", "GeometryCollection");
     232            geomObj.add("geometries", jsonArrayBuilder);
     233        }
     234
     235        /**
     236         * Visit a relation that only contains points
     237         * @param r The relation to visit
     238         */
     239        private void visitMultiPoints(final Relation r) {
     240            final JsonArrayBuilder multiPoint = Json.createArrayBuilder();
     241            r.getMembers().stream().map(RelationMember::getMember).filter(Node.class::isInstance).map(Node.class::cast)
     242                    .map(Node::getCoor).map(latLon -> getCoorArray(null, latLon))
     243                    .forEach(multiPoint::add);
     244            geomObj.add("type", "MultiPoint");
     245            geomObj.add("coordinates", multiPoint);
     246        }
     247
     248        /**
     249         * Visit a relation that is a multi line string
     250         * @param r The relation to convert
     251         */
     252        private void visitMultiLineString(final Relation r) {
     253            final JsonArrayBuilder multiLine = Json.createArrayBuilder();
     254            r.getMembers().stream().map(RelationMember::getMember).filter(Way.class::isInstance).map(Way.class::cast)
     255                    .map(Way::getNodes).map(p -> {
     256                JsonArrayBuilder array = getCoorsArray(p);
     257                LatLon ll = p.get(0).getCoor();
     258                // since first node is not duplicated as last node
     259                return ll != null ? array.add(getCoorArray(null, ll)) : array;
     260            }).forEach(multiLine::add);
     261            geomObj.add("type", "MultiLineString");
     262            geomObj.add("coordinates", multiLine);
     263            processedMultipolygonWays.addAll(r.getMemberPrimitives(Way.class));
     264        }
     265
     266        /**
     267         * Convert a multipolygon to geojson
     268         * @param r The relation to convert
     269         * @throws MultipolygonBuilder.JoinedPolygonCreationException See {@link MultipolygonBuilder#joinWays(Relation)}.
     270         * Note that if the exception is thrown, {@link #geomObj} will not have been modified.
     271         */
     272        private void visitMultipolygon(final Relation r) throws MultipolygonBuilder.JoinedPolygonCreationException {
     273                final Pair<List<MultipolygonBuilder.JoinedPolygon>, List<MultipolygonBuilder.JoinedPolygon>> mp =
     274                        MultipolygonBuilder.joinWays(r);
    164275                final JsonArrayBuilder polygon = Json.createArrayBuilder();
    165                 Stream.concat(mp.a.stream(), mp.b.stream())
     276                // Peek would theoretically be better for these two streams, but SonarLint doesn't like it.
     277                // java:S3864: "Stream.peek" should be used with caution
     278                final Stream<List<Node>> outer = mp.a.stream().map(MultipolygonBuilder.JoinedPolygon::getNodes).map(nodes -> {
     279                    final ArrayList<Node> tempNodes = new ArrayList<>(nodes);
     280                    tempNodes.add(tempNodes.get(0));
     281                    if (options.contains(Options.RIGHT_HAND_RULE) && Geometry.isClockwise(tempNodes)) {
     282                        Collections.reverse(nodes);
     283                    }
     284                    return nodes;
     285                });
     286                final Stream<List<Node>> inner = mp.b.stream().map(MultipolygonBuilder.JoinedPolygon::getNodes).map(nodes -> {
     287                    final ArrayList<Node> tempNodes = new ArrayList<>(nodes);
     288                    tempNodes.add(tempNodes.get(0));
     289                    // Note that we are checking !Geometry.isClockwise, which is different from the outer
     290                    // ring check.
     291                    if (options.contains(Options.RIGHT_HAND_RULE) && !Geometry.isClockwise(tempNodes)) {
     292                        Collections.reverse(nodes);
     293                    }
     294                    return nodes;
     295                });
     296                Stream.concat(outer, inner)
    166297                        .map(p -> {
    167                             JsonArrayBuilder array = getCoorsArray(p.getNodes());
    168                             LatLon ll = p.getNodes().get(0).getCoor();
     298                            JsonArrayBuilder array = getCoorsArray(p);
     299                            LatLon ll = p.get(0).getCoor();
    169300                            // since first node is not duplicated as last node
    170301                            return ll != null ? array.add(getCoorArray(null, ll)) : array;
    171                             })
     302                        })
    172303                        .forEach(polygon::add);
     304                final JsonArrayBuilder multiPolygon = Json.createArrayBuilder().add(polygon);
    173305                geomObj.add("type", "MultiPolygon");
    174                 final JsonArrayBuilder multiPolygon = Json.createArrayBuilder().add(polygon);
    175306                geomObj.add("coordinates", multiPolygon);
    176307                processedMultipolygonWays.addAll(r.getMemberPrimitives(Way.class));
    177             } catch (MultipolygonBuilder.JoinedPolygonCreationException ex) {
    178                 Logging.warn("GeoJSON: Failed to export multipolygon {0}", r.getUniqueId());
    179                 Logging.warn(ex);
    180             }
    181308        }
    182309
     
    205332    protected void appendPrimitive(OsmPrimitive p, JsonArrayBuilder array) {
    206333        if (p.isIncomplete() ||
    207             (SKIP_EMPTY_NODES.get() && p instanceof Node && p.getKeys().isEmpty())) {
     334            (this.options.contains(Options.SKIP_EMPTY_NODES) && p instanceof Node && p.getKeys().isEmpty())) {
    208335            return;
    209336        }
     
    211338        // Properties
    212339        final JsonObjectBuilder propObj = Json.createObjectBuilder();
    213         for (Entry<String, String> t : p.getKeys().entrySet()) {
    214             propObj.add(t.getKey(), convertValueToJson(t.getValue()));
     340        for (Map.Entry<String, String> t : p.getKeys().entrySet()) {
     341            // If writing OSM information, follow Overpass syntax (escape `@` with another `@`)
     342            final String key = options.contains(Options.WRITE_OSM_INFORMATION) && t.getKey().startsWith("@")
     343                    ? '@' + t.getKey() : t.getKey();
     344            propObj.add(key, convertValueToJson(t.getValue()));
     345        }
     346        if (options.contains(Options.WRITE_OSM_INFORMATION)) {
     347            // Use the same format as Overpass
     348            propObj.add("@id", p.getPrimitiveId().getType().getAPIName() + '/' + p.getUniqueId()); // type/id
     349            if (!p.isNew()) {
     350                propObj.add("@timestamp", Instant.ofEpochSecond(p.getRawTimestamp()).toString());
     351                propObj.add("@version", Integer.toString(p.getVersion()));
     352                propObj.add("@changeset", Long.toString(p.getChangesetId()));
     353            }
     354            if (p.getUser() != null) {
     355                propObj.add("@user", p.getUser().getName());
     356                propObj.add("@uid", p.getUser().getId());
     357            }
     358            if (options.contains(Options.WRITE_OSM_INFORMATION) && p.getReferrers(true).stream().anyMatch(Relation.class::isInstance)) {
     359                final JsonArrayBuilder jsonArrayBuilder = Json.createArrayBuilder();
     360                for (Relation relation : Utils.filteredCollection(p.getReferrers(), Relation.class)) {
     361                    final JsonObjectBuilder relationObject = Json.createObjectBuilder();
     362                    relationObject.add("rel", relation.getId());
     363                    Collection<RelationMember> members = relation.getMembersFor(Collections.singleton(p));
     364                    // Each role is a separate object in overpass-turbo geojson export. For now, just concat them.
     365                    relationObject.add("role",
     366                            members.stream().map(RelationMember::getRole).collect(Collectors.joining(";")));
     367                    final JsonObjectBuilder relationKeys = Json.createObjectBuilder();
     368                    // Uncertain if the @relation reltags need to be @ escaped. I don't think so, as example output
     369                    // didn't have any metadata in it.
     370                    for (Map.Entry<String, String> tag : relation.getKeys().entrySet()) {
     371                        relationKeys.add(tag.getKey(), convertValueToJson(tag.getValue()));
     372                    }
     373                    relationObject.add("reltags", relationKeys);
     374                }
     375                propObj.add("@relations", jsonArrayBuilder);
     376            }
    215377        }
    216378        final JsonObject prop = propObj.build();
Note: See TracChangeset for help on using the changeset viewer.