Ticket #17170: 17170.patch

File 17170.patch, 45.1 KB (added by simon04, 13 days ago)
  • deleted file scripts/TagInfoExtract.groovy

    commit c5e399a1b2f55ef2cd43ce5e04d8cc90d3971b02
    Author: Simon Legner <Simon.Legner@gmail.com>
    Date:   Fri Jan 4 00:20:34 2019 +0100
    
        fix #17170 - Migrate TagInfoExtract.groovy to Java
    
    diff --git a/scripts/TagInfoExtract.groovy b/scripts/TagInfoExtract.groovy
    deleted file mode 100644
    index 4f3f565cb..000000000
    + -  
    1 // License: GPL. For details, see LICENSE file.
    2 /**
    3  * Extracts tag information for the taginfo project.
    4  *
    5  * Run from the base directory of a JOSM checkout:
    6  *
    7  * groovy -cp dist/josm-custom.jar scripts/taginfoextract.groovy -t mappaint
    8  * groovy -cp dist/josm-custom.jar scripts/taginfoextract.groovy -t presets
    9  * groovy -cp dist/josm-custom.jar scripts/taginfoextract.groovy -t external_presets
    10  */
    11 import java.awt.image.BufferedImage
    12 import java.nio.file.FileSystems
    13 import java.nio.file.Files
    14 import java.nio.file.Path
    15 import java.time.Instant
    16 import java.time.ZoneId
    17 import java.time.format.DateTimeFormatter
    18 
    19 import javax.imageio.ImageIO
    20 import javax.json.Json
    21 import javax.json.stream.JsonGenerator
    22 
    23 import org.openstreetmap.josm.actions.DeleteAction
    24 import org.openstreetmap.josm.command.DeleteCommand
    25 import org.openstreetmap.josm.data.Preferences
    26 import org.openstreetmap.josm.data.Version
    27 import org.openstreetmap.josm.data.coor.LatLon
    28 import org.openstreetmap.josm.data.osm.Node
    29 import org.openstreetmap.josm.data.osm.OsmPrimitive
    30 import org.openstreetmap.josm.data.osm.Way
    31 import org.openstreetmap.josm.data.osm.visitor.paint.MapPaintSettings
    32 import org.openstreetmap.josm.data.osm.visitor.paint.StyledMapRenderer
    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.gui.NavigatableComponent
    38 import org.openstreetmap.josm.gui.mappaint.Environment
    39 import org.openstreetmap.josm.gui.mappaint.MultiCascade
    40 import org.openstreetmap.josm.gui.mappaint.MapPaintStyles.IconReference
    41 import org.openstreetmap.josm.gui.mappaint.mapcss.MapCSSStyleSource
    42 import org.openstreetmap.josm.gui.mappaint.mapcss.ConditionFactory.SimpleKeyValueCondition
    43 import org.openstreetmap.josm.gui.mappaint.mapcss.Selector.GeneralSelector
    44 import org.openstreetmap.josm.gui.mappaint.mapcss.parsergen.MapCSSParser
    45 import org.openstreetmap.josm.gui.mappaint.styleelement.AreaElement
    46 import org.openstreetmap.josm.gui.mappaint.styleelement.LineElement
    47 import org.openstreetmap.josm.gui.mappaint.styleelement.StyleElement
    48 import org.openstreetmap.josm.gui.preferences.map.TaggingPresetPreference
    49 import org.openstreetmap.josm.gui.tagging.presets.TaggingPreset
    50 import org.openstreetmap.josm.gui.tagging.presets.TaggingPresetReader
    51 import org.openstreetmap.josm.gui.tagging.presets.TaggingPresetType
    52 import org.openstreetmap.josm.gui.tagging.presets.items.KeyedItem
    53 import org.openstreetmap.josm.gui.tagging.presets.items.KeyedItem.MatchType
    54 import org.openstreetmap.josm.io.CachedFile
    55 import org.openstreetmap.josm.spi.preferences.Config
    56 import org.openstreetmap.josm.tools.Logging
    57 import org.openstreetmap.josm.tools.RightAndLefthandTraffic
    58 import org.openstreetmap.josm.tools.Territories
    59 import org.openstreetmap.josm.tools.Utils
    60 
    61 import edu.umd.cs.findbugs.annotations.SuppressFBWarnings
    62 
    63 class TagInfoExtract {
    64 
    65     static def options
    66     static String image_dir
    67     int josm_svn_revision
    68     String input_file
    69     MapCSSStyleSource style_source
    70     FileWriter output_file
    71     String base_dir = "."
    72     Set tags = []
    73 
    74     private def cached_svnrev
    75 
    76     /**
    77      * Check if a certain tag is supported by the style as node / way / area.
    78      */
    79     abstract class Checker {
    80 
    81         def tag
    82         OsmPrimitive osm
    83 
    84         Checker(tag) {
    85             this.tag = tag
    86         }
    87 
    88         Environment apply_stylesheet(OsmPrimitive osm) {
    89             osm.put(tag[0], tag[1])
    90             MultiCascade mc = new MultiCascade()
    91 
    92             Environment env = new Environment(osm, mc, null, style_source)
    93             for (def r in style_source.rules) {
    94                 env.clearSelectorMatchingInformation()
    95                 if (r.selector.matches(env)) {
    96                     // ignore selector range
    97                     if (env.layer == null) {
    98                         env.layer = "default"
    99                     }
    100                     r.execute(env)
    101                 }
    102             }
    103             env.layer = "default"
    104             return env
    105         }
    106 
    107         /**
    108          * Create image file from StyleElement.
    109          * @return the URL
    110          */
    111         def create_image(StyleElement elem_style, type, nc) {
    112             def img = new BufferedImage(16, 16, BufferedImage.TYPE_INT_ARGB)
    113             def g = img.createGraphics()
    114             g.setClip(0, 0, 16, 16)
    115             def renderer = new StyledMapRenderer(g, nc, false)
    116             renderer.getSettings(false)
    117             elem_style.paintPrimitive(osm, MapPaintSettings.INSTANCE, renderer, false, false, false)
    118             def base_url = options.imgurlprefix ? options.imgurlprefix : image_dir
    119             def image_name = "${type}_${tag[0]}=${tag[1]}.png"
    120             ImageIO.write(img, "png", new File("${image_dir}/${image_name}"))
    121             return "${base_url}/${image_name}"
    122         }
    123 
    124         /**
    125          * Checks, if tag is supported and find URL for image icon in this case.
    126          * @param generate_image if true, create or find a suitable image icon and return URL,
    127          * if false, just check if tag is supported and return true or false
    128          */
    129         abstract def find_url(boolean generate_image)
    130     }
    131 
    132     @SuppressFBWarnings(value = "MF_CLASS_MASKS_FIELD")
    133     class NodeChecker extends Checker {
    134         NodeChecker(tag) {
    135             super(tag)
    136         }
    137 
    138         @Override
    139         def find_url(boolean generate_image) {
    140             osm = new Node(LatLon.ZERO)
    141             def env = apply_stylesheet(osm)
    142             def c = env.mc.getCascade("default")
    143             def image = c.get("icon-image")
    144             if (image) {
    145                 if (image instanceof IconReference && !image.isDeprecatedIcon()) {
    146                     return find_image_url(image.iconName)
    147                 }
    148             }
    149         }
    150     }
    151 
    152     @SuppressFBWarnings(value = "MF_CLASS_MASKS_FIELD")
    153     class WayChecker extends Checker {
    154         WayChecker(tag) {
    155             super(tag)
    156         }
    157 
    158         @Override
    159         def find_url(boolean generate_image) {
    160             osm = new Way()
    161             def nc = new NavigatableComponent()
    162             def n1 = new Node(nc.getLatLon(2,8))
    163             def n2 = new Node(nc.getLatLon(14,8))
    164             ((Way)osm).addNode(n1)
    165             ((Way)osm).addNode(n2)
    166             def env = apply_stylesheet(osm)
    167             def les = LineElement.createLine(env)
    168             if (les != null) {
    169                 if (!generate_image) return true
    170                 return create_image(les, 'way', nc)
    171             }
    172         }
    173     }
    174 
    175     @SuppressFBWarnings(value = "MF_CLASS_MASKS_FIELD")
    176     class AreaChecker extends Checker {
    177         AreaChecker(tag) {
    178             super(tag)
    179         }
    180 
    181         @Override
    182         def find_url(boolean generate_image) {
    183             osm = new Way()
    184             def nc = new NavigatableComponent()
    185             def n1 = new Node(nc.getLatLon(2,2))
    186             def n2 = new Node(nc.getLatLon(14,2))
    187             def n3 = new Node(nc.getLatLon(14,14))
    188             def n4 = new Node(nc.getLatLon(2,14))
    189             ((Way)osm).addNode(n1)
    190             ((Way)osm).addNode(n2)
    191             ((Way)osm).addNode(n3)
    192             ((Way)osm).addNode(n4)
    193             ((Way)osm).addNode(n1)
    194             def env = apply_stylesheet(osm)
    195             def aes = AreaElement.create(env)
    196             if (aes != null) {
    197                 if (!generate_image) return true
    198                 return create_image(aes, 'area', nc)
    199             }
    200         }
    201     }
    202 
    203     /**
    204      * Main method.
    205      */
    206     static main(def args) {
    207         parse_command_line_arguments(args)
    208         def script = new TagInfoExtract()
    209         if (!options.t || options.t == 'mappaint') {
    210             script.run()
    211         } else if (options.t == 'presets') {
    212             script.run_presets()
    213         } else if (options.t == 'external_presets') {
    214             script.run_external_presets()
    215         } else {
    216             System.err.println 'Invalid type ' + options.t
    217             if (!options.noexit) {
    218                 System.exit(1)
    219             }
    220         }
    221 
    222         if (!options.noexit) {
    223             System.exit(0)
    224         }
    225     }
    226 
    227     /**
    228      * Parse command line arguments.
    229      */
    230     static void parse_command_line_arguments(args) {
    231         def cli = new CliBuilder(usage:'taginfoextract.groovy [options] [inputfile]',
    232             header:"Options:",
    233             footer:"[inputfile]           the file to process (optional, default is 'resource://styles/standard/elemstyles.mapcss')")
    234         cli.o(args:1, argName: "file", "output file (json), - prints to stdout (default: -)")
    235         cli.t(args:1, argName: "type", "the project type to be generated")
    236         cli._(longOpt:'svnrev', args:1, argName:"revision", "corresponding revision of the repository https://svn.openstreetmap.org/ (optional, current revision is read from the local checkout or from the web if not given, see --svnweb)")
    237         cli._(longOpt:'imgdir', args:1, argName:"directory", "directory to put the generated images in (default: ./taginfo-img)")
    238         cli._(longOpt:'noexit', "don't call System.exit(), for use from Ant script")
    239         cli._(longOpt:'svnweb', 'fetch revision of the repository https://svn.openstreetmap.org/ from web and not from the local repository')
    240         cli._(longOpt:'imgurlprefix', args:1, argName:'prefix', 'image URLs prefix for generated image files')
    241         cli.h(longOpt:'help', "show this help")
    242         options = cli.parse(args)
    243 
    244         if (options.h) {
    245             cli.usage()
    246             System.exit(0)
    247         }
    248         if (options.arguments().size() > 1) {
    249             System.err.println "Error: More than one input file given!"
    250             cli.usage()
    251             System.exit(-1)
    252         }
    253         if (options.svnrev) {
    254             assert Integer.parseInt(options.svnrev) > 0
    255         }
    256         image_dir = 'taginfo-img'
    257         if (options.imgdir) {
    258             image_dir = options.imgdir
    259         }
    260         def image_dir_file = new File(image_dir)
    261         if (!image_dir_file.exists()) {
    262             image_dir_file.mkdirs()
    263         }
    264     }
    265 
    266     void run_presets() {
    267         init()
    268         def presets = TaggingPresetReader.readAll(input_file, true)
    269         def tags = convert_presets(presets, "", true)
    270         write_json("JOSM main presets", "Tags supported by the default presets in the OSM editor JOSM", tags)
    271     }
    272 
    273     def convert_presets(Iterable<TaggingPreset> presets, String descriptionPrefix, boolean addImages) {
    274         def tags = []
    275         for (TaggingPreset preset : presets) {
    276             for (KeyedItem item : Utils.filteredCollection(preset.data, KeyedItem.class)) {
    277                 def values
    278                 switch (MatchType.ofString(item.match)) {
    279                     case MatchType.KEY_REQUIRED: values = item.getValues(); break;
    280                     case MatchType.KEY_VALUE_REQUIRED: values = item.getValues(); break;
    281                     default: values = [];
    282                 }
    283                 for (String value : values) {
    284                     def tag = [
    285                             description: descriptionPrefix + preset.name,
    286                             key: item.key,
    287                             value: value,
    288                     ]
    289                     def otypes = preset.types.collect {
    290                         it == TaggingPresetType.CLOSEDWAY ? "area" :
    291                             (it == TaggingPresetType.MULTIPOLYGON ? "relation" : it.toString().toLowerCase(Locale.ENGLISH))
    292                     }
    293                     if (!otypes.isEmpty()) tag += [object_types: otypes]
    294                     if (addImages && preset.iconName) tag += [icon_url: find_image_url(preset.iconName)]
    295                     tags += tag
    296                 }
    297             }
    298         }
    299         return tags
    300     }
    301 
    302     void run_external_presets() {
    303         init()
    304         TaggingPresetReader.setLoadIcons(false)
    305         def sources = new TaggingPresetPreference.TaggingPresetSourceEditor().loadAndGetAvailableSources()
    306         def tags = []
    307         for (def source : sources) {
    308             if (source.url.startsWith("resource")) {
    309                 // default presets
    310                 continue;
    311             }
    312             try {
    313                 println "Loading ${source.url}"
    314                 def presets = TaggingPresetReader.readAll(source.url, false)
    315                 def t = convert_presets(presets, source.title + " ", false)
    316                 println "Converting ${t.size()} presets of ${source.title}"
    317                 tags += t
    318             } catch (Exception ex) {
    319                 System.err.println("Skipping ${source.url} due to error")
    320                 ex.printStackTrace()
    321             }
    322         }
    323         write_json("JOSM user presets", "Tags supported by the user contributed presets in the OSM editor JOSM", tags)
    324     }
    325 
    326     void run() {
    327         init()
    328         parse_style_sheet()
    329         collect_tags()
    330 
    331         def tags = tags.collect {
    332             def tag = it
    333             def types = []
    334             def final_url = null
    335 
    336             def node_url = new NodeChecker(tag).find_url(true)
    337             if (node_url) {
    338                 types += 'node'
    339                 final_url = node_url
    340             }
    341             def way_url = new WayChecker(tag).find_url(final_url == null)
    342             if (way_url) {
    343                 types += 'way'
    344                 if (!final_url) {
    345                     final_url = way_url
    346                 }
    347             }
    348             def area_url = new AreaChecker(tag).find_url(final_url == null)
    349             if (area_url) {
    350                 types += 'area'
    351                 if (!final_url) {
    352                     final_url = area_url
    353                 }
    354             }
    355 
    356             def obj = [key: tag[0], value: tag[1]]
    357             if (types) obj += [object_types: types]
    358             if (final_url) obj += [icon_url: final_url]
    359             obj
    360         }
    361 
    362         write_json("JOSM main mappaint style", "Tags supported by the main mappaint style in the OSM editor JOSM", tags)
    363     }
    364 
    365     void write_json(String name, String description, List<Map<String, ?>> tags) {
    366         def config = [:]
    367         config[JsonGenerator.PRETTY_PRINTING] = output_file == null
    368         def writer = output_file != null ? output_file : new StringWriter()
    369         def json = Json.createWriterFactory(config).createWriter(writer)
    370         try {
    371             def project = Json.createObjectBuilder()
    372                 .add("name", name)
    373                 .add("description", description)
    374                 .add("project_url", "https://josm.openstreetmap.de/")
    375                 .add("icon_url", "https://josm.openstreetmap.de/export/7770/josm/trunk/images/logo_16x16x8.png")
    376                 .add("contact_name", "JOSM developer team")
    377                 .add("contact_email", "josm-dev@openstreetmap.org")
    378             def jsonTags = Json.createArrayBuilder()
    379             for (def t : tags) {
    380                 def o = Json.createObjectBuilder()
    381                 for (def e : t.entrySet()) {
    382                     def val = e.getValue()
    383                     if (e.getValue() instanceof List) {
    384                         def arr = Json.createArrayBuilder()
    385                         for (def v : e.getValue()) {
    386                             arr.add(v)
    387                         }
    388                         val = arr.build()
    389                     }
    390                     o.add(e.getKey(), val)
    391                 }
    392                 jsonTags.add(o.build())
    393             }
    394             json.writeObject(Json.createObjectBuilder()
    395                 .add("data_format", 1)
    396                 .add("data_updated", DateTimeFormatter.ofPattern("yyyyMMdd'T'hhmmss'Z'").withZone(ZoneId.of("Z")).format(Instant.now()))
    397                 .add("project", project.build())
    398                 .add("tags", jsonTags.build())
    399                 .build())
    400         } finally {
    401             json.close()
    402         }
    403 
    404         if (output_file != null) {
    405             output_file.close()
    406         } else {
    407             print writer.toString()
    408         }
    409     }
    410 
    411     /**
    412      * Initialize the script.
    413      */
    414     def init() {
    415         Logging.setLogLevel(Logging.LEVEL_INFO)
    416         Preferences.main().enableSaveOnPut(false)
    417         Config.setPreferencesInstance(Preferences.main())
    418         Config.setBaseDirectoriesProvider(JosmBaseDirectories.getInstance())
    419         Config.setUrlsProvider(JosmUrls.getInstance())
    420         ProjectionRegistry.setProjection(Projections.getProjectionByCode("EPSG:3857"))
    421         Path tmpdir = Files.createTempDirectory(FileSystems.getDefault().getPath(base_dir), "pref")
    422         tmpdir.toFile().deleteOnExit()
    423         System.setProperty("josm.home", tmpdir.toString())
    424         DeleteCommand.setDeletionCallback(DeleteAction.defaultDeletionCallback)
    425         Territories.initialize()
    426         RightAndLefthandTraffic.initialize()
    427 
    428         josm_svn_revision = Version.getInstance().getVersion()
    429         assert josm_svn_revision != Version.JOSM_UNKNOWN_VERSION
    430 
    431         if (options.arguments().size() == 0 && (!options.t || options.t == 'mappaint')) {
    432             input_file = "resource://styles/standard/elemstyles.mapcss"
    433         } else if (options.arguments().size() == 0 && options.t == 'presets') {
    434             input_file = "resource://data/defaultpresets.xml"
    435         } else {
    436             input_file = options.arguments()[0]
    437         }
    438 
    439         output_file = null
    440         if (options.o && options.o != "-") {
    441             output_file = new FileWriter(options.o)
    442         }
    443     }
    444 
    445     /**
    446      * Determine full image url (can refer to JOSM or OSM repository).
    447      */
    448     def find_image_url(String path) {
    449         def f = new File("${base_dir}/images/styles/standard/${path}")
    450         if (f.exists()) {
    451             def rev = osm_svn_revision()
    452             return "https://trac.openstreetmap.org/export/${rev}/subversion/applications/share/map-icons/classic.small/${path}"
    453         }
    454         f = new File("${base_dir}/images/${path}")
    455         if (f.exists()) {
    456             if (path.startsWith("images/styles/standard/")) {
    457                 path = path.substring("images/styles/standard/".length())
    458                 def rev = osm_svn_revision()
    459                 return "https://trac.openstreetmap.org/export/${rev}/subversion/applications/share/map-icons/classic.small/${path}"
    460             } else if (path.startsWith("styles/standard/")) {
    461                 path = path.substring("styles/standard/".length())
    462                 def rev = osm_svn_revision()
    463                 return "https://trac.openstreetmap.org/export/${rev}/subversion/applications/share/map-icons/classic.small/${path}"
    464             } else {
    465                 return "https://josm.openstreetmap.de/export/${josm_svn_revision}/josm/trunk/images/${path}"
    466             }
    467         }
    468         assert false, "Cannot find image url for ${path}"
    469     }
    470 
    471     /**
    472      * Get revision for the repository https://svn.openstreetmap.org.
    473      */
    474     def osm_svn_revision() {
    475         if (cached_svnrev != null) return cached_svnrev
    476         if (options.svnrev) {
    477             cached_svnrev = Integer.parseInt(options.svnrev)
    478             return cached_svnrev
    479         }
    480         def xml
    481         if (options.svnweb) {
    482             xml = "svn info --xml https://svn.openstreetmap.org/applications/share/map-icons/classic.small".execute().text
    483         } else {
    484             xml = "svn info --xml ${base_dir}/images/styles/standard/".execute().text
    485         }
    486 
    487         def svninfo = new XmlParser().parseText(xml)
    488         def rev = svninfo.entry.'@revision'[0]
    489         cached_svnrev = Integer.parseInt(rev)
    490         assert cached_svnrev > 0
    491         return cached_svnrev
    492     }
    493 
    494     /**
    495      * Read the style sheet file and parse the MapCSS code.
    496      */
    497     def parse_style_sheet() {
    498         def file = new CachedFile(input_file)
    499         def stream = file.getInputStream()
    500         def parser = new MapCSSParser(stream, "UTF-8", MapCSSParser.LexicalState.DEFAULT)
    501         style_source = new MapCSSStyleSource("")
    502         style_source.url = ""
    503         parser.sheet(style_source)
    504     }
    505 
    506     /**
    507      * Collect all the tag from the style sheet.
    508      */
    509     def collect_tags() {
    510         for (rule in style_source.rules) {
    511             def selector = rule.selector
    512             if (selector instanceof GeneralSelector) {
    513                 def conditions = selector.getConditions()
    514                 for (cond in conditions) {
    515                     if (cond instanceof SimpleKeyValueCondition) {
    516                         tags.add([cond.k, cond.v])
    517                     }
    518                 }
    519             }
    520         }
    521     }
    522 }
  • src/org/openstreetmap/josm/gui/preferences/map/TaggingPresetPreference.java

    diff --git a/src/org/openstreetmap/josm/gui/preferences/map/TaggingPresetPreference.java b/src/org/openstreetmap/josm/gui/preferences/map/TaggingPresetPreference.java
    index 85164bf34..8f015eefb 100644
    a b public void addGui(PreferenceTabbedPane gui) { 
    181181        gui.addValidationListener(validationListener);
    182182    }
    183183
    184     static class TaggingPresetSourceEditor extends SourceEditor {
     184    public static class TaggingPresetSourceEditor extends SourceEditor {
    185185
    186186        private static final String ICONPREF = "taggingpreset.icon.sources";
    187187
    188         TaggingPresetSourceEditor() {
     188        public TaggingPresetSourceEditor() {
    189189            super(SourceType.TAGGING_PRESET, Config.getUrls().getJOSMWebsite()+"/presets", presetSourceProviders, true);
    190190        }
    191191
  • src/org/openstreetmap/josm/gui/tagging/presets/items/KeyedItem.java

    diff --git a/src/org/openstreetmap/josm/gui/tagging/presets/items/KeyedItem.java b/src/org/openstreetmap/josm/gui/tagging/presets/items/KeyedItem.java
    index 698d56412..d40ce08ed 100644
    a b protected static Usage determineBooleanUsage(Collection<OsmPrimitive> sel, Strin 
    164164        return returnValue;
    165165    }
    166166
     167    /**
     168     * Determines whether key or key+value are required.
     169     * @return whether key or key+value are required
     170     */
     171    public boolean isKeyRequired() {
     172        final MatchType type = MatchType.ofString(match);
     173        return MatchType.KEY_REQUIRED.equals(type) || MatchType.KEY_VALUE_REQUIRED.equals(type);
     174    }
     175
    167176    /**
    168177     * Returns the default match.
    169178     * @return the default match
  • new file src/org/openstreetmap/josm/tools/TagInfoExtract.java

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