Ticket #15182: 15182.patch
File 15182.patch, 26.3 KB (added by , 2 years ago) |
---|
-
new file src/org/openstreetmap/josm/data/validation/ValidatorCLI.java
diff --git a/src/org/openstreetmap/josm/data/validation/ValidatorCLI.java b/src/org/openstreetmap/josm/data/validation/ValidatorCLI.java new file mode 100644 index 0000000000..6428bb1ba7
- + 1 // License: GPL. For details, see LICENSE file. 2 package org.openstreetmap.josm.data.validation; 3 4 import static org.openstreetmap.josm.tools.I18n.tr; 5 6 import java.io.File; 7 import java.io.FileOutputStream; 8 import java.io.IOException; 9 import java.io.InputStream; 10 import java.nio.charset.StandardCharsets; 11 import java.nio.file.Files; 12 import java.nio.file.Paths; 13 import java.util.Arrays; 14 import java.util.Collection; 15 import java.util.Locale; 16 import java.util.Optional; 17 import java.util.logging.Level; 18 19 import org.openstreetmap.josm.cli.CLIModule; 20 import org.openstreetmap.josm.data.osm.DataSet; 21 import org.openstreetmap.josm.data.preferences.JosmBaseDirectories; 22 import org.openstreetmap.josm.data.preferences.JosmUrls; 23 import org.openstreetmap.josm.data.projection.ProjectionRegistry; 24 import org.openstreetmap.josm.data.projection.Projections; 25 import org.openstreetmap.josm.gui.MainApplication; 26 import org.openstreetmap.josm.gui.layer.OsmDataLayer; 27 import org.openstreetmap.josm.gui.progress.NullProgressMonitor; 28 import org.openstreetmap.josm.io.Compression; 29 import org.openstreetmap.josm.io.GeoJSONMapRouletteWriter; 30 import org.openstreetmap.josm.io.IllegalDataException; 31 import org.openstreetmap.josm.io.OsmReader; 32 import org.openstreetmap.josm.spi.preferences.Config; 33 import org.openstreetmap.josm.spi.preferences.MemoryPreferences; 34 import org.openstreetmap.josm.tools.Http1Client; 35 import org.openstreetmap.josm.tools.HttpClient; 36 import org.openstreetmap.josm.tools.JosmRuntimeException; 37 import org.openstreetmap.josm.tools.Logging; 38 import org.openstreetmap.josm.tools.OptionParser; 39 import org.openstreetmap.josm.tools.Stopwatch; 40 import org.openstreetmap.josm.tools.Territories; 41 42 /** 43 * Add a validate command to the JOSM command line interface. 44 * @author Taylor Smock 45 * @since xxx 46 */ 47 public class ValidatorCLI implements CLIModule { 48 public static ValidatorCLI INSTANCE = new ValidatorCLI(); 49 50 /** The input file */ 51 private String input; 52 /** The output file */ 53 private String output; 54 55 /** The log level */ 56 private Level logLevel; 57 58 private enum Option { 59 HELP(false, 'h'), 60 INPUT(true, 'i'), 61 OUTPUT(true, 'o'), 62 DEBUG(false, '*'), 63 TRACE(false, '*'); 64 65 private final String name; 66 private final boolean requiresArgument; 67 private final char shortOption; 68 Option(final boolean requiresArgument, final char shortOption) { 69 this.name = name().toLowerCase(Locale.ROOT).replace('_', '-'); 70 this.requiresArgument = requiresArgument; 71 this.shortOption = shortOption; 72 } 73 74 /** 75 * Replies the option name 76 * @return The option name, in lowercase 77 */ 78 public String getName() { 79 return this.name; 80 } 81 82 /** 83 * Determines if this option requires an argument. 84 * @return {@code true} if this option requires an argument, {@code false} otherwise 85 */ 86 public boolean requiresArgument() { 87 return this.requiresArgument; 88 } 89 90 /** 91 * Replies the short option (single letter) associated with this option. 92 * @return the short option or '*' if there is no short option 93 */ 94 public char getShortOption() { 95 return this.shortOption; 96 } 97 } 98 99 @Override 100 public String getActionKeyword() { 101 return "validate"; 102 } 103 104 @Override 105 public void processArguments(final String[] argArray) { 106 try { 107 Logging.setLogLevel(Level.INFO); 108 this.parseArguments(argArray); 109 this.initialize(); 110 final Stopwatch stopwatch = Stopwatch.createStarted(); 111 final String task = tr("Validating {0}, saving output to {1}", this.input, this.output); 112 Logging.info(task); 113 OsmValidator.initializeTests(); 114 final DataSet dataSet = this.loadDataSet(); 115 // Adding a layer specifically for UntaggedWay.class (it wants the active dataset, which is from the 116 // current layer). 117 MainApplication.getLayerManager().addLayer(new OsmDataLayer(dataSet, this.input, new File(this.input))); 118 Collection<Test> tests = OsmValidator.getEnabledTests(false); 119 tests.parallelStream().forEach(test -> test.startTest(NullProgressMonitor.INSTANCE)); 120 tests.parallelStream().forEach(test -> test.visit(dataSet.allPrimitives())); 121 tests.parallelStream().forEach(Test::endTest); 122 if (Files.isRegularFile(Paths.get(this.output)) && !Files.deleteIfExists(Paths.get(this.output))) { 123 Logging.error("Could not delete {0}, attempting to append", this.output); 124 } 125 GeoJSONMapRouletteWriter geoJSONMapRouletteWriter = new GeoJSONMapRouletteWriter(dataSet); 126 try (FileOutputStream fileOutputStream = new FileOutputStream(this.output)) { 127 for (Test test : tests) { 128 test.getErrors().stream().map(geoJSONMapRouletteWriter::write) 129 .filter(Optional::isPresent).map(Optional::get) 130 .map(jsonObject -> jsonObject.toString().getBytes(StandardCharsets.UTF_8)).forEach(bytes -> { 131 try { 132 // Write the ASCII Record Separator character 133 fileOutputStream.write(0x1e); 134 fileOutputStream.write(bytes); 135 // Write the ASCII Line Feed character 136 fileOutputStream.write(0x0a); 137 } catch (IOException e) { 138 throw new JosmRuntimeException(e); 139 } 140 }); 141 } 142 } 143 Logging.info(stopwatch.toString(task)); 144 } catch (JosmRuntimeException | IllegalArgumentException | IllegalDataException | IOException e) { 145 Logging.info(e); 146 System.exit(1); 147 } 148 System.exit(0); 149 } 150 151 /** 152 * Initialize everything that might be needed 153 * 154 * Arguments may need to be parsed first. 155 */ 156 void initialize() { 157 Logging.setLogLevel(this.logLevel); 158 HttpClient.setFactory(Http1Client::new); 159 Config.setBaseDirectoriesProvider(JosmBaseDirectories.getInstance()); // for right-left-hand traffic cache file 160 Config.setPreferencesInstance(new MemoryPreferences()); 161 Config.setUrlsProvider(JosmUrls.getInstance()); 162 ProjectionRegistry.setProjection(Projections.getProjectionByCode("epsg:3857".toUpperCase(Locale.ROOT))); 163 164 Territories.initializeInternalData(); 165 OsmValidator.initialize(); 166 } 167 168 /** 169 * Parse command line arguments and do some low-level error checking. 170 * @param argArray the arguments array 171 */ 172 void parseArguments(String[] argArray) { 173 Logging.setLogLevel(Level.INFO); 174 175 OptionParser parser = new OptionParser("JOSM rendering"); 176 for (Option o : Option.values()) { 177 if (o.requiresArgument()) { 178 parser.addArgumentParameter(o.getName(), 179 OptionParser.OptionCount.REQUIRED, 180 arg -> handleOption(o, arg)); 181 } else { 182 parser.addFlagParameter(o.getName(), () -> handleOption(o)); 183 } 184 if (o.getShortOption() != '*') { 185 parser.addShortAlias(o.getName(), Character.toString(o.getShortOption())); 186 } 187 } 188 parser.parseOptionsOrExit(Arrays.asList(argArray)); 189 } 190 191 private void handleOption(final Option option) { 192 switch (option) { 193 case HELP: 194 showHelp(); 195 System.exit(0); 196 break; 197 case DEBUG: 198 this.logLevel = Logging.LEVEL_DEBUG; 199 break; 200 case TRACE: 201 this.logLevel = Logging.LEVEL_TRACE; 202 break; 203 default: 204 throw new AssertionError("Unexpected option: " + option); 205 } 206 } 207 208 private void handleOption(final Option option, final String argument) { 209 switch (option) { 210 case INPUT: 211 this.input = argument; 212 break; 213 case OUTPUT: 214 this.output = argument; 215 break; 216 default: 217 throw new AssertionError("Unexpected option: " + option); 218 } 219 } 220 221 private DataSet loadDataSet() throws IOException, IllegalDataException { 222 if (this.input == null) { 223 throw new IllegalArgumentException(tr("Missing argument - input data file ({0})", "--input|-i")); 224 } 225 try (InputStream inputStream = Compression.getUncompressedFileInputStream(Paths.get(this.input))) { 226 return OsmReader.parseDataSet(inputStream, null); 227 } catch (IllegalDataException e) { 228 throw new IllegalDataException(tr("In .osm data file ''{0}'' - ", this.input) + e.getMessage(), e); 229 } 230 } 231 232 private static void showHelp() { 233 System.out.println(getHelp()); 234 } 235 236 private static String getHelp() { 237 return tr("JOSM Validation command line interface") + "\n\n" + 238 tr("Usage") + ":\n" + 239 "\tjava -jar josm.jar validate <options>\n\n" + 240 tr("Description") + ":\n" + 241 tr("Validates data and saves the result to a file.") + "\n\n"+ 242 tr("Options") + ":\n" + 243 "\t--help|-h " + tr("Show this help") + "\n" + 244 "\t--input|-i <file> " + tr("Input data file name (.osm)") + "\n" + 245 "\t--output|-o <file> " + tr("Output data file name (.geojson, line-by-line delimited for MapRoulette)"); 246 } 247 } -
src/org/openstreetmap/josm/gui/MainApplication.java
diff --git a/src/org/openstreetmap/josm/gui/MainApplication.java b/src/org/openstreetmap/josm/gui/MainApplication.java index a4572b8b9b..438efd8a07 100644
a b import org.openstreetmap.josm.data.projection.ProjectionRegistry; 98 98 import org.openstreetmap.josm.data.projection.datum.NTV2GridShiftFileSource; 99 99 import org.openstreetmap.josm.data.projection.datum.NTV2GridShiftFileWrapper; 100 100 import org.openstreetmap.josm.data.projection.datum.NTV2Proj4DirGridShiftFileSource; 101 import org.openstreetmap.josm.data.validation.ValidatorCLI; 101 102 import org.openstreetmap.josm.data.validation.tests.MapCSSTagChecker; 102 103 import org.openstreetmap.josm.gui.ProgramArguments.Option; 103 104 import org.openstreetmap.josm.gui.SplashScreen.SplashProgressMonitor; … … public class MainApplication { 311 312 registerCLIModule(JOSM_CLI_MODULE); 312 313 registerCLIModule(ProjectionCLI.INSTANCE); 313 314 registerCLIModule(RenderingCLI.INSTANCE); 315 registerCLIModule(ValidatorCLI.INSTANCE); 314 316 } 315 317 316 318 /** … … public class MainApplication { 660 662 tr("commands")+":\n"+ 661 663 "\trunjosm "+tr("launch JOSM (default, performed when no command is specified)")+'\n'+ 662 664 "\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" + 664 667 tr("For details on the {0} and {1} commands, run them with the {2} option.", "render", "project", "--help")+'\n'+ 665 668 tr("The remainder of this help page documents the {0} command.", "runjosm")+"\n\n"+ 666 669 tr("options")+":\n"+ -
src/org/openstreetmap/josm/gui/util/GuiHelper.java
diff --git a/src/org/openstreetmap/josm/gui/util/GuiHelper.java b/src/org/openstreetmap/josm/gui/util/GuiHelper.java index 89ab9f84b0..4c17334af2 100644
a b public final class GuiHelper { 288 288 * @since 10271 289 289 */ 290 290 public static void assertCallFromEdt() { 291 if (!SwingUtilities.isEventDispatchThread() ) {291 if (!SwingUtilities.isEventDispatchThread() && !GraphicsEnvironment.isHeadless()) { 292 292 throw new IllegalStateException( 293 293 "Needs to be called from the EDT thread, not from " + Thread.currentThread().getName()); 294 294 } -
new file src/org/openstreetmap/josm/io/GeoJSONMapRouletteWriter.java
diff --git a/src/org/openstreetmap/josm/io/GeoJSONMapRouletteWriter.java b/src/org/openstreetmap/josm/io/GeoJSONMapRouletteWriter.java new file mode 100644 index 0000000000..d61be93cd8
- + 1 // License: GPL. For details, see LICENSE file. 2 package org.openstreetmap.josm.io; 3 4 import java.util.Objects; 5 import java.util.Optional; 6 7 import javax.json.Json; 8 import javax.json.JsonArray; 9 import javax.json.JsonArrayBuilder; 10 import javax.json.JsonObject; 11 import javax.json.JsonObjectBuilder; 12 import javax.json.JsonValue; 13 14 import org.openstreetmap.josm.data.osm.DataSet; 15 import org.openstreetmap.josm.data.osm.OsmPrimitive; 16 import org.openstreetmap.josm.data.osm.WaySegment; 17 import org.openstreetmap.josm.data.validation.TestError; 18 import org.openstreetmap.josm.tools.Logging; 19 20 /** 21 * Convert {@link TestError} to MapRoulette Tasks 22 * @author Taylor Smock 23 * @since xxx 24 */ 25 public class GeoJSONMapRouletteWriter extends GeoJSONWriter { 26 27 /** 28 * Constructs a new {@code GeoJSONWriter}. 29 * @param ds The originating OSM dataset 30 */ 31 public GeoJSONMapRouletteWriter(DataSet ds) { 32 super(ds); 33 super.setOptions(Options.RIGHT_HAND_RULE, Options.WRITE_OSM_INFORMATION); 34 } 35 36 /** 37 * Convert a test error to a string 38 * @param testError The test error to convert 39 * @return The MapRoulette challenge object 40 */ 41 public Optional<JsonObject> write(final TestError testError) { 42 final JsonObjectBuilder jsonObjectBuilder = Json.createObjectBuilder(); 43 final JsonArrayBuilder featuresBuilder = Json.createArrayBuilder(); 44 final JsonObjectBuilder propertiesBuilder = Json.createObjectBuilder(); 45 propertiesBuilder.add("message", testError.getMessage()); 46 Optional.ofNullable(testError.getDescription()).ifPresent(description -> propertiesBuilder.add("description", description)); 47 propertiesBuilder.add("code", testError.getCode()); 48 propertiesBuilder.add("fixable", testError.isFixable()); 49 propertiesBuilder.add("severity", testError.getSeverity().toString()); 50 propertiesBuilder.add("severityInteger", testError.getSeverity().getLevel()); 51 propertiesBuilder.add("test", testError.getTester().getName()); 52 testError.getPrimitives().forEach(primitive -> super.appendPrimitive(primitive, featuresBuilder)); 53 testError.getHighlighted().stream().map(p -> { 54 if (p instanceof OsmPrimitive) { 55 return p; 56 } else if (p instanceof WaySegment) { 57 return ((WaySegment) p).toWay(); 58 } 59 Logging.error("Could not convert {0} to an OsmPrimitive", p); 60 return null; 61 }).filter(Objects::nonNull).map(OsmPrimitive.class::cast) 62 .forEach(primitive -> super.appendPrimitive(primitive, featuresBuilder)); 63 final JsonArray featureArray = featuresBuilder.build(); 64 final JsonArrayBuilder featuresMessageBuilder = Json.createArrayBuilder(); 65 if (featureArray.isEmpty()) { 66 Logging.error("Could not generate task for {0}", testError.getMessage()); 67 return Optional.empty(); 68 } 69 JsonObject primitive = featureArray.getJsonObject(0); 70 JsonObjectBuilder replacementPrimitive = Json.createObjectBuilder(primitive); 71 final JsonObjectBuilder properties; 72 if (primitive.containsKey("properties") && primitive.get("properties").getValueType() == JsonValue.ValueType.OBJECT) { 73 properties = Json.createObjectBuilder(primitive.getJsonObject("properties")); 74 } else { 75 properties = Json.createObjectBuilder(); 76 } 77 properties.addAll(propertiesBuilder); 78 replacementPrimitive.add("properties", properties); 79 featuresMessageBuilder.add(replacementPrimitive); 80 for (int i = 1; i < featureArray.size(); i++) { 81 featuresMessageBuilder.add(featureArray.get(i)); 82 } 83 // For now, don't add any cooperativeWork objects, as JOSM should be able to find the fixes. 84 // This should change if the ValidatorCLI can use plugins (especially those introducing external data, like 85 // the ElevationProfile plugin (which provides elevation data)). 86 jsonObjectBuilder.add("type", "FeatureCollection"); 87 jsonObjectBuilder.add("features", featuresMessageBuilder); 88 return Optional.of(jsonObjectBuilder.build()); 89 } 90 } -
src/org/openstreetmap/josm/io/GeoJSONWriter.java
diff --git a/src/org/openstreetmap/josm/io/GeoJSONWriter.java b/src/org/openstreetmap/josm/io/GeoJSONWriter.java index d2eb644846..cb87f7b767 100644
a b import java.io.StringWriter; 6 6 import java.io.Writer; 7 7 import java.math.BigDecimal; 8 8 import java.math.RoundingMode; 9 import java.time.Instant; 10 import java.util.ArrayList; 11 import java.util.Arrays; 9 12 import java.util.Collection; 10 13 import java.util.Collections; 14 import java.util.EnumSet; 11 15 import java.util.HashSet; 12 16 import java.util.Iterator; 13 17 import java.util.List; 14 18 import java.util.Map; 15 import java.util.Map.Entry;16 19 import java.util.Set; 20 import java.util.stream.Collectors; 17 21 import java.util.stream.Stream; 18 22 19 23 import javax.json.Json; … … import org.openstreetmap.josm.data.osm.MultipolygonBuilder.JoinedPolygon; 35 39 import org.openstreetmap.josm.data.osm.Node; 36 40 import org.openstreetmap.josm.data.osm.OsmPrimitive; 37 41 import org.openstreetmap.josm.data.osm.Relation; 42 import org.openstreetmap.josm.data.osm.RelationMember; 38 43 import org.openstreetmap.josm.data.osm.Way; 39 44 import org.openstreetmap.josm.data.osm.visitor.OsmPrimitiveVisitor; 40 45 import org.openstreetmap.josm.data.preferences.BooleanProperty; 41 46 import org.openstreetmap.josm.data.projection.Projection; 42 47 import org.openstreetmap.josm.data.projection.Projections; 43 48 import org.openstreetmap.josm.gui.mappaint.ElemStyles; 49 import org.openstreetmap.josm.tools.Geometry; 44 50 import org.openstreetmap.josm.tools.Logging; 45 51 import org.openstreetmap.josm.tools.Pair; 52 import org.openstreetmap.josm.tools.Utils; 46 53 47 54 /** 48 55 * Writes OSM data as a GeoJSON string, using JSR 353: Java API for JSON Processing (JSON-P). … … import org.openstreetmap.josm.tools.Pair; 51 58 */ 52 59 public class GeoJSONWriter { 53 60 61 enum Options { 62 /** If using the right hand rule, we have to ensure that the "right" side is the interior of the object. */ 63 RIGHT_HAND_RULE, 64 /** Write OSM information to the feature properties field. This tries to follow the Overpass turbo format. */ 65 WRITE_OSM_INFORMATION; 66 } 67 54 68 private final DataSet data; 55 private final Projection projection;69 private static final Projection projection = Projections.getProjectionByCode("EPSG:4326"); // WGS 84 56 70 private static final BooleanProperty SKIP_EMPTY_NODES = new BooleanProperty("geojson.export.skip-empty-nodes", true); 57 71 private static final BooleanProperty UNTAGGED_CLOSED_IS_POLYGON = new BooleanProperty("geojson.export.untagged-closed-is-polygon", false); 58 72 private static final Set<Way> processedMultipolygonWays = new HashSet<>(); 73 private EnumSet<Options> options = EnumSet.noneOf(Options.class); 59 74 60 75 /** 61 76 * This is used to determine that a tag should be interpreted as a json … … public class GeoJSONWriter { 77 92 */ 78 93 public GeoJSONWriter(DataSet ds) { 79 94 this.data = ds; 80 this.projection = Projections.getProjectionByCode("EPSG:4326"); // WGS 84 95 } 96 97 /** 98 * Set the options for this writer. See {@link Options}. 99 * @param options The options to set. 100 */ 101 void setOptions(final Options... options) { 102 this.options.clear(); 103 this.options.addAll(Arrays.asList(options)); 81 104 } 82 105 83 106 /** … … public class GeoJSONWriter { 117 140 } 118 141 } 119 142 143 /** 144 * Convert a primitive to a json object 145 */ 120 146 private class GeometryPrimitiveVisitor implements OsmPrimitiveVisitor { 121 147 122 148 private final JsonObjectBuilder geomObj; … … public class GeoJSONWriter { 141 167 // no need to write this object again 142 168 return; 143 169 } 144 final JsonArrayBuilder array = getCoorsArray(w.getNodes());145 170 boolean writeAsPolygon = w.isClosed() && ((!w.isTagged() && UNTAGGED_CLOSED_IS_POLYGON.get()) 146 171 || ElemStyles.hasAreaElemStyle(w, false)); 172 final List<Node> nodes = w.getNodes(); 173 if (writeAsPolygon && options.contains(Options.RIGHT_HAND_RULE) && Geometry.isClockwise(nodes)) { 174 Collections.reverse(nodes); 175 } 176 final JsonArrayBuilder array = getCoorsArray(nodes); 147 177 if (writeAsPolygon) { 148 178 geomObj.add("type", "Polygon"); 149 179 geomObj.add("coordinates", Json.createArrayBuilder().add(array)); … … public class GeoJSONWriter { 162 192 try { 163 193 final Pair<List<JoinedPolygon>, List<JoinedPolygon>> mp = MultipolygonBuilder.joinWays(r); 164 194 final JsonArrayBuilder polygon = Json.createArrayBuilder(); 165 Stream.concat(mp.a.stream(), mp.b.stream()) 195 final Stream<List<Node>> outer = mp.a.stream().map(JoinedPolygon::getNodes).map(nodes -> { 196 final ArrayList<Node> tempNodes = new ArrayList<>(nodes); 197 tempNodes.add(tempNodes.get(0)); 198 if (options.contains(Options.RIGHT_HAND_RULE) && Geometry.isClockwise(tempNodes)) { 199 Collections.reverse(nodes); 200 } 201 return nodes; 202 }); 203 final Stream<List<Node>> inner = mp.b.stream().map(JoinedPolygon::getNodes).map(nodes -> { 204 final ArrayList<Node> tempNodes = new ArrayList<>(nodes); 205 tempNodes.add(tempNodes.get(0)); 206 if (options.contains(Options.RIGHT_HAND_RULE) && !Geometry.isClockwise(tempNodes)) { 207 Collections.reverse(nodes); 208 } 209 return nodes; 210 }); 211 Stream.concat(outer, inner) 166 212 .map(p -> { 167 JsonArrayBuilder array = getCoorsArray(p .getNodes());168 LatLon ll = p.get Nodes().get(0).getCoor();213 JsonArrayBuilder array = getCoorsArray(p); 214 LatLon ll = p.get(0).getCoor(); 169 215 // since first node is not duplicated as last node 170 216 return ll != null ? array.add(getCoorArray(null, ll)) : array; 171 217 }) … … public class GeoJSONWriter { 210 256 211 257 // Properties 212 258 final JsonObjectBuilder propObj = Json.createObjectBuilder(); 213 for (Entry<String, String> t : p.getKeys().entrySet()) { 214 propObj.add(t.getKey(), convertValueToJson(t.getValue())); 259 for (Map.Entry<String, String> t : p.getKeys().entrySet()) { 260 // If writing OSM information, follow Overpass syntax (escape `@` with another `@`) 261 final String key = options.contains(Options.WRITE_OSM_INFORMATION) && t.getKey().startsWith("@") 262 ? '@' + t.getKey() : t.getKey(); 263 propObj.add(key, convertValueToJson(t.getValue())); 264 } 265 if (options.contains(Options.WRITE_OSM_INFORMATION)) { 266 // Use the same format as Overpass 267 propObj.add("@id", p.getPrimitiveId().getType().getAPIName() + '/' + p.getId()); // type/id 268 if (!p.isNew()) { 269 propObj.add("@timestamp", Instant.ofEpochSecond(p.getRawTimestamp()).toString()); 270 propObj.add("@version", Integer.toString(p.getVersion())); 271 propObj.add("@changeset", Long.toString(p.getChangesetId())); 272 } 273 if (p.getUser() != null) { 274 propObj.add("@user", p.getUser().getName()); 275 propObj.add("@uid", p.getUser().getId()); 276 } 277 if (options.contains(Options.WRITE_OSM_INFORMATION) && p.getReferrers(true).stream().anyMatch(Relation.class::isInstance)) { 278 final JsonArrayBuilder jsonArrayBuilder = Json.createArrayBuilder(); 279 for (Relation relation : Utils.filteredCollection(p.getReferrers(), Relation.class)) { 280 final JsonObjectBuilder relationObject = Json.createObjectBuilder(); 281 relationObject.add("rel", relation.getId()); 282 Collection<RelationMember> members = relation.getMembersFor(Collections.singleton(p)); 283 // Each role is a separate object in overpass-turbo geojson export. For now, just concat them. 284 relationObject.add("role", 285 members.stream().map(RelationMember::getRole).collect(Collectors.joining(";"))); 286 final JsonObjectBuilder relationKeys = Json.createObjectBuilder(); 287 // Uncertain if the @relation reltags need to be @ escaped. I don't think so, as example output 288 // didn't have any metadata in it. 289 for (Map.Entry<String, String> tag : relation.getKeys().entrySet()) { 290 relationKeys.add(tag.getKey(), convertValueToJson(tag.getValue())); 291 } 292 relationObject.add("reltags", relationKeys); 293 } 294 propObj.add("@relations", jsonArrayBuilder); 295 } 215 296 } 216 297 final JsonObject prop = propObj.build(); 217 298