| 1 | // License: GPL. For details, see LICENSE file.
|
|---|
| 2 |
|
|---|
| 3 | import java.awt.Graphics2D;
|
|---|
| 4 | import java.awt.image.BufferedImage;
|
|---|
| 5 | import java.io.BufferedReader;
|
|---|
| 6 | import java.io.IOException;
|
|---|
| 7 | import java.io.OutputStream;
|
|---|
| 8 | import java.io.StringWriter;
|
|---|
| 9 | import java.io.UncheckedIOException;
|
|---|
| 10 | import java.io.Writer;
|
|---|
| 11 | import java.nio.file.Files;
|
|---|
| 12 | import java.nio.file.Path;
|
|---|
| 13 | import java.nio.file.Paths;
|
|---|
| 14 | import java.time.Instant;
|
|---|
| 15 | import java.time.ZoneId;
|
|---|
| 16 | import java.time.format.DateTimeFormatter;
|
|---|
| 17 | import java.util.ArrayList;
|
|---|
| 18 | import java.util.Arrays;
|
|---|
| 19 | import java.util.Collection;
|
|---|
| 20 | import java.util.Collections;
|
|---|
| 21 | import java.util.EnumSet;
|
|---|
| 22 | import java.util.LinkedHashMap;
|
|---|
| 23 | import java.util.List;
|
|---|
| 24 | import java.util.Locale;
|
|---|
| 25 | import java.util.Map;
|
|---|
| 26 | import java.util.Optional;
|
|---|
| 27 | import java.util.Set;
|
|---|
| 28 | import java.util.regex.Matcher;
|
|---|
| 29 | import java.util.regex.Pattern;
|
|---|
| 30 | import java.util.stream.Collectors;
|
|---|
| 31 | import java.util.stream.Stream;
|
|---|
| 32 |
|
|---|
| 33 | import javax.imageio.ImageIO;
|
|---|
| 34 |
|
|---|
| 35 | import org.openstreetmap.josm.actions.DeleteAction;
|
|---|
| 36 | import org.openstreetmap.josm.command.DeleteCommand;
|
|---|
| 37 | import org.openstreetmap.josm.data.Preferences;
|
|---|
| 38 | import org.openstreetmap.josm.data.Version;
|
|---|
| 39 | import org.openstreetmap.josm.data.coor.LatLon;
|
|---|
| 40 | import org.openstreetmap.josm.data.osm.Node;
|
|---|
| 41 | import org.openstreetmap.josm.data.osm.OsmPrimitive;
|
|---|
| 42 | import org.openstreetmap.josm.data.osm.Tag;
|
|---|
| 43 | import org.openstreetmap.josm.data.osm.Way;
|
|---|
| 44 | import org.openstreetmap.josm.data.osm.visitor.paint.MapPaintSettings;
|
|---|
| 45 | import org.openstreetmap.josm.data.osm.visitor.paint.StyledMapRenderer;
|
|---|
| 46 | import org.openstreetmap.josm.data.preferences.JosmBaseDirectories;
|
|---|
| 47 | import org.openstreetmap.josm.data.preferences.JosmUrls;
|
|---|
| 48 | import org.openstreetmap.josm.data.preferences.sources.ExtendedSourceEntry;
|
|---|
| 49 | import org.openstreetmap.josm.data.preferences.sources.SourceEntry;
|
|---|
| 50 | import org.openstreetmap.josm.data.projection.ProjectionRegistry;
|
|---|
| 51 | import org.openstreetmap.josm.data.projection.Projections;
|
|---|
| 52 | import org.openstreetmap.josm.gui.NavigatableComponent;
|
|---|
| 53 | import org.openstreetmap.josm.gui.mappaint.Cascade;
|
|---|
| 54 | import org.openstreetmap.josm.gui.mappaint.Environment;
|
|---|
| 55 | import org.openstreetmap.josm.gui.mappaint.MapPaintStyles;
|
|---|
| 56 | import org.openstreetmap.josm.gui.mappaint.MultiCascade;
|
|---|
| 57 | import org.openstreetmap.josm.gui.mappaint.mapcss.ConditionFactory;
|
|---|
| 58 | import org.openstreetmap.josm.gui.mappaint.mapcss.MapCSSRule;
|
|---|
| 59 | import org.openstreetmap.josm.gui.mappaint.mapcss.MapCSSStyleSource;
|
|---|
| 60 | import org.openstreetmap.josm.gui.mappaint.mapcss.parsergen.MapCSSParser;
|
|---|
| 61 | import org.openstreetmap.josm.gui.mappaint.mapcss.parsergen.ParseException;
|
|---|
| 62 | import org.openstreetmap.josm.gui.mappaint.styleelement.AreaElement;
|
|---|
| 63 | import org.openstreetmap.josm.gui.mappaint.styleelement.LineElement;
|
|---|
| 64 | import org.openstreetmap.josm.gui.mappaint.styleelement.StyleElement;
|
|---|
| 65 | import org.openstreetmap.josm.gui.preferences.map.TaggingPresetPreference;
|
|---|
| 66 | import org.openstreetmap.josm.gui.tagging.presets.TaggingPreset;
|
|---|
| 67 | import org.openstreetmap.josm.gui.tagging.presets.TaggingPresetReader;
|
|---|
| 68 | import org.openstreetmap.josm.gui.tagging.presets.TaggingPresetType;
|
|---|
| 69 | import org.openstreetmap.josm.gui.tagging.presets.items.CheckGroup;
|
|---|
| 70 | import org.openstreetmap.josm.gui.tagging.presets.items.KeyedItem;
|
|---|
| 71 | import org.openstreetmap.josm.io.CachedFile;
|
|---|
| 72 | import org.openstreetmap.josm.io.OsmTransferException;
|
|---|
| 73 | import org.openstreetmap.josm.spi.preferences.Config;
|
|---|
| 74 | import org.openstreetmap.josm.tools.Http1Client;
|
|---|
| 75 | import org.openstreetmap.josm.tools.HttpClient;
|
|---|
| 76 | import org.openstreetmap.josm.tools.Logging;
|
|---|
| 77 | import org.openstreetmap.josm.tools.OptionParser;
|
|---|
| 78 | import org.openstreetmap.josm.tools.Territories;
|
|---|
| 79 | import org.openstreetmap.josm.tools.Utils;
|
|---|
| 80 | import org.xml.sax.SAXException;
|
|---|
| 81 |
|
|---|
| 82 | import jakarta.json.Json;
|
|---|
| 83 | import jakarta.json.JsonArrayBuilder;
|
|---|
| 84 | import jakarta.json.JsonObjectBuilder;
|
|---|
| 85 | import jakarta.json.JsonWriter;
|
|---|
| 86 | import jakarta.json.stream.JsonGenerator;
|
|---|
| 87 |
|
|---|
| 88 | /**
|
|---|
| 89 | * Extracts tag information for the taginfo project.
|
|---|
| 90 | * <p>
|
|---|
| 91 | * Run from the base directory of a JOSM checkout:
|
|---|
| 92 | * <p>
|
|---|
| 93 | * <pre>
|
|---|
| 94 | * java -cp dist/josm-custom.jar TagInfoExtract --type mappaint
|
|---|
| 95 | * java -cp dist/josm-custom.jar TagInfoExtract --type presets
|
|---|
| 96 | * java -cp dist/josm-custom.jar TagInfoExtract --type external_presets
|
|---|
| 97 | * </pre>
|
|---|
| 98 | */
|
|---|
| 99 | public class TagInfoExtract {
|
|---|
| 100 |
|
|---|
| 101 | /**
|
|---|
| 102 | * Main method.
|
|---|
| 103 | * @param args Main program arguments
|
|---|
| 104 | * @throws IOException if an IO exception occurs
|
|---|
| 105 | * @throws OsmTransferException if something happened when communicating with the OSM server
|
|---|
| 106 | * @throws ParseException if there was an issue parsing MapCSS
|
|---|
| 107 | * @throws SAXException if there was an issue parsing XML
|
|---|
| 108 | */
|
|---|
| 109 | public static void main(String[] args) throws IOException, OsmTransferException, ParseException, SAXException {
|
|---|
| 110 | HttpClient.setFactory(Http1Client::new);
|
|---|
| 111 | TagInfoExtract script = new TagInfoExtract();
|
|---|
| 112 | script.parseCommandLineArguments(args);
|
|---|
| 113 | script.init();
|
|---|
| 114 | switch (script.options.mode) {
|
|---|
| 115 | case MAPPAINT:
|
|---|
| 116 | script.new StyleSheet().run();
|
|---|
| 117 | break;
|
|---|
| 118 | case PRESETS:
|
|---|
| 119 | script.new Presets().run();
|
|---|
| 120 | break;
|
|---|
| 121 | case EXTERNAL_PRESETS:
|
|---|
| 122 | script.new ExternalPresets().run();
|
|---|
| 123 | break;
|
|---|
| 124 | }
|
|---|
| 125 | if (!script.options.noexit) {
|
|---|
| 126 | System.exit(0);
|
|---|
| 127 | }
|
|---|
| 128 | }
|
|---|
| 129 |
|
|---|
| 130 | enum Mode {
|
|---|
| 131 | MAPPAINT, PRESETS, EXTERNAL_PRESETS
|
|---|
| 132 | }
|
|---|
| 133 |
|
|---|
| 134 | private final Options options = new Options();
|
|---|
| 135 |
|
|---|
| 136 | /**
|
|---|
| 137 | * Parse command line arguments.
|
|---|
| 138 | * @param args command line arguments
|
|---|
| 139 | */
|
|---|
| 140 | private void parseCommandLineArguments(String[] args) {
|
|---|
| 141 | if (args.length == 1 && "--help".equals(args[0])) {
|
|---|
| 142 | this.usage();
|
|---|
| 143 | }
|
|---|
| 144 | final OptionParser parser = new OptionParser(getClass().getName());
|
|---|
| 145 | parser.addArgumentParameter("type", OptionParser.OptionCount.REQUIRED, options::setMode);
|
|---|
| 146 | parser.addArgumentParameter("input", OptionParser.OptionCount.OPTIONAL, options::setInputFile);
|
|---|
| 147 | parser.addArgumentParameter("output", OptionParser.OptionCount.OPTIONAL, options::setOutputFile);
|
|---|
| 148 | parser.addArgumentParameter("imgdir", OptionParser.OptionCount.OPTIONAL, options::setImageDir);
|
|---|
| 149 | parser.addArgumentParameter("imgurlprefix", OptionParser.OptionCount.OPTIONAL, options::setImageUrlPrefix);
|
|---|
| 150 | parser.addFlagParameter("noexit", options::setNoExit);
|
|---|
| 151 | parser.addFlagParameter("help", this::usage);
|
|---|
| 152 | parser.parseOptionsOrExit(Arrays.asList(args));
|
|---|
| 153 | }
|
|---|
| 154 |
|
|---|
| 155 | private void usage() {
|
|---|
| 156 | System.out.println("java " + getClass().getName());
|
|---|
| 157 | System.out.println(" --type TYPE\tthe project type to be generated: " + Arrays.toString(Mode.values()));
|
|---|
| 158 | System.out.println(" --input FILE\tthe input file to use (overrides defaults for types mappaint, presets)");
|
|---|
| 159 | System.out.println(" --output FILE\tthe output file to use (defaults to STDOUT)");
|
|---|
| 160 | System.out.println(" --imgdir DIRECTORY\tthe directory to put the generated images in (default: " + options.imageDir + ")");
|
|---|
| 161 | System.out.println(" --imgurlprefix STRING\timage URLs prefix for generated image files (public path on webserver)");
|
|---|
| 162 | System.out.println(" --noexit\tdo not call System.exit(), for use from Ant script");
|
|---|
| 163 | System.out.println(" --help\tshow this help");
|
|---|
| 164 | System.exit(0);
|
|---|
| 165 | }
|
|---|
| 166 |
|
|---|
| 167 | private static final class Options {
|
|---|
| 168 | Mode mode;
|
|---|
| 169 | int josmSvnRevision = Version.getInstance().getVersion();
|
|---|
| 170 | Path baseDir = Paths.get("");
|
|---|
| 171 | Path imageDir = Paths.get("taginfo-img");
|
|---|
| 172 | String imageUrlPrefix;
|
|---|
| 173 | CachedFile inputFile;
|
|---|
| 174 | Path outputFile;
|
|---|
| 175 | boolean noexit;
|
|---|
| 176 |
|
|---|
| 177 | void setMode(String value) {
|
|---|
| 178 | mode = Mode.valueOf(value.toUpperCase(Locale.ENGLISH));
|
|---|
| 179 | switch (mode) {
|
|---|
| 180 | case MAPPAINT:
|
|---|
| 181 | inputFile = new CachedFile("resource://styles/standard/elemstyles.mapcss");
|
|---|
| 182 | break;
|
|---|
| 183 | case PRESETS:
|
|---|
| 184 | inputFile = new CachedFile("resource://data/defaultpresets.xml");
|
|---|
| 185 | break;
|
|---|
| 186 | default:
|
|---|
| 187 | inputFile = null;
|
|---|
| 188 | }
|
|---|
| 189 | }
|
|---|
| 190 |
|
|---|
| 191 | void setInputFile(String value) {
|
|---|
| 192 | inputFile = new CachedFile(value);
|
|---|
| 193 | }
|
|---|
| 194 |
|
|---|
| 195 | void setOutputFile(String value) {
|
|---|
| 196 | outputFile = Paths.get(value);
|
|---|
| 197 | }
|
|---|
| 198 |
|
|---|
| 199 | void setImageDir(String value) {
|
|---|
| 200 | imageDir = Paths.get(value);
|
|---|
| 201 | }
|
|---|
| 202 |
|
|---|
| 203 | void setImageUrlPrefix(String value) {
|
|---|
| 204 | imageUrlPrefix = value;
|
|---|
| 205 | }
|
|---|
| 206 |
|
|---|
| 207 | void setNoExit() {
|
|---|
| 208 | noexit = true;
|
|---|
| 209 | }
|
|---|
| 210 |
|
|---|
| 211 | /**
|
|---|
| 212 | * Determine full image url (can refer to JOSM or OSM repository).
|
|---|
| 213 | * @param path the image path
|
|---|
| 214 | * @return full image url
|
|---|
| 215 | */
|
|---|
| 216 | private String findImageUrl(String path) {
|
|---|
| 217 | final Path f = baseDir.resolve("resources").resolve("images").resolve(path);
|
|---|
| 218 | if (Files.exists(f)) {
|
|---|
| 219 | return "https://josm.openstreetmap.de/export/" + josmSvnRevision + "/josm/trunk/resources/images/" + path;
|
|---|
| 220 | }
|
|---|
| 221 | throw new IllegalStateException("Cannot find image url for " + path);
|
|---|
| 222 | }
|
|---|
| 223 | }
|
|---|
| 224 |
|
|---|
| 225 | private abstract class Extractor {
|
|---|
| 226 | abstract void run() throws IOException, OsmTransferException, ParseException, SAXException;
|
|---|
| 227 |
|
|---|
| 228 | void writeJson(String name, String description, Iterable<TagInfoTag> tags) throws IOException {
|
|---|
| 229 | try (Writer writer = options.outputFile != null ? Files.newBufferedWriter(options.outputFile) : new StringWriter();
|
|---|
| 230 | JsonWriter json = Json
|
|---|
| 231 | .createWriterFactory(Collections.singletonMap(JsonGenerator.PRETTY_PRINTING, true))
|
|---|
| 232 | .createWriter(writer)) {
|
|---|
| 233 | JsonObjectBuilder project = Json.createObjectBuilder()
|
|---|
| 234 | .add("name", name)
|
|---|
| 235 | .add("description", description)
|
|---|
| 236 | .add("project_url", JosmUrls.getInstance().getJOSMWebsite())
|
|---|
| 237 | .add("icon_url", options.findImageUrl("logo_16x16x8.png"))
|
|---|
| 238 | .add("contact_name", "JOSM developer team")
|
|---|
| 239 | .add("contact_email", "josm-dev@openstreetmap.org");
|
|---|
| 240 | final JsonArrayBuilder jsonTags = Json.createArrayBuilder();
|
|---|
| 241 | for (TagInfoTag t : tags) {
|
|---|
| 242 | jsonTags.add(t.toJson());
|
|---|
| 243 | }
|
|---|
| 244 | json.writeObject(Json.createObjectBuilder()
|
|---|
| 245 | .add("data_format", 1)
|
|---|
| 246 | .add("data_updated", DateTimeFormatter.ofPattern("yyyyMMdd'T'hhmmss'Z'").withZone(ZoneId.of("Z")).format(Instant.now()))
|
|---|
| 247 | .add("project", project)
|
|---|
| 248 | .add("tags", jsonTags)
|
|---|
| 249 | .build());
|
|---|
| 250 | if (options.outputFile == null) {
|
|---|
| 251 | System.out.println(writer);
|
|---|
| 252 | }
|
|---|
| 253 | }
|
|---|
| 254 | }
|
|---|
| 255 | }
|
|---|
| 256 |
|
|---|
| 257 | private class Presets extends Extractor {
|
|---|
| 258 |
|
|---|
| 259 | @Override
|
|---|
| 260 | void run() throws IOException, OsmTransferException, SAXException {
|
|---|
| 261 | try (BufferedReader reader = options.inputFile.getContentReader()) {
|
|---|
| 262 | Collection<TaggingPreset> presets = TaggingPresetReader.readAll(reader, true);
|
|---|
| 263 | List<TagInfoTag> tags = convertPresets(presets, "", true);
|
|---|
| 264 | Logging.info("Converting {0} internal presets", tags.size());
|
|---|
| 265 | writeJson("JOSM main presets", "Tags supported by the default presets in the OSM editor JOSM", tags);
|
|---|
| 266 | }
|
|---|
| 267 | }
|
|---|
| 268 |
|
|---|
| 269 | List<TagInfoTag> convertPresets(Iterable<TaggingPreset> presets, String descriptionPrefix, boolean addImages) {
|
|---|
| 270 | final List<TagInfoTag> tags = new ArrayList<>();
|
|---|
| 271 | final Map<Tag, TagInfoTag> requiredTags = new LinkedHashMap<>();
|
|---|
| 272 | final Map<Tag, TagInfoTag> optionalTags = new LinkedHashMap<>();
|
|---|
| 273 | for (TaggingPreset preset : presets) {
|
|---|
| 274 | preset.data.stream()
|
|---|
| 275 | .flatMap(item -> item instanceof KeyedItem
|
|---|
| 276 | ? Stream.of(((KeyedItem) item))
|
|---|
| 277 | : item instanceof CheckGroup
|
|---|
| 278 | ? ((CheckGroup) item).checks.stream()
|
|---|
| 279 | : Stream.empty())
|
|---|
| 280 | .forEach(item -> {
|
|---|
| 281 | for (String value : values(item)) {
|
|---|
| 282 | Set<TagInfoTag.Type> types = TagInfoTag.Type.forPresetTypes(preset.types);
|
|---|
| 283 | if (item.isKeyRequired()) {
|
|---|
| 284 | fillTagsMap(requiredTags, item, value, preset.getName(), types,
|
|---|
| 285 | descriptionPrefix + TagInfoTag.REQUIRED_FOR_COUNT + ": ",
|
|---|
| 286 | addImages && preset.iconName != null ? options.findImageUrl(preset.iconName) : null);
|
|---|
| 287 | } else {
|
|---|
| 288 | fillTagsMap(optionalTags, item, value, preset.getName(), types,
|
|---|
| 289 | descriptionPrefix + TagInfoTag.OPTIONAL_FOR_COUNT + ": ", null);
|
|---|
| 290 | }
|
|---|
| 291 | }
|
|---|
| 292 | });
|
|---|
| 293 | }
|
|---|
| 294 | tags.addAll(requiredTags.values());
|
|---|
| 295 | tags.addAll(optionalTags.values());
|
|---|
| 296 | return tags;
|
|---|
| 297 | }
|
|---|
| 298 |
|
|---|
| 299 | private void fillTagsMap(Map<Tag, TagInfoTag> optionalTags, KeyedItem item, String value,
|
|---|
| 300 | String presetName, Set<TagInfoTag.Type> types, String descriptionPrefix, String iconUrl) {
|
|---|
| 301 | optionalTags.compute(new Tag(item.key, value), (osmTag, tagInfoTag) -> {
|
|---|
| 302 | if (tagInfoTag == null) {
|
|---|
| 303 | return new TagInfoTag(descriptionPrefix + presetName, item.key, value, types, iconUrl);
|
|---|
| 304 | } else {
|
|---|
| 305 | tagInfoTag.descriptions.add(presetName);
|
|---|
| 306 | tagInfoTag.objectTypes.addAll(types);
|
|---|
| 307 | return tagInfoTag;
|
|---|
| 308 | }
|
|---|
| 309 | });
|
|---|
| 310 | }
|
|---|
| 311 |
|
|---|
| 312 | private Collection<String> values(KeyedItem item) {
|
|---|
| 313 | final Collection<String> values = item.getValues();
|
|---|
| 314 | return values.isEmpty() || values.size() > 50 ? Collections.singleton(null) : values;
|
|---|
| 315 | }
|
|---|
| 316 | }
|
|---|
| 317 |
|
|---|
| 318 | private final class ExternalPresets extends Presets {
|
|---|
| 319 |
|
|---|
| 320 | @Override
|
|---|
| 321 | void run() throws IOException, OsmTransferException, SAXException {
|
|---|
| 322 | TaggingPresetReader.setLoadIcons(false);
|
|---|
| 323 | final Collection<ExtendedSourceEntry> sources = new TaggingPresetPreference.TaggingPresetSourceEditor().loadAndGetAvailableSources();
|
|---|
| 324 | final List<TagInfoTag> tags = new ArrayList<>();
|
|---|
| 325 | for (SourceEntry source : sources) {
|
|---|
| 326 | if (source.url.startsWith("resource")) {
|
|---|
| 327 | // default presets
|
|---|
| 328 | continue;
|
|---|
| 329 | }
|
|---|
| 330 | try {
|
|---|
| 331 | Logging.info("Loading {0}", source.url);
|
|---|
| 332 | Collection<TaggingPreset> presets = TaggingPresetReader.readAll(source.url, false);
|
|---|
| 333 | final List<TagInfoTag> t = convertPresets(presets, source.title + " ", false);
|
|---|
| 334 | Logging.info("Converted {0} presets of {1} to {2} tags", presets.size(), source.title, t.size());
|
|---|
| 335 | tags.addAll(t);
|
|---|
| 336 | } catch (Exception ex) {
|
|---|
| 337 | Logging.warn("Skipping {0} due to error", source.url);
|
|---|
| 338 | Logging.warn(ex);
|
|---|
| 339 | }
|
|---|
| 340 | }
|
|---|
| 341 | writeJson("JOSM user presets", "Tags supported by the user contributed presets to the OSM editors JOSM and Vespucci", tags);
|
|---|
| 342 | }
|
|---|
| 343 | }
|
|---|
| 344 |
|
|---|
| 345 | private final class StyleSheet extends Extractor {
|
|---|
| 346 | private MapCSSStyleSource styleSource;
|
|---|
| 347 |
|
|---|
| 348 | @Override
|
|---|
| 349 | void run() throws IOException, ParseException {
|
|---|
| 350 | init();
|
|---|
| 351 | parseStyleSheet();
|
|---|
| 352 | final List<TagInfoTag> tags = convertStyleSheet();
|
|---|
| 353 | writeJson("JOSM main mappaint style", "Tags supported by the main mappaint style in the OSM editor JOSM", tags);
|
|---|
| 354 | }
|
|---|
| 355 |
|
|---|
| 356 | /**
|
|---|
| 357 | * Read the style sheet file and parse the MapCSS code.
|
|---|
| 358 | * @throws IOException if any I/O error occurs
|
|---|
| 359 | * @throws ParseException in case of parsing error
|
|---|
| 360 | */
|
|---|
| 361 | private void parseStyleSheet() throws IOException, ParseException {
|
|---|
| 362 | try (BufferedReader reader = options.inputFile.getContentReader()) {
|
|---|
| 363 | MapCSSParser parser = new MapCSSParser(reader, MapCSSParser.LexicalState.DEFAULT);
|
|---|
| 364 | styleSource = new MapCSSStyleSource("");
|
|---|
| 365 | styleSource.url = "";
|
|---|
| 366 | parser.sheet(styleSource);
|
|---|
| 367 | }
|
|---|
| 368 | }
|
|---|
| 369 |
|
|---|
| 370 | /**
|
|---|
| 371 | * Collect all the tag from the style sheet.
|
|---|
| 372 | * @return list of taginfo tags
|
|---|
| 373 | */
|
|---|
| 374 | private List<TagInfoTag> convertStyleSheet() {
|
|---|
| 375 | return styleSource.rules.stream()
|
|---|
| 376 | .flatMap(rule -> rule.selectors.stream())
|
|---|
| 377 | .flatMap(selector -> selector.getConditions().stream())
|
|---|
| 378 | .filter(ConditionFactory.SimpleKeyValueCondition.class::isInstance)
|
|---|
| 379 | .map(ConditionFactory.SimpleKeyValueCondition.class::cast)
|
|---|
| 380 | .map(condition -> condition.asTag(null))
|
|---|
| 381 | .distinct()
|
|---|
| 382 | .map(tag -> {
|
|---|
| 383 | String iconUrl = null;
|
|---|
| 384 | final EnumSet<TagInfoTag.Type> types = EnumSet.noneOf(TagInfoTag.Type.class);
|
|---|
| 385 | Optional<String> nodeUrl = new NodeChecker(tag).findUrl(true);
|
|---|
| 386 | if (nodeUrl.isPresent()) {
|
|---|
| 387 | iconUrl = nodeUrl.get();
|
|---|
| 388 | types.add(TagInfoTag.Type.NODE);
|
|---|
| 389 | }
|
|---|
| 390 | Optional<String> wayUrl = new WayChecker(tag).findUrl(iconUrl == null);
|
|---|
| 391 | if (wayUrl.isPresent()) {
|
|---|
| 392 | if (iconUrl == null) {
|
|---|
| 393 | iconUrl = wayUrl.get();
|
|---|
| 394 | }
|
|---|
| 395 | types.add(TagInfoTag.Type.WAY);
|
|---|
| 396 | }
|
|---|
| 397 | Optional<String> areaUrl = new AreaChecker(tag).findUrl(iconUrl == null);
|
|---|
| 398 | if (areaUrl.isPresent()) {
|
|---|
| 399 | if (iconUrl == null) {
|
|---|
| 400 | iconUrl = areaUrl.get();
|
|---|
| 401 | }
|
|---|
| 402 | types.add(TagInfoTag.Type.AREA);
|
|---|
| 403 | }
|
|---|
| 404 | return new TagInfoTag(null, tag.getKey(), tag.getValue(), types, iconUrl);
|
|---|
| 405 | })
|
|---|
| 406 | .collect(Collectors.toList());
|
|---|
| 407 | }
|
|---|
| 408 |
|
|---|
| 409 | /**
|
|---|
| 410 | * Check if a certain tag is supported by the style as node / way / area.
|
|---|
| 411 | */
|
|---|
| 412 | private abstract class Checker {
|
|---|
| 413 | private final Pattern reservedChars = Pattern.compile("[<>:\"|\\?\\*]");
|
|---|
| 414 |
|
|---|
| 415 | Checker(Tag tag) {
|
|---|
| 416 | this.tag = tag;
|
|---|
| 417 | }
|
|---|
| 418 |
|
|---|
| 419 | Environment applyStylesheet(OsmPrimitive osm) {
|
|---|
| 420 | osm.put(tag);
|
|---|
| 421 | MultiCascade mc = new MultiCascade();
|
|---|
| 422 |
|
|---|
| 423 | Environment env = new Environment(osm, mc, null, styleSource);
|
|---|
| 424 | for (MapCSSRule r : styleSource.rules) {
|
|---|
| 425 | env.clearSelectorMatchingInformation();
|
|---|
| 426 | if (r.matches(env)) {
|
|---|
| 427 | // ignore selector range
|
|---|
| 428 | if (env.layer == null) {
|
|---|
| 429 | env.layer = "default";
|
|---|
| 430 | }
|
|---|
| 431 | r.execute(env);
|
|---|
| 432 | }
|
|---|
| 433 | }
|
|---|
| 434 | env.layer = "default";
|
|---|
| 435 | return env;
|
|---|
| 436 | }
|
|---|
| 437 |
|
|---|
| 438 | /**
|
|---|
| 439 | * Create image file from StyleElement.
|
|---|
| 440 | * @param element style element
|
|---|
| 441 | * @param type object type
|
|---|
| 442 | * @param nc navigable component
|
|---|
| 443 | *
|
|---|
| 444 | * @return the URL
|
|---|
| 445 | */
|
|---|
| 446 | String createImage(StyleElement element, final String type, NavigatableComponent nc) {
|
|---|
| 447 | BufferedImage img = new BufferedImage(16, 16, BufferedImage.TYPE_INT_ARGB);
|
|---|
| 448 | Graphics2D g = img.createGraphics();
|
|---|
| 449 | g.setClip(0, 0, 16, 16);
|
|---|
| 450 | StyledMapRenderer renderer = new StyledMapRenderer(g, nc, false);
|
|---|
| 451 | renderer.getSettings(false);
|
|---|
| 452 | element.paintPrimitive(osm, MapPaintSettings.INSTANCE, renderer, false, false, false);
|
|---|
| 453 | final String imageName = type + "_" + normalize(tag.toString()) + ".png";
|
|---|
| 454 | try (OutputStream out = Files.newOutputStream(options.imageDir.resolve(imageName))) {
|
|---|
| 455 | ImageIO.write(img, "png", out);
|
|---|
| 456 | } catch (IOException e) {
|
|---|
| 457 | throw new UncheckedIOException(e);
|
|---|
| 458 | }
|
|---|
| 459 | final String baseUrl = options.imageUrlPrefix != null ? options.imageUrlPrefix : options.imageDir.toString();
|
|---|
| 460 | return baseUrl + "/" + imageName;
|
|---|
| 461 | }
|
|---|
| 462 |
|
|---|
| 463 | /**
|
|---|
| 464 | * Normalizes tag so that it can used as a filename on all platforms, including Windows.
|
|---|
| 465 | * @param tag OSM tag, that can contain illegal path characters
|
|---|
| 466 | * @return OSM tag with all illegal path characters replaced by underscore ('_')
|
|---|
| 467 | */
|
|---|
| 468 | String normalize(String tag) {
|
|---|
| 469 | Matcher m = reservedChars.matcher(tag);
|
|---|
| 470 | return m.find() ? m.replaceAll("_") : tag;
|
|---|
| 471 | }
|
|---|
| 472 |
|
|---|
| 473 | /**
|
|---|
| 474 | * Checks, if tag is supported and find URL for image icon in this case.
|
|---|
| 475 | *
|
|---|
| 476 | * @param generateImage if true, create or find a suitable image icon and return URL,
|
|---|
| 477 | * if false, just check if tag is supported and return true or false
|
|---|
| 478 | * @return URL for image icon if tag is supported
|
|---|
| 479 | */
|
|---|
| 480 | abstract Optional<String> findUrl(boolean generateImage);
|
|---|
| 481 |
|
|---|
| 482 | protected Tag tag;
|
|---|
| 483 | protected OsmPrimitive osm;
|
|---|
| 484 | }
|
|---|
| 485 |
|
|---|
| 486 | private class NodeChecker extends Checker {
|
|---|
| 487 | NodeChecker(Tag tag) {
|
|---|
| 488 | super(tag);
|
|---|
| 489 | }
|
|---|
| 490 |
|
|---|
| 491 | @Override
|
|---|
| 492 | Optional<String> findUrl(boolean generateImage) {
|
|---|
| 493 | this.osm = new Node(LatLon.ZERO);
|
|---|
| 494 | Environment env = applyStylesheet(osm);
|
|---|
| 495 | Cascade c = env.getCascade("default");
|
|---|
| 496 | Object image = c.get("icon-image");
|
|---|
| 497 | if (image instanceof MapPaintStyles.IconReference && !((MapPaintStyles.IconReference) image).isDeprecatedIcon()) {
|
|---|
| 498 | return Optional.of(options.findImageUrl(((MapPaintStyles.IconReference) image).iconName));
|
|---|
| 499 | }
|
|---|
| 500 | return Optional.empty();
|
|---|
| 501 | }
|
|---|
| 502 | }
|
|---|
| 503 |
|
|---|
| 504 | private class WayChecker extends Checker {
|
|---|
| 505 | WayChecker(Tag tag) {
|
|---|
| 506 | super(tag);
|
|---|
| 507 | }
|
|---|
| 508 |
|
|---|
| 509 | @Override
|
|---|
| 510 | Optional<String> findUrl(boolean generateImage) {
|
|---|
| 511 | this.osm = new Way();
|
|---|
| 512 | NavigatableComponent nc = new NavigatableComponent();
|
|---|
| 513 | Node n1 = new Node(nc.getLatLon(2, 8));
|
|---|
| 514 | Node n2 = new Node(nc.getLatLon(14, 8));
|
|---|
| 515 | ((Way) osm).addNode(n1);
|
|---|
| 516 | ((Way) osm).addNode(n2);
|
|---|
| 517 | Environment env = applyStylesheet(osm);
|
|---|
| 518 | LineElement les = LineElement.createLine(env);
|
|---|
| 519 | if (les != null) {
|
|---|
| 520 | return Optional.of(generateImage ? createImage(les, "way", nc) : "");
|
|---|
| 521 | }
|
|---|
| 522 | return Optional.empty();
|
|---|
| 523 | }
|
|---|
| 524 | }
|
|---|
| 525 |
|
|---|
| 526 | private class AreaChecker extends Checker {
|
|---|
| 527 | AreaChecker(Tag tag) {
|
|---|
| 528 | super(tag);
|
|---|
| 529 | }
|
|---|
| 530 |
|
|---|
| 531 | @Override
|
|---|
| 532 | Optional<String> findUrl(boolean generateImage) {
|
|---|
| 533 | this.osm = new Way();
|
|---|
| 534 | NavigatableComponent nc = new NavigatableComponent();
|
|---|
| 535 | Node n1 = new Node(nc.getLatLon(2, 2));
|
|---|
| 536 | Node n2 = new Node(nc.getLatLon(14, 2));
|
|---|
| 537 | Node n3 = new Node(nc.getLatLon(14, 14));
|
|---|
| 538 | Node n4 = new Node(nc.getLatLon(2, 14));
|
|---|
| 539 | ((Way) osm).addNode(n1);
|
|---|
| 540 | ((Way) osm).addNode(n2);
|
|---|
| 541 | ((Way) osm).addNode(n3);
|
|---|
| 542 | ((Way) osm).addNode(n4);
|
|---|
| 543 | ((Way) osm).addNode(n1);
|
|---|
| 544 | Environment env = applyStylesheet(osm);
|
|---|
| 545 | AreaElement aes = AreaElement.create(env);
|
|---|
| 546 | if (aes != null) {
|
|---|
| 547 | if (!generateImage) return Optional.of("");
|
|---|
| 548 | return Optional.of(createImage(aes, "area", nc));
|
|---|
| 549 | }
|
|---|
| 550 | return Optional.empty();
|
|---|
| 551 | }
|
|---|
| 552 | }
|
|---|
| 553 | }
|
|---|
| 554 |
|
|---|
| 555 | /**
|
|---|
| 556 | * POJO representing a <a href="https://wiki.openstreetmap.org/wiki/Taginfo/Projects">Taginfo tag</a>.
|
|---|
| 557 | */
|
|---|
| 558 | private static class TagInfoTag {
|
|---|
| 559 | static final String REQUIRED_FOR_COUNT = "Required for {count}";
|
|---|
| 560 | static final String OPTIONAL_FOR_COUNT = "Optional for {count}";
|
|---|
| 561 | final Collection<String> descriptions = new ArrayList<>();
|
|---|
| 562 | final String key;
|
|---|
| 563 | final String value;
|
|---|
| 564 | final Set<Type> objectTypes;
|
|---|
| 565 | final String iconURL;
|
|---|
| 566 |
|
|---|
| 567 | TagInfoTag(String description, String key, String value, Set<Type> objectTypes, String iconURL) {
|
|---|
| 568 | if (description != null) {
|
|---|
| 569 | this.descriptions.add(description);
|
|---|
| 570 | }
|
|---|
| 571 | this.key = key;
|
|---|
| 572 | this.value = value;
|
|---|
| 573 | this.objectTypes = objectTypes;
|
|---|
| 574 | this.iconURL = iconURL;
|
|---|
| 575 | }
|
|---|
| 576 |
|
|---|
| 577 | JsonObjectBuilder toJson() {
|
|---|
| 578 | final JsonObjectBuilder object = Json.createObjectBuilder();
|
|---|
| 579 | if (!descriptions.isEmpty()) {
|
|---|
| 580 | final int size = descriptions.size();
|
|---|
| 581 | object.add("description", String.join(", ", Utils.limit(descriptions, 8, "..."))
|
|---|
| 582 | .replace(REQUIRED_FOR_COUNT, size > 3 ? "Required for " + size : "Required for")
|
|---|
| 583 | .replace(OPTIONAL_FOR_COUNT, size > 3 ? "Optional for " + size : "Optional for"));
|
|---|
| 584 | }
|
|---|
| 585 | object.add("key", key);
|
|---|
| 586 | if (value != null) {
|
|---|
| 587 | object.add("value", value);
|
|---|
| 588 | }
|
|---|
| 589 | if ((!objectTypes.isEmpty())) {
|
|---|
| 590 | final JsonArrayBuilder types = Json.createArrayBuilder();
|
|---|
| 591 | objectTypes.stream().map(Enum::name).map(String::toLowerCase).forEach(types::add);
|
|---|
| 592 | object.add("object_types", types);
|
|---|
| 593 | }
|
|---|
| 594 | if (iconURL != null) {
|
|---|
| 595 | object.add("icon_url", iconURL);
|
|---|
| 596 | }
|
|---|
| 597 | return object;
|
|---|
| 598 | }
|
|---|
| 599 |
|
|---|
| 600 | enum Type {
|
|---|
| 601 | NODE, WAY, AREA, RELATION;
|
|---|
| 602 |
|
|---|
| 603 | static TagInfoTag.Type forPresetType(TaggingPresetType type) {
|
|---|
| 604 | switch (type) {
|
|---|
| 605 | case CLOSEDWAY:
|
|---|
| 606 | return AREA;
|
|---|
| 607 | case MULTIPOLYGON:
|
|---|
| 608 | return RELATION;
|
|---|
| 609 | default:
|
|---|
| 610 | return valueOf(type.toString());
|
|---|
| 611 | }
|
|---|
| 612 | }
|
|---|
| 613 |
|
|---|
| 614 | static Set<TagInfoTag.Type> forPresetTypes(Set<TaggingPresetType> types) {
|
|---|
| 615 | final EnumSet<TagInfoTag.Type> enums = EnumSet.noneOf(TagInfoTag.Type.class);
|
|---|
| 616 | return types == null ? enums : types.stream()
|
|---|
| 617 | .map(Type::forPresetType)
|
|---|
| 618 | .collect(Collectors.toCollection(() -> enums));
|
|---|
| 619 | }
|
|---|
| 620 | }
|
|---|
| 621 | }
|
|---|
| 622 |
|
|---|
| 623 | /**
|
|---|
| 624 | * Initialize the script.
|
|---|
| 625 | * @throws IOException if any I/O error occurs
|
|---|
| 626 | */
|
|---|
| 627 | private void init() throws IOException {
|
|---|
| 628 | Logging.setLogLevel(Logging.LEVEL_INFO);
|
|---|
| 629 | Preferences.main().enableSaveOnPut(false);
|
|---|
| 630 | Config.setPreferencesInstance(Preferences.main());
|
|---|
| 631 | Config.setBaseDirectoriesProvider(JosmBaseDirectories.getInstance());
|
|---|
| 632 | Config.setUrlsProvider(JosmUrls.getInstance());
|
|---|
| 633 | ProjectionRegistry.setProjection(Projections.getProjectionByCode("EPSG:3857"));
|
|---|
| 634 | Path tmpdir = Files.createTempDirectory(options.baseDir, "pref");
|
|---|
| 635 | tmpdir.toFile().deleteOnExit();
|
|---|
| 636 | System.setProperty("josm.home", tmpdir.toString());
|
|---|
| 637 | DeleteCommand.setDeletionCallback(DeleteAction.defaultDeletionCallback);
|
|---|
| 638 | Territories.initializeInternalData();
|
|---|
| 639 | Files.createDirectories(options.imageDir);
|
|---|
| 640 | }
|
|---|
| 641 | }
|
|---|