Ticket #15182: 15182.3.patch
File 15182.3.patch, 65.9 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..2403bb38c4
- + 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 import static org.openstreetmap.josm.tools.I18n.trn; 6 7 import java.io.File; 8 import java.io.IOException; 9 import java.io.InputStream; 10 import java.io.OutputStream; 11 import java.nio.charset.StandardCharsets; 12 import java.nio.file.Files; 13 import java.nio.file.Paths; 14 import java.util.ArrayList; 15 import java.util.Arrays; 16 import java.util.Collection; 17 import java.util.Collections; 18 import java.util.HashMap; 19 import java.util.List; 20 import java.util.Locale; 21 import java.util.Map; 22 import java.util.Optional; 23 import java.util.concurrent.atomic.AtomicReference; 24 import java.util.function.Supplier; 25 import java.util.logging.Level; 26 import java.util.stream.Collectors; 27 28 import org.apache.commons.compress.utils.FileNameUtils; 29 import org.openstreetmap.josm.actions.ExtensionFileFilter; 30 import org.openstreetmap.josm.cli.CLIModule; 31 import org.openstreetmap.josm.data.Preferences; 32 import org.openstreetmap.josm.data.osm.DataSet; 33 import org.openstreetmap.josm.data.preferences.JosmBaseDirectories; 34 import org.openstreetmap.josm.data.preferences.JosmUrls; 35 import org.openstreetmap.josm.data.projection.ProjectionRegistry; 36 import org.openstreetmap.josm.data.projection.Projections; 37 import org.openstreetmap.josm.data.validation.tests.MapCSSTagChecker; 38 import org.openstreetmap.josm.gui.MainApplication; 39 import org.openstreetmap.josm.gui.io.CustomConfigurator; 40 import org.openstreetmap.josm.gui.io.importexport.FileImporter; 41 import org.openstreetmap.josm.gui.layer.OsmDataLayer; 42 import org.openstreetmap.josm.gui.mappaint.MapPaintStyles; 43 import org.openstreetmap.josm.gui.mappaint.mapcss.MapCSSStyleSource; 44 import org.openstreetmap.josm.gui.mappaint.mapcss.parsergen.ParseException; 45 import org.openstreetmap.josm.gui.progress.CLIProgressMonitor; 46 import org.openstreetmap.josm.gui.progress.ProgressMonitor; 47 import org.openstreetmap.josm.io.Compression; 48 import org.openstreetmap.josm.io.GeoJSONMapRouletteWriter; 49 import org.openstreetmap.josm.io.IllegalDataException; 50 import org.openstreetmap.josm.io.OsmChangeReader; 51 import org.openstreetmap.josm.spi.lifecycle.Lifecycle; 52 import org.openstreetmap.josm.spi.preferences.Config; 53 import org.openstreetmap.josm.spi.preferences.IPreferences; 54 import org.openstreetmap.josm.spi.preferences.MemoryPreferences; 55 import org.openstreetmap.josm.tools.Http1Client; 56 import org.openstreetmap.josm.tools.HttpClient; 57 import org.openstreetmap.josm.tools.I18n; 58 import org.openstreetmap.josm.tools.JosmRuntimeException; 59 import org.openstreetmap.josm.tools.Logging; 60 import org.openstreetmap.josm.tools.OptionParser; 61 import org.openstreetmap.josm.tools.Stopwatch; 62 import org.openstreetmap.josm.tools.Territories; 63 import org.openstreetmap.josm.tools.Utils; 64 65 /** 66 * Add a validate command to the JOSM command line interface. 67 * @author Taylor Smock 68 * @since xxx 69 */ 70 public class ValidatorCLI implements CLIModule { 71 public static final ValidatorCLI INSTANCE = new ValidatorCLI(); 72 73 /** The input file(s) */ 74 private final List<String> input = new ArrayList<>(); 75 /** The change files. input file -> list of change files */ 76 private final Map<String, List<String>> changeFiles = new HashMap<>(); 77 /** The output file(s). If {@code null}, use input filename as base (replace extension with geojson). input -> output */ 78 private final Map<String, String> output = new HashMap<>(); 79 80 private static final Supplier<ProgressMonitor> progressMonitorFactory = CLIProgressMonitor::new; 81 82 /** The log level */ 83 private Level logLevel; 84 85 private enum Option { 86 /** --help Show the help for validate */ 87 HELP(false, 'h'), 88 /** --input=<input-file> Set the current input file */ 89 INPUT(true, 'i', OptionParser.OptionCount.MULTIPLE), 90 /** --output=<output-file> Set the output file for the current input file */ 91 OUTPUT(true, 'o', OptionParser.OptionCount.MULTIPLE), 92 /** --change-file=<change-file> Add a change file */ 93 CHANGE_FILE(true, 'c', OptionParser.OptionCount.MULTIPLE), 94 /** --debug Set logging level to debug */ 95 DEBUG(false, '*'), 96 /** --trace Set logging level to trace */ 97 TRACE(false, '*'), 98 /** --language=<language> Set the language */ 99 LANGUAGE(true, 'l'), 100 /** --load-preferences=<url-to-xml> Changes preferences according to the XML file */ 101 LOAD_PREFERENCES(true, 'p'), 102 /** --set=<key>=<value> Set preference key to value */ 103 SET(true, 's'); 104 105 private final String name; 106 private final boolean requiresArgument; 107 private final char shortOption; 108 private final OptionParser.OptionCount optionCount; 109 Option(final boolean requiresArgument, final char shortOption) { 110 this(requiresArgument, shortOption, OptionParser.OptionCount.OPTIONAL); 111 } 112 113 Option(final boolean requiresArgument, final char shortOption, final OptionParser.OptionCount optionCount) { 114 this.name = name().toLowerCase(Locale.ROOT).replace('_', '-'); 115 this.requiresArgument = requiresArgument; 116 this.shortOption = shortOption; 117 this.optionCount = optionCount; 118 } 119 120 /** 121 * Replies the option name 122 * @return The option name, in lowercase 123 */ 124 public String getName() { 125 return this.name; 126 } 127 128 /** 129 * Get the number of times this option should be seen 130 * @return The option count 131 */ 132 public OptionParser.OptionCount getOptionCount() { 133 return this.optionCount; 134 } 135 136 /** 137 * Replies the short option (single letter) associated with this option. 138 * @return the short option or '*' if there is no short option 139 */ 140 public char getShortOption() { 141 return this.shortOption; 142 } 143 144 /** 145 * Determines if this option requires an argument. 146 * @return {@code true} if this option requires an argument, {@code false} otherwise 147 */ 148 public boolean requiresArgument() { 149 return this.requiresArgument; 150 } 151 152 } 153 154 @Override 155 public String getActionKeyword() { 156 return "validate"; 157 } 158 159 @Override 160 public void processArguments(final String[] argArray) { 161 try { 162 // Ensure that preferences are only in memory 163 Config.setPreferencesInstance(new MemoryPreferences()); 164 Logging.setLogLevel(Level.INFO); 165 this.parseArguments(argArray); 166 if (this.input.isEmpty()) { 167 throw new IllegalArgumentException(tr("Missing argument - input data file ({0})", "--input|-i")); 168 } 169 this.initialize(); 170 final ProgressMonitor fileMonitor = progressMonitorFactory.get(); 171 fileMonitor.beginTask(tr("Processing files..."), this.input.size()); 172 for (String inputFile : this.input) { 173 if (inputFile.endsWith(".validator.mapcss")) { 174 this.processValidatorFile(inputFile); 175 } else if (inputFile.endsWith(".mapcss")) { 176 this.processMapcssFile(inputFile); 177 } else { 178 this.processFile(inputFile); 179 } 180 fileMonitor.worked(1); 181 } 182 fileMonitor.finishTask(); 183 } catch (Exception e) { 184 Logging.info(e); 185 Lifecycle.exitJosm(true, 1); 186 } 187 Lifecycle.exitJosm(true, 0); 188 } 189 190 /** 191 * Process a standard mapcss file 192 * @param inputFile The mapcss file to validate 193 * @throws ParseException if the file does not match the mapcss syntax 194 */ 195 private void processMapcssFile(final String inputFile) throws ParseException { 196 final MapCSSStyleSource styleSource = new MapCSSStyleSource(new File(inputFile).toURI().getPath(), inputFile, inputFile); 197 styleSource.loadStyleSource(); 198 if (!styleSource.getErrors().isEmpty()) { 199 throw new ParseException(trn("{0} had {1} error", "{0} had {1} errors", styleSource.getErrors().size(), 200 inputFile, styleSource.getErrors().size())); 201 } else { 202 Logging.info(tr("{0} had no errors", inputFile)); 203 } 204 } 205 206 /** 207 * Process a validator file 208 * @param inputFile The file to check 209 * @throws IOException if there is a problem reading the file 210 * @throws ParseException if the file does not match the validator mapcss syntax 211 */ 212 private void processValidatorFile(final String inputFile) throws ParseException, IOException { 213 // Check asserts 214 Config.getPref().putBoolean("validator.check_assert_local_rules", true); 215 final MapCSSTagChecker mapCSSTagChecker = new MapCSSTagChecker(); 216 final Collection<String> assertionErrors = new ArrayList<>(); 217 final MapCSSTagChecker.ParseResult result = mapCSSTagChecker.addMapCSS(new File(inputFile).toURI().getPath(), 218 assertionErrors::add); 219 if (!result.parseErrors.isEmpty() || !assertionErrors.isEmpty()) { 220 for (Throwable throwable : result.parseErrors) { 221 Logging.error(throwable); 222 } 223 for (String error : assertionErrors) { 224 Logging.error(error); 225 } 226 throw new ParseException(trn("{0} had {1} error", "{0} had {1} errors", result.parseErrors.size() + assertionErrors.size(), 227 inputFile, result.parseErrors.size() + assertionErrors.size())); 228 } else { 229 Logging.info(tr("{0} had no errors"), inputFile); 230 } 231 } 232 233 /** 234 * Process an OSM file 235 * @param inputFile The input filename 236 * @throws IllegalArgumentException If an argument is not valid 237 * @throws IllegalDataException If there is bad data 238 * @throws IOException If a file could not be read or written 239 */ 240 private void processFile(final String inputFile) throws IllegalDataException, IOException { 241 final File inputFileFile = new File(inputFile); 242 final List<FileImporter> inputFileImporters = ExtensionFileFilter.getImporters().stream() 243 .filter(importer -> importer.acceptFile(inputFileFile)).collect(Collectors.toList()); 244 final Stopwatch stopwatch = Stopwatch.createStarted(); 245 if (inputFileImporters.stream().noneMatch(fileImporter -> 246 fileImporter.importDataHandleExceptions(inputFileFile, progressMonitorFactory.get()))) { 247 throw new IOException(tr("Could not load input file: {0}", inputFile)); 248 } 249 final String outputFile = Optional.ofNullable(this.output.get(inputFile)).orElseGet(() -> getDefaultOutputName(inputFile)); 250 final String task = tr("Validating {0}, saving output to {1}", inputFile, outputFile); 251 OsmDataLayer dataLayer = null; 252 try { 253 Logging.info(task); 254 OsmValidator.initializeTests(); 255 dataLayer = MainApplication.getLayerManager().getLayersOfType(OsmDataLayer.class) 256 .stream().filter(layer -> inputFileFile.equals(layer.getAssociatedFile())) 257 .findFirst().orElseThrow(() -> new JosmRuntimeException(tr("Could not find a layer for {0}", inputFile))); 258 final DataSet dataSet = dataLayer.getDataSet(); 259 if (this.changeFiles.containsKey(inputFile)) { 260 ProgressMonitor changeFilesMonitor = progressMonitorFactory.get(); 261 for (String changeFile : this.changeFiles.getOrDefault(inputFile, Collections.emptyList())) { 262 try (InputStream changeStream = Compression.getUncompressedFileInputStream(Paths.get(changeFile))) { 263 dataSet.mergeFrom(OsmChangeReader.parseDataSet(changeStream, changeFilesMonitor)); 264 } 265 } 266 } 267 Collection<Test> tests = OsmValidator.getEnabledTests(false); 268 if (Files.isRegularFile(Paths.get(outputFile)) && !Files.deleteIfExists(Paths.get(outputFile))) { 269 Logging.error("Could not delete {0}, attempting to append", outputFile); 270 } 271 GeoJSONMapRouletteWriter geoJSONMapRouletteWriter = new GeoJSONMapRouletteWriter(dataSet); 272 try (OutputStream fileOutputStream = Files.newOutputStream(Paths.get(outputFile))) { 273 tests.parallelStream().forEach(test -> runTest(test, geoJSONMapRouletteWriter, fileOutputStream, dataSet)); 274 } 275 } finally { 276 if (dataLayer != null) { 277 MainApplication.getLayerManager().removeLayer(dataLayer); 278 } 279 Logging.info(stopwatch.toString(task)); 280 } 281 } 282 283 /** 284 * Get the default output name 285 * @param inputString The input file 286 * @return The default output name for the input file (extension stripped, ".geojson" added) 287 */ 288 private static String getDefaultOutputName(final String inputString) { 289 final String extension = FileNameUtils.getExtension(inputString); 290 if (!Arrays.asList("zip", "bz", "xz", "geojson").contains(extension)) { 291 return FileNameUtils.getBaseName(inputString) + ".geojson"; 292 } else if ("geojson".equals(extension)) { 293 // Account for geojson input files 294 return FileNameUtils.getBaseName(inputString) + ".validated.geojson"; 295 } 296 return FileNameUtils.getBaseName(FileNameUtils.getBaseName(inputString)) + ".geojson"; 297 } 298 299 /** 300 * Run a test 301 * @param test The test to run 302 * @param geoJSONMapRouletteWriter The object to use to create challenges 303 * @param fileOutputStream The location to write data to 304 * @param dataSet The dataset to check 305 */ 306 private void runTest(final Test test, final GeoJSONMapRouletteWriter geoJSONMapRouletteWriter, 307 final OutputStream fileOutputStream, DataSet dataSet) { 308 test.startTest(progressMonitorFactory.get()); 309 test.visit(dataSet.allPrimitives()); 310 test.endTest(); 311 test.getErrors().stream().map(geoJSONMapRouletteWriter::write) 312 .filter(Optional::isPresent).map(Optional::get) 313 .map(jsonObject -> jsonObject.toString().getBytes(StandardCharsets.UTF_8)).forEach(bytes -> { 314 try { 315 writeToFile(fileOutputStream, bytes); 316 } catch (IOException e) { 317 throw new JosmRuntimeException(e); 318 } 319 }); 320 test.clear(); 321 } 322 323 /** 324 * Write to a file. Synchronized to avoid writing to the same file in different threads. 325 * 326 * @param fileOutputStream The file output stream to read 327 * @param bytes The bytes to write (surrounded by RS and LF) 328 * @throws IOException If we couldn't write to file 329 */ 330 private synchronized void writeToFile(final OutputStream fileOutputStream, final byte[] bytes) 331 throws IOException { 332 // Write the ASCII Record Separator character 333 fileOutputStream.write(0x1e); 334 fileOutputStream.write(bytes); 335 // Write the ASCII Line Feed character 336 fileOutputStream.write(0x0a); 337 } 338 339 /** 340 * Initialize everything that might be needed 341 * 342 * Arguments may need to be parsed first. 343 */ 344 void initialize() { 345 Logging.setLogLevel(this.logLevel); 346 HttpClient.setFactory(Http1Client::new); 347 Config.setBaseDirectoriesProvider(JosmBaseDirectories.getInstance()); // for right-left-hand traffic cache file 348 Config.setUrlsProvider(JosmUrls.getInstance()); 349 ProjectionRegistry.setProjection(Projections.getProjectionByCode("epsg:3857".toUpperCase(Locale.ROOT))); 350 351 Territories.initializeInternalData(); 352 OsmValidator.initialize(); 353 MapPaintStyles.readFromPreferences(); 354 } 355 356 /** 357 * Parse command line arguments and do some low-level error checking. 358 * @param argArray the arguments array 359 */ 360 void parseArguments(String[] argArray) { 361 Logging.setLogLevel(Level.INFO); 362 363 OptionParser parser = new OptionParser("JOSM validate"); 364 final AtomicReference<String> currentInput = new AtomicReference<>(null); 365 for (Option o : Option.values()) { 366 if (o.requiresArgument()) { 367 parser.addArgumentParameter(o.getName(), 368 o.getOptionCount(), 369 arg -> handleOption(currentInput.get(), o, arg).ifPresent(currentInput::set)); 370 } else { 371 parser.addFlagParameter(o.getName(), () -> handleOption(o)); 372 } 373 if (o.getShortOption() != '*') { 374 parser.addShortAlias(o.getName(), Character.toString(o.getShortOption())); 375 } 376 } 377 parser.parseOptionsOrExit(Arrays.asList(argArray)); 378 } 379 380 private void handleOption(final Option option) { 381 switch (option) { 382 case HELP: 383 showHelp(); 384 System.exit(0); 385 break; 386 case DEBUG: 387 this.logLevel = Logging.LEVEL_DEBUG; 388 break; 389 case TRACE: 390 this.logLevel = Logging.LEVEL_TRACE; 391 break; 392 default: 393 throw new AssertionError("Unexpected option: " + option); 394 } 395 } 396 397 /** 398 * Handle an option 399 * @param currentInput The current input file, if any. May be {@code null}. 400 * @param option The option to parse 401 * @param argument The argument for the option 402 * @return The new input file, if any. 403 */ 404 private Optional<String> handleOption(final String currentInput, final Option option, final String argument) { 405 switch (option) { 406 case INPUT: 407 this.input.add(argument); 408 return Optional.of(argument); 409 case OUTPUT: 410 this.output.put(currentInput, argument); 411 break; 412 case CHANGE_FILE: 413 this.changeFiles.computeIfAbsent(currentInput, key -> new ArrayList<>()).add(argument); 414 break; 415 case LANGUAGE: 416 I18n.set(argument); 417 break; 418 case LOAD_PREFERENCES: 419 final Preferences tempPreferences = new Preferences(); 420 tempPreferences.enableSaveOnPut(false); 421 CustomConfigurator.XMLCommandProcessor config = new CustomConfigurator.XMLCommandProcessor(tempPreferences); 422 try (InputStream is = Utils.openStream(new File(argument).toURI().toURL())) { 423 config.openAndReadXML(is); 424 } catch (IOException e) { 425 throw new JosmRuntimeException(e); 426 } 427 final IPreferences pref = Config.getPref(); 428 if (pref instanceof MemoryPreferences) { 429 final MemoryPreferences memoryPreferences = (MemoryPreferences) pref; 430 tempPreferences.getAllSettings().entrySet().stream().filter(entry -> entry.getValue().isNew()) 431 .forEach(entry -> memoryPreferences.putSetting(entry.getKey(), entry.getValue())); 432 } else { 433 throw new JosmRuntimeException(tr("Preferences are not the expected type")); 434 } 435 break; 436 case SET: 437 438 default: 439 throw new AssertionError("Unexpected option: " + option); 440 } 441 return Optional.empty(); 442 } 443 444 private static void showHelp() { 445 System.out.println(getHelp()); 446 } 447 448 private static String getHelp() { 449 final String helpPadding = "\t "; 450 // CHECKSTYLE.OFF: SingleSpaceSeparator 451 return tr("JOSM Validation command line interface") + "\n\n" + 452 tr("Usage") + ":\n" + 453 "\tjava -jar josm.jar validate <options>\n\n" + 454 tr("Description") + ":\n" + 455 tr("Validates data and saves the result to a file.") + "\n\n"+ 456 tr("Options") + ":\n" + 457 "\t--help|-h " + tr("Show this help") + "\n" + 458 "\t--input|-i <file> " + tr("Input data file name (.osm, .validator.mapcss, .mapcss).") + '\n' + 459 helpPadding + tr("OSM files can be specified multiple times. Required.") + '\n' + 460 helpPadding + tr(".validator.mapcss and .mapcss files will stop processing on first error.") + '\n' + 461 helpPadding + tr("Non-osm files do not use --output or --change-file") + '\n' + 462 "\t--output|-o <file> " + tr("Output data file name (.geojson, line-by-line delimited for MapRoulette). Optional.") 463 + '\n' + 464 "\t--change-file|-c <file> " + tr("Change file name (.osc). Can be specified multiple times per input.") + '\n' + 465 helpPadding + tr("Changes will be applied in the specified order. Optional."); 466 // CHECKSTYLE.ON: SingleSpaceSeparator 467 } 468 } -
src/org/openstreetmap/josm/data/validation/tests/MapCSSTagChecker.java
diff --git a/src/org/openstreetmap/josm/data/validation/tests/MapCSSTagChecker.java b/src/org/openstreetmap/josm/data/validation/tests/MapCSSTagChecker.java index 7db32bd554..664dac89ed 100644
a b public class MapCSSTagChecker extends Test.TagTest { 253 253 } 254 254 } 255 255 256 /**257 * A handler for assertion error messages (for not fulfilled "assertMatch", "assertNoMatch").258 */259 @FunctionalInterface260 interface AssertionConsumer extends Consumer<String> {261 }262 263 256 /** 264 257 * Adds a new MapCSS config file from the given URL. 265 258 * @param url The unique URL of the MapCSS config file … … public class MapCSSTagChecker extends Test.TagTest { 274 267 return addMapCSS(url, checkAssertions ? Logging::warn : null); 275 268 } 276 269 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 xxx (public, primarily for ValidatorCLI) 280 */ 281 public synchronized ParseResult addMapCSS(String url, Consumer<String> assertionConsumer) throws ParseException, IOException { 278 282 CheckParameterUtil.ensureParameterNotNull(url, "url"); 279 283 ParseResult result; 280 284 try (CachedFile cache = new CachedFile(url); -
src/org/openstreetmap/josm/data/validation/tests/MapCSSTagCheckerAsserts.java
diff --git a/src/org/openstreetmap/josm/data/validation/tests/MapCSSTagCheckerAsserts.java b/src/org/openstreetmap/josm/data/validation/tests/MapCSSTagCheckerAsserts.java index d004882872..838868f9d5 100644
a b import java.util.List; 10 10 import java.util.Map; 11 11 import java.util.Optional; 12 12 import java.util.Set; 13 import java.util.function.Consumer; 13 14 import java.util.stream.Collectors; 14 15 15 16 import org.openstreetmap.josm.command.Command; … … final class MapCSSTagCheckerAsserts { 46 47 * @param assertionConsumer The handler for assertion error messages 47 48 */ 48 49 static void checkAsserts(final MapCSSTagCheckerRule check, final Map<String, Boolean> assertions, 49 final MapCSSTagChecker.AssertionConsumerassertionConsumer) {50 final Consumer<String> assertionConsumer) { 50 51 final DataSet ds = new DataSet(); 51 52 Logging.debug("Check: {0}", check); 52 53 for (final Map.Entry<String, Boolean> i : assertions.entrySet()) { -
src/org/openstreetmap/josm/data/validation/tests/MapCSSTagCheckerRule.java
diff --git a/src/org/openstreetmap/josm/data/validation/tests/MapCSSTagCheckerRule.java b/src/org/openstreetmap/josm/data/validation/tests/MapCSSTagCheckerRule.java index 4e012467e8..478eb57650 100644
a b import java.util.Map; 15 15 import java.util.Objects; 16 16 import java.util.Optional; 17 17 import java.util.Set; 18 import java.util.function.Consumer; 18 19 import java.util.function.Predicate; 19 20 import java.util.regex.Matcher; 20 21 import java.util.regex.Pattern; … … import org.openstreetmap.josm.data.osm.WaySegment; 31 32 import org.openstreetmap.josm.data.validation.Severity; 32 33 import org.openstreetmap.josm.data.validation.Test; 33 34 import org.openstreetmap.josm.data.validation.TestError; 34 import org.openstreetmap.josm.data.validation.tests.MapCSSTagChecker.AssertionConsumer;35 35 import org.openstreetmap.josm.gui.mappaint.Environment; 36 36 import org.openstreetmap.josm.gui.mappaint.Keyword; 37 37 import org.openstreetmap.josm.gui.mappaint.mapcss.Condition; … … final class MapCSSTagCheckerRule implements Predicate<OsmPrimitive> { 106 106 107 107 private static final String POSSIBLE_THROWS = "throwError/throwWarning/throwOther"; 108 108 109 static MapCSSTagCheckerRule ofMapCSSRule(final MapCSSRule rule, AssertionConsumerassertionConsumer) throws IllegalDataException {109 static MapCSSTagCheckerRule ofMapCSSRule(final MapCSSRule rule, Consumer<String> assertionConsumer) throws IllegalDataException { 110 110 final MapCSSTagCheckerRule check = new MapCSSTagCheckerRule(rule); 111 111 final Map<String, Boolean> assertions = new HashMap<>(); 112 112 for (Instruction i : rule.declaration.instructions) { … … final class MapCSSTagCheckerRule implements Predicate<OsmPrimitive> { 185 185 return readMapCSS(css, null); 186 186 } 187 187 188 static MapCSSTagChecker.ParseResult readMapCSS(Reader css, AssertionConsumerassertionConsumer) throws ParseException {188 static MapCSSTagChecker.ParseResult readMapCSS(Reader css, Consumer<String> assertionConsumer) throws ParseException { 189 189 CheckParameterUtil.ensureParameterNotNull(css, "css"); 190 190 191 191 final MapCSSStyleSource source = new MapCSSStyleSource(""); -
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 05da963669..8aa31512fa 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/io/importexport/OsmImporter.java
diff --git a/src/org/openstreetmap/josm/gui/io/importexport/OsmImporter.java b/src/org/openstreetmap/josm/gui/io/importexport/OsmImporter.java index d839be5963..4216843f60 100644
a b public class OsmImporter extends FileImporter { 97 97 final OsmImporterData data = loadLayer(in, associatedFile, 98 98 associatedFile == null ? OsmDataLayer.createNewName() : associatedFile.getName(), pm); 99 99 100 final OsmDataLayer layer = data.getLayer(); 101 // Note: addLayer calls GuiHelper.runInEDTAndWaitWithException 102 MainApplication.getLayerManager().addLayer(layer); 100 103 // FIXME: remove UI stuff from IO subsystem 101 104 GuiHelper.runInEDT(() -> { 102 OsmDataLayer layer = data.getLayer();103 MainApplication.getLayerManager().addLayer(layer);104 105 data.getPostLayerTask().run(); 105 106 data.getLayer().onPostLoadFromFile(); 106 107 }); -
src/org/openstreetmap/josm/gui/preferences/projection/CustomProjectionChoice.java
diff --git a/src/org/openstreetmap/josm/gui/preferences/projection/CustomProjectionChoice.java b/src/org/openstreetmap/josm/gui/preferences/projection/CustomProjectionChoice.java index e4db57d1d0..6dfc53eb17 100644
a b import org.openstreetmap.josm.data.projection.Projection; 23 23 import org.openstreetmap.josm.data.projection.ProjectionConfigurationException; 24 24 import org.openstreetmap.josm.data.projection.Projections; 25 25 import org.openstreetmap.josm.gui.ExtendedDialog; 26 import org.openstreetmap.josm.gui.tagging.ac.AutoCompTextField; 26 27 import org.openstreetmap.josm.gui.widgets.AbstractTextComponentValidator; 27 28 import org.openstreetmap.josm.gui.widgets.HistoryComboBox; 28 29 import org.openstreetmap.josm.gui.widgets.HtmlPanel; … … public class CustomProjectionChoice extends AbstractProjectionChoice implements 52 53 53 54 private static class PreferencePanel extends JPanel { 54 55 55 public JosmTextFieldinput;56 public AutoCompTextField<String> input; 56 57 private HistoryComboBox cbInput; 57 58 58 59 PreferencePanel(String initialText, ActionListener listener) { … … public class CustomProjectionChoice extends AbstractProjectionChoice implements 60 61 } 61 62 62 63 private void build(String initialText, final ActionListener listener) { 63 input = new JosmTextField(30);64 input = new AutoCompTextField<>(30); 64 65 cbInput = new HistoryComboBox(); 65 66 cbInput.setEditor(new BasicComboBoxEditor() { 66 67 @Override -
src/org/openstreetmap/josm/gui/progress/AbstractProgressMonitor.java
diff --git a/src/org/openstreetmap/josm/gui/progress/AbstractProgressMonitor.java b/src/org/openstreetmap/josm/gui/progress/AbstractProgressMonitor.java index 31e2c0b3c9..f8d2381f66 100644
a b public abstract class AbstractProgressMonitor implements ProgressMonitor { 233 233 * Ticks handling 234 234 ==================*/ 235 235 236 /** 237 * Update progress message 238 * @param value The percentage of completion (this and child progress) 239 */ 236 240 protected abstract void updateProgress(double value); 237 241 238 242 @Override -
new file src/org/openstreetmap/josm/gui/progress/CLIProgressMonitor.java
diff --git a/src/org/openstreetmap/josm/gui/progress/CLIProgressMonitor.java b/src/org/openstreetmap/josm/gui/progress/CLIProgressMonitor.java new file mode 100644 index 0000000000..b41ab40ace
- + 1 // License: GPL. For details, see LICENSE file. 2 package org.openstreetmap.josm.gui.progress; 3 4 import static org.openstreetmap.josm.tools.I18n.tr; 5 6 import java.awt.Component; 7 import java.util.Optional; 8 import java.util.concurrent.TimeUnit; 9 10 import org.openstreetmap.josm.tools.Logging; 11 import org.openstreetmap.josm.tools.Stopwatch; 12 import org.openstreetmap.josm.tools.Utils; 13 14 /** 15 * CLI implementation of a {@link ProgressMonitor} 16 * @author Taylor Smock 17 * @since xxx 18 */ 19 public class CLIProgressMonitor extends AbstractProgressMonitor { 20 /** The current task id */ 21 private ProgressTaskId taskId; 22 /** The current task title */ 23 private String title = ""; 24 /** The custom text (prepended with '/') */ 25 private String customText = ""; 26 /** The last time we updated the progress information */ 27 private Stopwatch lastUpdateTime; 28 /** The start time of the monitor */ 29 private Stopwatch startTime; 30 31 /** 32 * Create a new {@link CLIProgressMonitor} 33 */ 34 public CLIProgressMonitor() { 35 super(new CancelHandler()); 36 } 37 38 @Override 39 protected void doBeginTask() { 40 if (!Utils.isBlank(this.title)) { 41 Logging.info(tr("Beginning task{2}: {0}{1}", this.title, this.customText, 42 Optional.ofNullable(this.taskId).map(ProgressTaskId::getId).map(id -> ' ' + id).orElse(""))); 43 } 44 this.startTime = Stopwatch.createStarted(); 45 this.lastUpdateTime = this.startTime; 46 } 47 48 @Override 49 protected void doFinishTask() { 50 Logging.info(tr("Finishing task{2}: {0}{1} ({3})", this.title, this.customText, 51 Optional.ofNullable(this.taskId).map(ProgressTaskId::getId).map(id -> ' ' + id).orElse(""), this.startTime)); 52 this.lastUpdateTime = null; 53 } 54 55 @Override 56 protected void doSetIntermediate(boolean value) { 57 // Do nothing for now 58 } 59 60 @Override 61 protected void doSetTitle(String title) { 62 this.title = Optional.ofNullable(title).orElse(""); 63 } 64 65 @Override 66 protected void doSetCustomText(String customText) { 67 this.customText = Optional.ofNullable(customText).map(str -> '/' + str).orElse(""); 68 } 69 70 @Override 71 protected void updateProgress(double value) { 72 if (this.lastUpdateTime == null || this.lastUpdateTime.elapsed() > TimeUnit.SECONDS.toMillis(10)) { 73 this.lastUpdateTime = Stopwatch.createStarted(); 74 Logging.info(tr("Progress of task{2}: {0}{1} is {3}% ({4})", this.title, this.customText, 75 Optional.ofNullable(this.taskId).map(ProgressTaskId::getId).map(id -> ' ' + id).orElse(""), value * 100, this.startTime)); 76 } 77 } 78 79 @Override 80 public void setProgressTaskId(ProgressTaskId taskId) { 81 this.taskId = taskId; 82 } 83 84 @Override 85 public ProgressTaskId getProgressTaskId() { 86 return this.taskId; 87 } 88 89 @Override 90 public Component getWindowParent() { 91 return null; 92 } 93 } -
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..fca07f34ac
- + 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 import java.util.stream.Stream; 7 8 import javax.json.Json; 9 import javax.json.JsonArray; 10 import javax.json.JsonArrayBuilder; 11 import javax.json.JsonObject; 12 import javax.json.JsonObjectBuilder; 13 import javax.json.JsonValue; 14 15 import org.openstreetmap.josm.data.osm.DataSet; 16 import org.openstreetmap.josm.data.osm.OsmPrimitive; 17 import org.openstreetmap.josm.data.osm.WaySegment; 18 import org.openstreetmap.josm.data.validation.TestError; 19 import org.openstreetmap.josm.tools.Logging; 20 21 /** 22 * Convert {@link TestError} to MapRoulette Tasks 23 * @author Taylor Smock 24 * @since xxx 25 */ 26 public class GeoJSONMapRouletteWriter extends GeoJSONWriter { 27 28 /** 29 * Constructs a new {@code GeoJSONWriter}. 30 * @param ds The originating OSM dataset 31 */ 32 public GeoJSONMapRouletteWriter(DataSet ds) { 33 super(ds); 34 super.setOptions(Options.RIGHT_HAND_RULE, Options.WRITE_OSM_INFORMATION); 35 } 36 37 /** 38 * Convert a test error to a string 39 * @param testError The test error to convert 40 * @return The MapRoulette challenge object 41 */ 42 public Optional<JsonObject> write(final TestError testError) { 43 final JsonObjectBuilder jsonObjectBuilder = Json.createObjectBuilder(); 44 final JsonArrayBuilder featuresBuilder = Json.createArrayBuilder(); 45 final JsonObjectBuilder propertiesBuilder = Json.createObjectBuilder(); 46 propertiesBuilder.add("message", testError.getMessage()); 47 Optional.ofNullable(testError.getDescription()).ifPresent(description -> propertiesBuilder.add("description", description)); 48 propertiesBuilder.add("code", testError.getCode()); 49 propertiesBuilder.add("fixable", testError.isFixable()); 50 propertiesBuilder.add("severity", testError.getSeverity().toString()); 51 propertiesBuilder.add("severityInteger", testError.getSeverity().getLevel()); 52 propertiesBuilder.add("test", testError.getTester().getName()); 53 Stream.concat(testError.getPrimitives().stream(), testError.getHighlighted().stream()).distinct().map(p -> { 54 if (p instanceof OsmPrimitive) { 55 return p; 56 } else if (p instanceof WaySegment) { 57 return ((WaySegment) p).toWay(); 58 } 59 Logging.trace("Could not convert {0} to an OsmPrimitive", p); 60 return null; 61 }).filter(Objects::nonNull).distinct().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.trace("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..23ac2ba7a4 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.Bounds; 30 34 import org.openstreetmap.josm.data.coor.EastNorth; 31 35 import org.openstreetmap.josm.data.coor.LatLon; 32 36 import org.openstreetmap.josm.data.osm.DataSet; 37 import org.openstreetmap.josm.data.osm.INode; 38 import org.openstreetmap.josm.data.osm.IWay; 33 39 import org.openstreetmap.josm.data.osm.MultipolygonBuilder; 34 import org.openstreetmap.josm.data.osm.MultipolygonBuilder.JoinedPolygon;35 40 import org.openstreetmap.josm.data.osm.Node; 36 41 import org.openstreetmap.josm.data.osm.OsmPrimitive; 37 42 import org.openstreetmap.josm.data.osm.Relation; 43 import org.openstreetmap.josm.data.osm.RelationMember; 38 44 import org.openstreetmap.josm.data.osm.Way; 39 45 import org.openstreetmap.josm.data.osm.visitor.OsmPrimitiveVisitor; 40 46 import org.openstreetmap.josm.data.preferences.BooleanProperty; 41 47 import org.openstreetmap.josm.data.projection.Projection; 42 48 import org.openstreetmap.josm.data.projection.Projections; 43 49 import org.openstreetmap.josm.gui.mappaint.ElemStyles; 50 import org.openstreetmap.josm.tools.Geometry; 44 51 import org.openstreetmap.josm.tools.Logging; 45 52 import org.openstreetmap.josm.tools.Pair; 53 import org.openstreetmap.josm.tools.Utils; 46 54 47 55 /** 48 56 * Writes OSM data as a GeoJSON string, using JSR 353: Java API for JSON Processing (JSON-P). … … import org.openstreetmap.josm.tools.Pair; 51 59 */ 52 60 public class GeoJSONWriter { 53 61 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 54 71 private final DataSet data; 55 private final Projection projection;72 private static final Projection projection = Projections.getProjectionByCode("EPSG:4326"); // WGS 84 56 73 private static final BooleanProperty SKIP_EMPTY_NODES = new BooleanProperty("geojson.export.skip-empty-nodes", true); 57 74 private static final BooleanProperty UNTAGGED_CLOSED_IS_POLYGON = new BooleanProperty("geojson.export.untagged-closed-is-polygon", false); 58 75 private static final Set<Way> processedMultipolygonWays = new HashSet<>(); 76 private EnumSet<Options> options = EnumSet.noneOf(Options.class); 59 77 60 78 /** 61 79 * This is used to determine that a tag should be interpreted as a json … … public class GeoJSONWriter { 77 95 */ 78 96 public GeoJSONWriter(DataSet ds) { 79 97 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)); 81 110 } 82 111 83 112 /** … … public class GeoJSONWriter { 117 146 } 118 147 } 119 148 149 /** 150 * Convert a primitive to a json object 151 */ 120 152 private class GeometryPrimitiveVisitor implements OsmPrimitiveVisitor { 121 153 122 154 private final JsonObjectBuilder geomObj; … … public class GeoJSONWriter { 141 173 // no need to write this object again 142 174 return; 143 175 } 144 final JsonArrayBuilder array = getCoorsArray(w.getNodes());145 176 boolean writeAsPolygon = w.isClosed() && ((!w.isTagged() && UNTAGGED_CLOSED_IS_POLYGON.get()) 146 177 || 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); 147 183 if (writeAsPolygon) { 148 184 geomObj.add("type", "Polygon"); 149 185 geomObj.add("coordinates", Json.createArrayBuilder().add(array)); … … public class GeoJSONWriter { 159 195 if (r == null || !r.isMultipolygon() || r.hasIncompleteMembers()) { 160 196 return; 161 197 } 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); 164 275 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) 166 297 .map(p -> { 167 JsonArrayBuilder array = getCoorsArray(p .getNodes());168 LatLon ll = p.get Nodes().get(0).getCoor();298 JsonArrayBuilder array = getCoorsArray(p); 299 LatLon ll = p.get(0).getCoor(); 169 300 // since first node is not duplicated as last node 170 301 return ll != null ? array.add(getCoorArray(null, ll)) : array; 171 302 }) 172 303 .forEach(polygon::add); 173 geomObj.add("type", "MultiPolygon");174 304 final JsonArrayBuilder multiPolygon = Json.createArrayBuilder().add(polygon); 305 geomObj.add("type", "MultiPolygon"); 175 306 geomObj.add("coordinates", multiPolygon); 176 307 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 }181 308 } 182 309 183 310 private JsonArrayBuilder getCoorsArray(Iterable<Node> nodes) { … … public class GeoJSONWriter { 204 331 205 332 protected void appendPrimitive(OsmPrimitive p, JsonArrayBuilder array) { 206 333 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())) { 208 335 return; 209 336 } 210 337 211 338 // Properties 212 339 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 } 215 377 } 216 378 final JsonObject prop = propObj.build(); 217 379 -
new file test/unit/org/openstreetmap/josm/data/validation/ValidatorCLITest.java
diff --git a/test/unit/org/openstreetmap/josm/data/validation/ValidatorCLITest.java b/test/unit/org/openstreetmap/josm/data/validation/ValidatorCLITest.java new file mode 100644 index 0000000000..92466c9175
- + 1 // License: GPL. For details, see LICENSE file. 2 package org.openstreetmap.josm.data.validation; 3 4 import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; 5 import static org.junit.jupiter.api.Assertions.assertEquals; 6 import static org.junit.jupiter.api.Assertions.assertTrue; 7 import static org.junit.jupiter.api.Assertions.fail; 8 9 import java.io.ByteArrayInputStream; 10 import java.io.File; 11 import java.io.IOException; 12 import java.io.PrintWriter; 13 import java.lang.reflect.InvocationTargetException; 14 import java.nio.charset.StandardCharsets; 15 import java.nio.file.Files; 16 import java.nio.file.Path; 17 import java.nio.file.Paths; 18 import java.util.ArrayList; 19 import java.util.Collections; 20 import java.util.List; 21 import java.util.Objects; 22 import java.util.logging.Handler; 23 import java.util.logging.LogRecord; 24 import java.util.stream.Collectors; 25 import java.util.stream.Stream; 26 27 import javax.json.Json; 28 import javax.json.JsonObject; 29 import javax.json.JsonReader; 30 import javax.swing.SwingUtilities; 31 32 import org.junit.jupiter.api.AfterEach; 33 import org.junit.jupiter.api.BeforeEach; 34 import org.junit.jupiter.api.Test; 35 import org.junit.jupiter.api.io.TempDir; 36 import org.junit.jupiter.params.ParameterizedTest; 37 import org.junit.jupiter.params.provider.Arguments; 38 import org.junit.jupiter.params.provider.MethodSource; 39 import org.junit.jupiter.params.provider.ValueSource; 40 import org.openstreetmap.josm.TestUtils; 41 import org.openstreetmap.josm.data.coor.LatLon; 42 import org.openstreetmap.josm.data.osm.DataSet; 43 import org.openstreetmap.josm.data.osm.Node; 44 import org.openstreetmap.josm.gui.MainApplication; 45 import org.openstreetmap.josm.io.OsmWriter; 46 import org.openstreetmap.josm.io.OsmWriterFactory; 47 import org.openstreetmap.josm.spi.lifecycle.Lifecycle; 48 import org.openstreetmap.josm.testutils.annotations.BasicPreferences; 49 import org.openstreetmap.josm.tools.Logging; 50 import org.openstreetmap.josm.tools.Utils; 51 52 import mockit.Mock; 53 import mockit.MockUp; 54 55 /** 56 * Test class for {@link ValidatorCLI} 57 * @author Taylor Smock 58 */ 59 @BasicPreferences 60 class ValidatorCLITest { 61 @TempDir 62 static File temporaryDirectory; 63 64 TestHandler handler; 65 66 private static void synchronizeThreads() { 67 MainApplication.worker.execute(() -> { /* Sync worker thread */ }); 68 try { 69 SwingUtilities.invokeAndWait(() -> { /* Sync EDT thread */ }); 70 } catch (InterruptedException e) { 71 Thread.currentThread().interrupt(); 72 fail(e); 73 } catch (InvocationTargetException e) { 74 fail(e); 75 } 76 } 77 78 @BeforeEach 79 void setup() { 80 TestUtils.assumeWorkingJMockit(); 81 new LifecycleMock(); 82 this.handler = new TestHandler(); 83 Logging.getLogger().addHandler(this.handler); 84 } 85 86 @AfterEach 87 void tearDown() throws InterruptedException, InvocationTargetException { 88 synchronizeThreads(); 89 Logging.getLogger().removeHandler(this.handler); 90 this.handler.close(); 91 this.handler = null; 92 } 93 94 @ParameterizedTest 95 @ValueSource(strings = {"resources/styles/standard/elemstyles.mapcss", "resources/styles/standard/potlatch2.mapcss"}) 96 void testInternalMapcss(final String resourceLocation) { 97 new ValidatorCLI().processArguments(new String[]{"--input", resourceLocation}); 98 assertEquals(2, this.handler.logRecordList.size()); 99 assertEquals(resourceLocation + " had no errors", this.handler.logRecordList.get(0).getMessage()); 100 assertTrue(this.handler.logRecordList.get(1).getMessage().contains("Finishing task")); 101 } 102 103 static Stream<Arguments> testInternalValidatorMapcss() { 104 return Stream.of(Objects.requireNonNull(new File("resources/data/validator").listFiles())) 105 .filter(file -> file.getPath().endsWith(".mapcss")) 106 .map(file -> { 107 // External validator mapcss files must have validator.mapcss as the extension. 108 final String renamedValidator = file.getName().endsWith(".validator.mapcss") ? 109 file.getName() : file.getName().replace(".mapcss", ".validator.mapcss"); 110 try { 111 return Files.copy(file.toPath(), Paths.get(temporaryDirectory.getPath(), renamedValidator)).getFileName().toString(); 112 } catch (IOException e) { 113 fail(e); 114 } 115 return null; 116 }).map(Arguments::of); 117 } 118 119 @ParameterizedTest 120 @MethodSource 121 void testInternalValidatorMapcss(final String resourceLocation) { 122 final String path = Paths.get(temporaryDirectory.getPath(), resourceLocation).toString(); 123 new ValidatorCLI().processArguments(new String[]{"--input", path}); 124 assertEquals(2, this.handler.logRecordList.size(), this.handler.logRecordList.stream().map(LogRecord::getMessage).collect( 125 Collectors.joining(",\n"))); 126 assertEquals(path + " had no errors", this.handler.logRecordList.get(0).getMessage()); 127 assertTrue(this.handler.logRecordList.get(1).getMessage().contains("Finishing task")); 128 } 129 130 @Test 131 void testBadDataTicket13165() { 132 // Ticket #13165 was a validator non-regression test. 133 final String dataPath = TestUtils.getRegressionDataFile(13165, "13165.osm"); 134 final String outputPath = Paths.get(temporaryDirectory.getPath(), "testBadDataTicket13165.geojson").toString(); 135 new ValidatorCLI().processArguments(new String[]{"--input", dataPath, "--output", outputPath}); 136 final File outputFile = new File(outputPath); 137 assertTrue(outputFile.exists()); 138 synchronizeThreads(); 139 final List<JsonObject> errors = readJsonObjects(outputFile.toPath()); 140 assertEquals(3, errors.stream().map(ValidatorCLITest::getMessage).filter("Overlapping Identical Landuses"::equals).count()); 141 assertEquals(3, errors.size(), errors.stream().map(ValidatorCLITest::getMessage).collect(Collectors.joining("\n"))); 142 } 143 144 @Test 145 void testBadDataPlusChangeFile() throws IOException { 146 final ValidatorCLI validatorCLI = new ValidatorCLI(); 147 // Write test data out 148 final String osmPath = Paths.get(temporaryDirectory.getPath(), "testBadDataPlusChangeFile.osm").toString(); 149 final String changePath = Paths.get(temporaryDirectory.getPath(), "testBadDataPlusChangeFile.osc").toString(); 150 final String errorPath = Paths.get(temporaryDirectory.getPath(), "testBadDataPlusChangeFile.geojson").toString(); 151 final DataSet dataSet = new DataSet(); 152 final Node node = new Node(LatLon.ZERO); 153 node.setOsmId(1, 1); 154 dataSet.addPrimitive(node); 155 final PrintWriter printWriter = new PrintWriter(Files.newOutputStream(Paths.get(osmPath)), true); 156 final OsmWriter writer = OsmWriterFactory.createOsmWriter(printWriter, true, "0.6"); 157 writer.write(dataSet); 158 printWriter.flush(); 159 final PrintWriter changeWriter = new PrintWriter(Files.newOutputStream(Paths.get(changePath)), true); 160 changeWriter.write("<osmChange version=\"0.6\" generator=\"JOSM testBadDataPlusChangeFile\">"); 161 changeWriter.write("<delete><node id=\"1\"/></delete>"); 162 changeWriter.write("</osmChange>"); 163 changeWriter.flush(); 164 165 validatorCLI.processArguments(new String[] {"--input", osmPath, "--output", errorPath}); 166 final List<JsonObject> errors = readJsonObjects(Paths.get(errorPath)); 167 // There is already a mapped weather buoy at 0,0 (3000), and the node has no tags (201). 168 assertEquals(2, errors.size()); 169 Files.deleteIfExists(Paths.get(errorPath)); 170 171 validatorCLI.processArguments(new String[] {"--input", osmPath, "--change-file", changePath, "--output", errorPath}); 172 errors.clear(); 173 errors.addAll(readJsonObjects(Paths.get(errorPath))); 174 assertEquals(0, errors.size()); 175 Files.deleteIfExists(Paths.get(errorPath)); 176 } 177 178 /** 179 * Read json objects from a file 180 * @param path The file to read 181 * @return The json objects 182 */ 183 private static List<JsonObject> readJsonObjects(final Path path) { 184 if (Files.exists(path)) { 185 final List<String> lines = assertDoesNotThrow(() -> Files.readAllLines(path)); 186 lines.replaceAll(line -> Utils.strip(line.replace((char) 0x1e, ' '))); 187 return lines.stream().map(str -> Json.createReader(new ByteArrayInputStream(str.getBytes(StandardCharsets.UTF_8)))) 188 .map(JsonReader::readObject).collect(Collectors.toList()); 189 } 190 return Collections.emptyList(); 191 } 192 193 /** 194 * Get the validation message from a json object 195 * @param jsonObject The json object to parse 196 * @return The validator message 197 */ 198 private static String getMessage(JsonObject jsonObject) { 199 return jsonObject.getJsonArray("features").getValuesAs(JsonObject.class) 200 .stream().filter(feature -> feature.containsKey("properties")).map(feature -> feature.getJsonObject("properties")) 201 .filter(properties -> properties.containsKey("message")).map(properties -> properties.getJsonString("message").getString()) 202 .collect(Collectors.joining(",")); 203 } 204 205 /** 206 * This exists to avoid exiting the tests. 207 */ 208 private static final class LifecycleMock extends MockUp<Lifecycle> { 209 @Mock 210 public static boolean exitJosm(boolean exit, int exitCode) { 211 // No-op for now 212 return true; 213 } 214 } 215 216 private static final class TestHandler extends Handler { 217 final List<LogRecord> logRecordList = new ArrayList<>(); 218 219 @Override 220 public void publish(LogRecord record) { 221 this.logRecordList.add(record); 222 } 223 224 @Override 225 public void flush() { 226 this.logRecordList.clear(); 227 } 228 229 @Override 230 public void close() throws SecurityException { 231 this.flush(); 232 } 233 } 234 }