Ticket #15182: 15182.2.patch
| File 15182.2.patch, 62.3 KB (added by , 4 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..2e8d08125f
- + 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.mapcss.MapCSSStyleSource; 43 import org.openstreetmap.josm.gui.mappaint.mapcss.parsergen.ParseException; 44 import org.openstreetmap.josm.gui.progress.CLIProgressMonitor; 45 import org.openstreetmap.josm.gui.progress.ProgressMonitor; 46 import org.openstreetmap.josm.io.Compression; 47 import org.openstreetmap.josm.io.GeoJSONMapRouletteWriter; 48 import org.openstreetmap.josm.io.IllegalDataException; 49 import org.openstreetmap.josm.io.OsmChangeReader; 50 import org.openstreetmap.josm.spi.lifecycle.Lifecycle; 51 import org.openstreetmap.josm.spi.preferences.Config; 52 import org.openstreetmap.josm.spi.preferences.IPreferences; 53 import org.openstreetmap.josm.spi.preferences.MemoryPreferences; 54 import org.openstreetmap.josm.tools.Http1Client; 55 import org.openstreetmap.josm.tools.HttpClient; 56 import org.openstreetmap.josm.tools.I18n; 57 import org.openstreetmap.josm.tools.JosmRuntimeException; 58 import org.openstreetmap.josm.tools.Logging; 59 import org.openstreetmap.josm.tools.OptionParser; 60 import org.openstreetmap.josm.tools.Stopwatch; 61 import org.openstreetmap.josm.tools.Territories; 62 import org.openstreetmap.josm.tools.Utils; 63 64 /** 65 * Add a validate command to the JOSM command line interface. 66 * @author Taylor Smock 67 * @since xxx 68 */ 69 public class ValidatorCLI implements CLIModule { 70 public static final ValidatorCLI INSTANCE = new ValidatorCLI(); 71 72 /** The input file(s) */ 73 private final List<String> input = new ArrayList<>(); 74 /** The change files. input file -> list of change files */ 75 private final Map<String, List<String>> changeFiles = new HashMap<>(); 76 /** The output file(s). If {@code null}, use input filename as base (replace extension with geojson). input -> output */ 77 private final Map<String, String> output = new HashMap<>(); 78 79 private static final Supplier<ProgressMonitor> progressMonitorFactory = CLIProgressMonitor::new; 80 81 /** The log level */ 82 private Level logLevel; 83 84 private enum Option { 85 /** --help Show the help for validate */ 86 HELP(false, 'h'), 87 /** --input=<input-file> Set the current input file */ 88 INPUT(true, 'i', OptionParser.OptionCount.MULTIPLE), 89 /** --output=<output-file> Set the output file for the current input file */ 90 OUTPUT(true, 'o', OptionParser.OptionCount.MULTIPLE), 91 /** --change-file=<change-file> Add a change file */ 92 CHANGE_FILE(true, 'c', OptionParser.OptionCount.MULTIPLE), 93 /** --debug Set logging level to debug */ 94 DEBUG(false, '*'), 95 /** --trace Set logging level to trace */ 96 TRACE(false, '*'), 97 /** --language=<language> Set the language */ 98 LANGUAGE(true, 'l'), 99 /** --load-preferences=<url-to-xml> Changes preferences according to the XML file */ 100 LOAD_PREFERENCES(true, 'p'), 101 /** --set=<key>=<value> Set preference key to value */ 102 SET(true, 's'); 103 104 private final String name; 105 private final boolean requiresArgument; 106 private final char shortOption; 107 private final OptionParser.OptionCount optionCount; 108 Option(final boolean requiresArgument, final char shortOption) { 109 this(requiresArgument, shortOption, OptionParser.OptionCount.OPTIONAL); 110 } 111 112 Option(final boolean requiresArgument, final char shortOption, final OptionParser.OptionCount optionCount) { 113 this.name = name().toLowerCase(Locale.ROOT).replace('_', '-'); 114 this.requiresArgument = requiresArgument; 115 this.shortOption = shortOption; 116 this.optionCount = optionCount; 117 } 118 119 /** 120 * Replies the option name 121 * @return The option name, in lowercase 122 */ 123 public String getName() { 124 return this.name; 125 } 126 127 /** 128 * Get the number of times this option should be seen 129 * @return The option count 130 */ 131 public OptionParser.OptionCount getOptionCount() { 132 return this.optionCount; 133 } 134 135 /** 136 * Replies the short option (single letter) associated with this option. 137 * @return the short option or '*' if there is no short option 138 */ 139 public char getShortOption() { 140 return this.shortOption; 141 } 142 143 /** 144 * Determines if this option requires an argument. 145 * @return {@code true} if this option requires an argument, {@code false} otherwise 146 */ 147 public boolean requiresArgument() { 148 return this.requiresArgument; 149 } 150 151 } 152 153 @Override 154 public String getActionKeyword() { 155 return "validate"; 156 } 157 158 @Override 159 public void processArguments(final String[] argArray) { 160 try { 161 // Ensure that preferences are only in memory 162 Config.setPreferencesInstance(new MemoryPreferences()); 163 Logging.setLogLevel(Level.INFO); 164 this.parseArguments(argArray); 165 if (this.input.isEmpty()) { 166 throw new IllegalArgumentException(tr("Missing argument - input data file ({0})", "--input|-i")); 167 } 168 this.initialize(); 169 final ProgressMonitor fileMonitor = progressMonitorFactory.get(); 170 fileMonitor.beginTask(tr("Processing files..."), this.input.size()); 171 for (String inputFile : this.input) { 172 if (inputFile.endsWith(".validator.mapcss")) { 173 this.processValidatorFile(inputFile); 174 } else if (inputFile.endsWith(".mapcss")) { 175 this.processMapcssFile(inputFile); 176 } else { 177 this.processFile(inputFile); 178 } 179 fileMonitor.worked(1); 180 } 181 fileMonitor.finishTask(); 182 } catch (Exception e) { 183 Logging.info(e); 184 Lifecycle.exitJosm(true, 1); 185 } 186 Lifecycle.exitJosm(true, 0); 187 } 188 189 /** 190 * Process a standard mapcss file 191 * @param inputFile The mapcss file to validate 192 * @throws ParseException if the file does not match the mapcss syntax 193 */ 194 private void processMapcssFile(final String inputFile) throws ParseException { 195 final MapCSSStyleSource styleSource = new MapCSSStyleSource(new File(inputFile).toURI().getPath(), inputFile, inputFile); 196 styleSource.loadStyleSource(); 197 if (!styleSource.getErrors().isEmpty()) { 198 throw new ParseException(trn("{0} had {1} error", "{0} had {1} errors", styleSource.getErrors().size(), 199 inputFile, styleSource.getErrors().size())); 200 } else { 201 Logging.info(tr("{0} had no errors", inputFile)); 202 } 203 } 204 205 /** 206 * Process a validator file 207 * @param inputFile The file to check 208 * @throws IOException if there is a problem reading the file 209 * @throws ParseException if the file does not match the validator mapcss syntax 210 */ 211 private void processValidatorFile(final String inputFile) throws ParseException, IOException { 212 // Check asserts 213 Config.getPref().putBoolean("validator.check_assert_local_rules", true); 214 final MapCSSTagChecker mapCSSTagChecker = new MapCSSTagChecker(); 215 final Collection<String> assertionErrors = new ArrayList<>(); 216 final MapCSSTagChecker.ParseResult result = mapCSSTagChecker.addMapCSS(new File(inputFile).toURI().getPath(), 217 assertionErrors::add); 218 if (!result.parseErrors.isEmpty() || !assertionErrors.isEmpty()) { 219 for (Throwable throwable : result.parseErrors) { 220 Logging.error(throwable); 221 } 222 for (String error : assertionErrors) { 223 Logging.error(error); 224 } 225 throw new ParseException(trn("{0} had {1} error", "{0} had {1} errors", result.parseErrors.size() + assertionErrors.size(), 226 inputFile, result.parseErrors.size() + assertionErrors.size())); 227 } else { 228 Logging.info(tr("{0} had no errors"), inputFile); 229 } 230 } 231 232 /** 233 * Process an OSM file 234 * @param inputFile The input filename 235 * @throws IllegalArgumentException If an argument is not valid 236 * @throws IllegalDataException If there is bad data 237 * @throws IOException If a file could not be read or written 238 */ 239 private void processFile(final String inputFile) throws IllegalDataException, IOException { 240 final File inputFileFile = new File(inputFile); 241 final List<FileImporter> inputFileImporters = ExtensionFileFilter.getImporters().stream() 242 .filter(importer -> importer.acceptFile(inputFileFile)).collect(Collectors.toList()); 243 final Stopwatch stopwatch = Stopwatch.createStarted(); 244 if (inputFileImporters.stream().noneMatch(fileImporter -> 245 fileImporter.importDataHandleExceptions(inputFileFile, progressMonitorFactory.get()))) { 246 throw new IOException(tr("Could not load input file: {0}", inputFile)); 247 } 248 final String outputFile = Optional.ofNullable(this.output.get(inputFile)).orElseGet(() -> getDefaultOutputName(inputFile)); 249 final String task = tr("Validating {0}, saving output to {1}", inputFile, outputFile); 250 OsmDataLayer dataLayer = null; 251 try { 252 Logging.info(task); 253 OsmValidator.initializeTests(); 254 dataLayer = MainApplication.getLayerManager().getLayersOfType(OsmDataLayer.class) 255 .stream().filter(layer -> inputFileFile.equals(layer.getAssociatedFile())) 256 .findFirst().orElseThrow(() -> new JosmRuntimeException(tr("Could not find a layer for {0}", inputFile))); 257 final DataSet dataSet = dataLayer.getDataSet(); 258 if (this.changeFiles.containsKey(inputFile)) { 259 ProgressMonitor changeFilesMonitor = progressMonitorFactory.get(); 260 changeFilesMonitor.beginTask(tr("Reading change files"), this.changeFiles.size()); 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 changeFilesMonitor.finishTask(); 267 } 268 Collection<Test> tests = OsmValidator.getEnabledTests(false); 269 if (Files.isRegularFile(Paths.get(outputFile)) && !Files.deleteIfExists(Paths.get(outputFile))) { 270 Logging.error("Could not delete {0}, attempting to append", outputFile); 271 } 272 GeoJSONMapRouletteWriter geoJSONMapRouletteWriter = new GeoJSONMapRouletteWriter(dataSet); 273 try (OutputStream fileOutputStream = Files.newOutputStream(Paths.get(outputFile))) { 274 tests.parallelStream().forEach(test -> runTest(test, geoJSONMapRouletteWriter, fileOutputStream, dataSet)); 275 } 276 } finally { 277 if (dataLayer != null) { 278 MainApplication.getLayerManager().removeLayer(dataLayer); 279 } 280 Logging.info(stopwatch.toString(task)); 281 } 282 } 283 284 /** 285 * Get the default output name 286 * @param inputString The input file 287 * @return The default output name for the input file (extension stripped, ".geojson" added) 288 */ 289 private static String getDefaultOutputName(final String inputString) { 290 final String extension = FileNameUtils.getExtension(inputString); 291 if (!Arrays.asList("zip", "bz", "xz", "geojson").contains(extension)) { 292 return FileNameUtils.getBaseName(inputString) + ".geojson"; 293 } else if ("geojson".equals(extension)) { 294 // Account for geojson input files 295 return FileNameUtils.getBaseName(inputString) + ".validated.geojson"; 296 } 297 return FileNameUtils.getBaseName(FileNameUtils.getBaseName(inputString)) + ".geojson"; 298 } 299 300 /** 301 * Run a test 302 * @param test The test to run 303 * @param geoJSONMapRouletteWriter The object to use to create challenges 304 * @param fileOutputStream The location to write data to 305 * @param dataSet The dataset to check 306 */ 307 private void runTest(final Test test, final GeoJSONMapRouletteWriter geoJSONMapRouletteWriter, 308 final OutputStream fileOutputStream, DataSet dataSet) { 309 test.startTest(progressMonitorFactory.get()); 310 test.visit(dataSet.allPrimitives()); 311 test.endTest(); 312 test.getErrors().stream().map(geoJSONMapRouletteWriter::write) 313 .filter(Optional::isPresent).map(Optional::get) 314 .map(jsonObject -> jsonObject.toString().getBytes(StandardCharsets.UTF_8)).forEach(bytes -> { 315 try { 316 writeToFile(fileOutputStream, bytes); 317 } catch (IOException e) { 318 throw new JosmRuntimeException(e); 319 } 320 }); 321 test.clear(); 322 } 323 324 /** 325 * Write to a file. Synchronized to avoid writing to the same file in different threads. 326 * 327 * @param fileOutputStream The file output stream to read 328 * @param bytes The bytes to write (surrounded by RS and LF) 329 * @throws IOException If we couldn't write to file 330 */ 331 private synchronized void writeToFile(final OutputStream fileOutputStream, final byte[] bytes) 332 throws IOException { 333 // Write the ASCII Record Separator character 334 fileOutputStream.write(0x1e); 335 fileOutputStream.write(bytes); 336 // Write the ASCII Line Feed character 337 fileOutputStream.write(0x0a); 338 } 339 340 /** 341 * Initialize everything that might be needed 342 * 343 * Arguments may need to be parsed first. 344 */ 345 void initialize() { 346 Logging.setLogLevel(this.logLevel); 347 HttpClient.setFactory(Http1Client::new); 348 Config.setBaseDirectoriesProvider(JosmBaseDirectories.getInstance()); // for right-left-hand traffic cache file 349 Config.setUrlsProvider(JosmUrls.getInstance()); 350 ProjectionRegistry.setProjection(Projections.getProjectionByCode("epsg:3857".toUpperCase(Locale.ROOT))); 351 352 Territories.initializeInternalData(); 353 OsmValidator.initialize(); 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 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/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..bdd773d149
- + 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.nio.charset.StandardCharsets; 13 import java.nio.file.Files; 14 import java.nio.file.Paths; 15 import java.util.ArrayList; 16 import java.util.List; 17 import java.util.function.Function; 18 import java.util.logging.Handler; 19 import java.util.logging.LogRecord; 20 import java.util.stream.Collectors; 21 import java.util.stream.Stream; 22 23 import javax.json.Json; 24 import javax.json.JsonObject; 25 import javax.json.JsonReader; 26 27 import org.junit.jupiter.api.AfterEach; 28 import org.junit.jupiter.api.BeforeEach; 29 import org.junit.jupiter.api.Test; 30 import org.junit.jupiter.api.io.TempDir; 31 import org.junit.jupiter.params.ParameterizedTest; 32 import org.junit.jupiter.params.provider.Arguments; 33 import org.junit.jupiter.params.provider.MethodSource; 34 import org.junit.jupiter.params.provider.ValueSource; 35 import org.openstreetmap.josm.TestUtils; 36 import org.openstreetmap.josm.spi.lifecycle.Lifecycle; 37 import org.openstreetmap.josm.tools.Logging; 38 import org.openstreetmap.josm.tools.Utils; 39 40 import mockit.Mock; 41 import mockit.MockUp; 42 43 /** 44 * Test class for {@link ValidatorCLI} 45 * @author Taylor Smock 46 */ 47 class ValidatorCLITest { 48 @TempDir 49 static File temporaryDirectory; 50 51 TestHandler handler; 52 53 @BeforeEach 54 void setup() { 55 TestUtils.assumeWorkingJMockit(); 56 new LifecycleMock(); 57 this.handler = new TestHandler(); 58 Logging.getLogger().addHandler(this.handler); 59 } 60 61 @AfterEach 62 void tearDown() { 63 Logging.getLogger().removeHandler(this.handler); 64 this.handler.close(); 65 this.handler = null; 66 } 67 68 @ParameterizedTest 69 @ValueSource(strings = {"resources/styles/standard/elemstyles.mapcss", "resources/styles/standard/potlatch2.mapcss"}) 70 void testInternalMapcss(final String resourceLocation) { 71 new ValidatorCLI().processArguments(new String[]{"--input", resourceLocation}); 72 assertEquals(2, this.handler.logRecordList.size()); 73 assertEquals(resourceLocation + " had no errors", this.handler.logRecordList.get(0).getMessage()); 74 assertTrue(this.handler.logRecordList.get(1).getMessage().contains("Finishing task")); 75 } 76 77 static Stream<Arguments> testInternalValidatorMapcss() { 78 return Stream.of(new File("resources/data/validator").listFiles()) 79 .filter(file -> file.getPath().endsWith(".mapcss")) 80 .map(file -> { 81 // External validator mapcss files must have validator.mapcss as the extension. 82 final String renamedValidator = file.getName().endsWith(".validator.mapcss") ? 83 file.getName() : file.getName().replace(".mapcss", ".validator.mapcss"); 84 try { 85 return Files.copy(file.toPath(), Paths.get(temporaryDirectory.getPath(), renamedValidator)).getFileName().toString(); 86 } catch (IOException e) { 87 fail(e); 88 } 89 return null; 90 }).map(Arguments::of); 91 } 92 93 @ParameterizedTest 94 @MethodSource 95 void testInternalValidatorMapcss(final String resourceLocation) { 96 97 final String path = Paths.get(temporaryDirectory.getPath(), resourceLocation).toString(); 98 new ValidatorCLI().processArguments(new String[]{"--input", path}); 99 assertEquals(2, this.handler.logRecordList.size()); 100 assertEquals(path + " had no errors", this.handler.logRecordList.get(0).getMessage()); 101 assertTrue(this.handler.logRecordList.get(1).getMessage().contains("Finishing task")); 102 } 103 104 @Test 105 void testBadDataTicket13165() { 106 // Ticket #13165 was a validator non-regression test. 107 final String dataPath = TestUtils.getRegressionDataFile(13165, "13165.osm"); 108 final String outputPath = Paths.get(temporaryDirectory.getPath(), "testBadDataTicket13165.geojson").toString(); 109 new ValidatorCLI().processArguments(new String[]{"--input", dataPath, "--output", outputPath}); 110 final File outputFile = new File(outputPath); 111 assertTrue(outputFile.exists()); 112 final List<String> lines = assertDoesNotThrow(() -> Files.readAllLines(outputFile.toPath())); 113 lines.replaceAll(line -> Utils.strip(line.replace((char) 0x1e, ' '))); 114 final List<JsonObject> errors = lines.stream().map(str -> Json.createReader(new ByteArrayInputStream(str.getBytes( 115 StandardCharsets.UTF_8)))).map(JsonReader::readObject).collect(Collectors.toList()); 116 final Function<JsonObject, String> getMessage = obj -> obj.getJsonArray("features").getValuesAs(JsonObject.class) 117 .stream().filter(feature -> feature.containsKey("properties")).map(feature -> feature.getJsonObject("properties")) 118 .filter(properties -> properties.containsKey("message")).map(properties -> properties.getJsonString("message").getString()) 119 .collect(Collectors.joining(",")); 120 assertEquals(3, errors.stream().map(getMessage).filter("Overlapping Identical Landuses"::equals).count()); 121 assertEquals(3, errors.stream().map(getMessage).filter("No area style for multipolygon"::equals).count()); 122 } 123 124 /** 125 * This exists to avoid exiting the tests. 126 */ 127 private static final class LifecycleMock extends MockUp<Lifecycle> { 128 @Mock 129 public static boolean exitJosm(boolean exit, int exitCode) { 130 // No-op for now 131 return true; 132 } 133 } 134 135 private static final class TestHandler extends Handler { 136 final List<LogRecord> logRecordList = new ArrayList<>(); 137 138 @Override 139 public void publish(LogRecord record) { 140 this.logRecordList.add(record); 141 } 142 143 @Override 144 public void flush() { 145 this.logRecordList.clear(); 146 } 147 148 @Override 149 public void close() throws SecurityException { 150 this.flush(); 151 } 152 } 153 }
