// License: GPL. For details, see LICENSE file. import java.awt.Graphics2D; import java.awt.image.BufferedImage; import java.io.BufferedReader; import java.io.IOException; import java.io.OutputStream; import java.io.StringWriter; import java.io.UncheckedIOException; import java.io.Writer; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.time.Instant; import java.time.ZoneId; import java.time.format.DateTimeFormatter; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.EnumSet; import java.util.LinkedHashMap; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Optional; import java.util.Set; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.stream.Collectors; import java.util.stream.Stream; import javax.imageio.ImageIO; import javax.json.Json; import javax.json.JsonArrayBuilder; import javax.json.JsonObjectBuilder; import javax.json.JsonWriter; import javax.json.stream.JsonGenerator; import org.openstreetmap.josm.actions.DeleteAction; import org.openstreetmap.josm.command.DeleteCommand; import org.openstreetmap.josm.data.Preferences; import org.openstreetmap.josm.data.Version; import org.openstreetmap.josm.data.coor.LatLon; import org.openstreetmap.josm.data.osm.Node; import org.openstreetmap.josm.data.osm.OsmPrimitive; import org.openstreetmap.josm.data.osm.Tag; import org.openstreetmap.josm.data.osm.Way; import org.openstreetmap.josm.data.osm.visitor.paint.MapPaintSettings; import org.openstreetmap.josm.data.osm.visitor.paint.StyledMapRenderer; import org.openstreetmap.josm.data.preferences.JosmBaseDirectories; import org.openstreetmap.josm.data.preferences.JosmUrls; import org.openstreetmap.josm.data.preferences.sources.ExtendedSourceEntry; import org.openstreetmap.josm.data.preferences.sources.SourceEntry; import org.openstreetmap.josm.data.projection.ProjectionRegistry; import org.openstreetmap.josm.data.projection.Projections; import org.openstreetmap.josm.gui.NavigatableComponent; import org.openstreetmap.josm.gui.mappaint.Cascade; import org.openstreetmap.josm.gui.mappaint.Environment; import org.openstreetmap.josm.gui.mappaint.MapPaintStyles; import org.openstreetmap.josm.gui.mappaint.MultiCascade; import org.openstreetmap.josm.gui.mappaint.mapcss.ConditionFactory; import org.openstreetmap.josm.gui.mappaint.mapcss.MapCSSRule; import org.openstreetmap.josm.gui.mappaint.mapcss.MapCSSStyleSource; import org.openstreetmap.josm.gui.mappaint.mapcss.parsergen.MapCSSParser; import org.openstreetmap.josm.gui.mappaint.mapcss.parsergen.ParseException; import org.openstreetmap.josm.gui.mappaint.styleelement.AreaElement; import org.openstreetmap.josm.gui.mappaint.styleelement.LineElement; import org.openstreetmap.josm.gui.mappaint.styleelement.StyleElement; import org.openstreetmap.josm.gui.preferences.map.TaggingPresetPreference; import org.openstreetmap.josm.gui.tagging.presets.TaggingPreset; import org.openstreetmap.josm.gui.tagging.presets.TaggingPresetReader; import org.openstreetmap.josm.gui.tagging.presets.TaggingPresetType; import org.openstreetmap.josm.gui.tagging.presets.items.CheckGroup; import org.openstreetmap.josm.gui.tagging.presets.items.KeyedItem; import org.openstreetmap.josm.io.CachedFile; import org.openstreetmap.josm.io.OsmTransferException; import org.openstreetmap.josm.spi.preferences.Config; import org.openstreetmap.josm.tools.Http1Client; import org.openstreetmap.josm.tools.HttpClient; import org.openstreetmap.josm.tools.Logging; import org.openstreetmap.josm.tools.OptionParser; import org.openstreetmap.josm.tools.RightAndLefthandTraffic; import org.openstreetmap.josm.tools.Territories; import org.openstreetmap.josm.tools.Utils; import org.xml.sax.SAXException; /** * Extracts tag information for the taginfo project. *

* Run from the base directory of a JOSM checkout: *

* java -cp dist/josm-custom.jar TagInfoExtract --type mappaint * java -cp dist/josm-custom.jar TagInfoExtract --type presets * java -cp dist/josm-custom.jar TagInfoExtract --type external_presets */ public class TagInfoExtract { /** * Main method. * @param args Main program arguments * @throws Exception if any error occurs */ public static void main(String[] args) throws Exception { HttpClient.setFactory(Http1Client::new); TagInfoExtract script = new TagInfoExtract(); script.parseCommandLineArguments(args); script.init(); switch (script.options.mode) { case MAPPAINT: script.new StyleSheet().run(); break; case PRESETS: script.new Presets().run(); break; case EXTERNAL_PRESETS: script.new ExternalPresets().run(); break; default: throw new IllegalStateException("Invalid type " + script.options.mode); } if (!script.options.noexit) { System.exit(0); } } enum Mode { MAPPAINT, PRESETS, EXTERNAL_PRESETS } private final Options options = new Options(); /** * Parse command line arguments. * @param args command line arguments */ private void parseCommandLineArguments(String[] args) { if (args.length == 1 && "--help".equals(args[0])) { this.usage(); } final OptionParser parser = new OptionParser(getClass().getName()); parser.addArgumentParameter("type", OptionParser.OptionCount.REQUIRED, options::setMode); parser.addArgumentParameter("input", OptionParser.OptionCount.OPTIONAL, options::setInputFile); parser.addArgumentParameter("output", OptionParser.OptionCount.OPTIONAL, options::setOutputFile); parser.addArgumentParameter("imgdir", OptionParser.OptionCount.OPTIONAL, options::setImageDir); parser.addArgumentParameter("imgurlprefix", OptionParser.OptionCount.OPTIONAL, options::setImageUrlPrefix); parser.addFlagParameter("noexit", options::setNoExit); parser.addFlagParameter("help", this::usage); parser.parseOptionsOrExit(Arrays.asList(args)); } private void usage() { System.out.println("java " + getClass().getName()); System.out.println(" --type TYPE\tthe project type to be generated: " + Arrays.toString(Mode.values())); System.out.println(" --input FILE\tthe input file to use (overrides defaults for types mappaint, presets)"); System.out.println(" --output FILE\tthe output file to use (defaults to STDOUT)"); System.out.println(" --imgdir DIRECTORY\tthe directory to put the generated images in (default: " + options.imageDir + ")"); System.out.println(" --imgurlprefix STRING\timage URLs prefix for generated image files (public path on webserver)"); System.out.println(" --noexit\tdo not call System.exit(), for use from Ant script"); System.out.println(" --help\tshow this help"); System.exit(0); } private static class Options { Mode mode; int josmSvnRevision = Version.getInstance().getVersion(); Path baseDir = Paths.get(""); Path imageDir = Paths.get("taginfo-img"); String imageUrlPrefix; CachedFile inputFile; Path outputFile; boolean noexit; void setMode(String value) { mode = Mode.valueOf(value.toUpperCase(Locale.ENGLISH)); switch (mode) { case MAPPAINT: inputFile = new CachedFile("resource://styles/standard/elemstyles.mapcss"); break; case PRESETS: inputFile = new CachedFile("resource://data/defaultpresets.xml"); break; default: inputFile = null; } } void setInputFile(String value) { inputFile = new CachedFile(value); } void setOutputFile(String value) { outputFile = Paths.get(value); } void setImageDir(String value) { imageDir = Paths.get(value); } void setImageUrlPrefix(String value) { imageUrlPrefix = value; } void setNoExit() { noexit = true; } /** * Determine full image url (can refer to JOSM or OSM repository). * @param path the image path * @return full image url */ private String findImageUrl(String path) { final Path f = baseDir.resolve("resources").resolve("images").resolve(path); if (Files.exists(f)) { return "https://josm.openstreetmap.de/export/" + josmSvnRevision + "/josm/trunk/resources/images/" + path; } throw new IllegalStateException("Cannot find image url for " + path); } } private abstract class Extractor { abstract void run() throws Exception; void writeJson(String name, String description, Iterable tags) throws IOException { try (Writer writer = options.outputFile != null ? Files.newBufferedWriter(options.outputFile) : new StringWriter(); JsonWriter json = Json .createWriterFactory(Collections.singletonMap(JsonGenerator.PRETTY_PRINTING, true)) .createWriter(writer)) { JsonObjectBuilder project = Json.createObjectBuilder() .add("name", name) .add("description", description) .add("project_url", JosmUrls.getInstance().getJOSMWebsite()) .add("icon_url", options.findImageUrl("logo_16x16x8.png")) .add("contact_name", "JOSM developer team") .add("contact_email", "josm-dev@openstreetmap.org"); final JsonArrayBuilder jsonTags = Json.createArrayBuilder(); for (TagInfoTag t : tags) { jsonTags.add(t.toJson()); } json.writeObject(Json.createObjectBuilder() .add("data_format", 1) .add("data_updated", DateTimeFormatter.ofPattern("yyyyMMdd'T'hhmmss'Z'").withZone(ZoneId.of("Z")).format(Instant.now())) .add("project", project) .add("tags", jsonTags) .build()); if (options.outputFile == null) { System.out.println(writer.toString()); } } } } private class Presets extends Extractor { @Override void run() throws IOException, OsmTransferException, SAXException { try (BufferedReader reader = options.inputFile.getContentReader()) { Collection presets = TaggingPresetReader.readAll(reader, true); List tags = convertPresets(presets, "", true); Logging.info("Converting {0} internal presets", tags.size()); writeJson("JOSM main presets", "Tags supported by the default presets in the OSM editor JOSM", tags); } } List convertPresets(Iterable presets, String descriptionPrefix, boolean addImages) { final List tags = new ArrayList<>(); final Map optionalTags = new LinkedHashMap<>(); for (TaggingPreset preset : presets) { preset.data.stream() .flatMap(item -> item instanceof KeyedItem ? Stream.of(((KeyedItem) item)) : item instanceof CheckGroup ? ((CheckGroup) item).checks.stream() : Stream.empty()) .forEach(item -> { for (String value : item.getValues()) { Set types = TagInfoTag.Type.forPresetTypes(preset.types); if (item.isKeyRequired()) { tags.add(new TagInfoTag(descriptionPrefix + preset.getName(), item.key, value, types, addImages && preset.iconName != null ? options.findImageUrl(preset.iconName) : null)); } else { optionalTags.compute(new Tag(item.key, value), (osmTag, tagInfoTag) -> { if (tagInfoTag == null) { String description = descriptionPrefix + "Optional for: " + preset.getName(); return new TagInfoTag(description, item.key, value, types, null); } else { tagInfoTag.descriptions.add(preset.getName()); tagInfoTag.objectTypes.addAll(types); return tagInfoTag; } }); } } }); } tags.addAll(optionalTags.values()); return tags; } } private class ExternalPresets extends Presets { @Override void run() throws IOException, OsmTransferException, SAXException { TaggingPresetReader.setLoadIcons(false); final Collection sources = new TaggingPresetPreference.TaggingPresetSourceEditor().loadAndGetAvailableSources(); final List tags = new ArrayList<>(); for (SourceEntry source : sources) { if (source.url.startsWith("resource")) { // default presets continue; } try { Logging.info("Loading {0}", source.url); Collection presets = TaggingPresetReader.readAll(source.url, false); final List t = convertPresets(presets, source.title + " ", false); Logging.info("Converting {0} presets of {1}", t.size(), source.title); tags.addAll(t); } catch (Exception ex) { Logging.warn("Skipping {0} due to error", source.url); Logging.warn(ex); } } writeJson("JOSM user presets", "Tags supported by the user contributed presets in the OSM editor JOSM", tags); } } private class StyleSheet extends Extractor { private MapCSSStyleSource styleSource; @Override void run() throws IOException, ParseException { init(); parseStyleSheet(); final List tags = convertStyleSheet(); writeJson("JOSM main mappaint style", "Tags supported by the main mappaint style in the OSM editor JOSM", tags); } /** * Read the style sheet file and parse the MapCSS code. * @throws IOException if any I/O error occurs * @throws ParseException in case of parsing error */ private void parseStyleSheet() throws IOException, ParseException { try (BufferedReader reader = options.inputFile.getContentReader()) { MapCSSParser parser = new MapCSSParser(reader, MapCSSParser.LexicalState.DEFAULT); styleSource = new MapCSSStyleSource(""); styleSource.url = ""; parser.sheet(styleSource); } } /** * Collect all the tag from the style sheet. * @return list of taginfo tags */ private List convertStyleSheet() { return styleSource.rules.stream() .flatMap(rule -> rule.selectors.stream()) .flatMap(selector -> selector.getConditions().stream()) .filter(ConditionFactory.SimpleKeyValueCondition.class::isInstance) .map(ConditionFactory.SimpleKeyValueCondition.class::cast) .map(condition -> condition.asTag(null)) .distinct() .map(tag -> { String iconUrl = null; final EnumSet types = EnumSet.noneOf(TagInfoTag.Type.class); Optional nodeUrl = new NodeChecker(tag).findUrl(true); if (nodeUrl.isPresent()) { iconUrl = nodeUrl.get(); types.add(TagInfoTag.Type.NODE); } Optional wayUrl = new WayChecker(tag).findUrl(iconUrl == null); if (wayUrl.isPresent()) { if (iconUrl == null) { iconUrl = wayUrl.get(); } types.add(TagInfoTag.Type.WAY); } Optional areaUrl = new AreaChecker(tag).findUrl(iconUrl == null); if (areaUrl.isPresent()) { if (iconUrl == null) { iconUrl = areaUrl.get(); } types.add(TagInfoTag.Type.AREA); } return new TagInfoTag(null, tag.getKey(), tag.getValue(), types, iconUrl); }) .collect(Collectors.toList()); } /** * Check if a certain tag is supported by the style as node / way / area. */ private abstract class Checker { private final Pattern reservedChars = Pattern.compile("[<>:\"|\\?\\*]"); Checker(Tag tag) { this.tag = tag; } Environment applyStylesheet(OsmPrimitive osm) { osm.put(tag); MultiCascade mc = new MultiCascade(); Environment env = new Environment(osm, mc, null, styleSource); for (MapCSSRule r : styleSource.rules) { env.clearSelectorMatchingInformation(); if (r.matches(env)) { // ignore selector range if (env.layer == null) { env.layer = "default"; } r.execute(env); } } env.layer = "default"; return env; } /** * Create image file from StyleElement. * @param element style element * @param type object type * @param nc navigatable component * * @return the URL */ String createImage(StyleElement element, final String type, NavigatableComponent nc) { BufferedImage img = new BufferedImage(16, 16, BufferedImage.TYPE_INT_ARGB); Graphics2D g = img.createGraphics(); g.setClip(0, 0, 16, 16); StyledMapRenderer renderer = new StyledMapRenderer(g, nc, false); renderer.getSettings(false); element.paintPrimitive(osm, MapPaintSettings.INSTANCE, renderer, false, false, false); final String imageName = type + "_" + normalize(tag.toString()) + ".png"; try (OutputStream out = Files.newOutputStream(options.imageDir.resolve(imageName))) { ImageIO.write(img, "png", out); } catch (IOException e) { throw new UncheckedIOException(e); } final String baseUrl = options.imageUrlPrefix != null ? options.imageUrlPrefix : options.imageDir.toString(); return baseUrl + "/" + imageName; } /** * Normalizes tag so that it can used as a filename on all platforms, including Windows. * @param tag OSM tag, that can contain illegal path characters * @return OSM tag with all illegal path characters replaced by underscore ('_') */ String normalize(String tag) { Matcher m = reservedChars.matcher(tag); return m.find() ? m.replaceAll("_") : tag; } /** * Checks, if tag is supported and find URL for image icon in this case. * * @param generateImage if true, create or find a suitable image icon and return URL, * if false, just check if tag is supported and return true or false * @return URL for image icon if tag is supported */ abstract Optional findUrl(boolean generateImage); protected Tag tag; protected OsmPrimitive osm; } private class NodeChecker extends Checker { NodeChecker(Tag tag) { super(tag); } @Override Optional findUrl(boolean generateImage) { this.osm = new Node(LatLon.ZERO); Environment env = applyStylesheet(osm); Cascade c = env.mc.getCascade("default"); Object image = c.get("icon-image"); if (image instanceof MapPaintStyles.IconReference && !((MapPaintStyles.IconReference) image).isDeprecatedIcon()) { return Optional.of(options.findImageUrl(((MapPaintStyles.IconReference) image).iconName)); } return Optional.empty(); } } private class WayChecker extends Checker { WayChecker(Tag tag) { super(tag); } @Override Optional findUrl(boolean generateImage) { this.osm = new Way(); NavigatableComponent nc = new NavigatableComponent(); Node n1 = new Node(nc.getLatLon(2, 8)); Node n2 = new Node(nc.getLatLon(14, 8)); ((Way) osm).addNode(n1); ((Way) osm).addNode(n2); Environment env = applyStylesheet(osm); LineElement les = LineElement.createLine(env); if (les != null) { if (!generateImage) return Optional.of(""); return Optional.of(createImage(les, "way", nc)); } return Optional.empty(); } } private class AreaChecker extends Checker { AreaChecker(Tag tag) { super(tag); } @Override Optional findUrl(boolean generateImage) { this.osm = new Way(); NavigatableComponent nc = new NavigatableComponent(); Node n1 = new Node(nc.getLatLon(2, 2)); Node n2 = new Node(nc.getLatLon(14, 2)); Node n3 = new Node(nc.getLatLon(14, 14)); Node n4 = new Node(nc.getLatLon(2, 14)); ((Way) osm).addNode(n1); ((Way) osm).addNode(n2); ((Way) osm).addNode(n3); ((Way) osm).addNode(n4); ((Way) osm).addNode(n1); Environment env = applyStylesheet(osm); AreaElement aes = AreaElement.create(env); if (aes != null) { if (!generateImage) return Optional.of(""); return Optional.of(createImage(aes, "area", nc)); } return Optional.empty(); } } } /** * POJO representing a Taginfo tag. */ private static class TagInfoTag { final Collection descriptions = new ArrayList<>(); final String key; final String value; final Set objectTypes; final String iconURL; TagInfoTag(String description, String key, String value, Set objectTypes, String iconURL) { if (description != null) { this.descriptions.add(description); } this.key = key; this.value = value; this.objectTypes = objectTypes; this.iconURL = iconURL; } JsonObjectBuilder toJson() { final JsonObjectBuilder object = Json.createObjectBuilder(); if (!descriptions.isEmpty()) { object.add("description", String.join(", ", Utils.limit(descriptions, 8, "..."))); } object.add("key", key); object.add("value", value); if ((!objectTypes.isEmpty())) { final JsonArrayBuilder types = Json.createArrayBuilder(); objectTypes.stream().map(Enum::name).map(String::toLowerCase).forEach(types::add); object.add("object_types", types); } if (iconURL != null) { object.add("icon_url", iconURL); } return object; } enum Type { NODE, WAY, AREA, RELATION; static TagInfoTag.Type forPresetType(TaggingPresetType type) { switch (type) { case CLOSEDWAY: return AREA; case MULTIPOLYGON: return RELATION; default: return valueOf(type.toString()); } } static Set forPresetTypes(Set types) { if (types == null) { return Collections.emptySet(); } return types.stream() .map(Type::forPresetType) .collect(Collectors.toCollection(() -> EnumSet.noneOf(Type.class))); } } } /** * Initialize the script. * @throws IOException if any I/O error occurs */ private void init() throws IOException { Logging.setLogLevel(Logging.LEVEL_INFO); Preferences.main().enableSaveOnPut(false); Config.setPreferencesInstance(Preferences.main()); Config.setBaseDirectoriesProvider(JosmBaseDirectories.getInstance()); Config.setUrlsProvider(JosmUrls.getInstance()); ProjectionRegistry.setProjection(Projections.getProjectionByCode("EPSG:3857")); Path tmpdir = Files.createTempDirectory(options.baseDir, "pref"); tmpdir.toFile().deleteOnExit(); System.setProperty("josm.home", tmpdir.toString()); DeleteCommand.setDeletionCallback(DeleteAction.defaultDeletionCallback); Territories.initializeInternalData(); Files.createDirectories(options.imageDir); } }