source: josm/trunk/scripts/TagInfoExtract.groovy @ 14024

Last change on this file since 14024 was 14024, checked in by Don-vip, 5 years ago

taginfo: use embedded javax.json instead of groovy-json 2.5.0: unable to make it work with Ant anymore

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