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

Last change on this file since 14109 was 14025, checked in by Don-vip, 14 months ago

taginfo: remove withCloseable statement requiring groovy >= 2.4.0

  • Property svn:eol-style set to native
File size: 19.8 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        def json = Json.createWriterFactory(config).createWriter(writer)
368        try {
369            def project = Json.createObjectBuilder()
370                .add("name", name)
371                .add("description", description)
372                .add("project_url", "https://josm.openstreetmap.de/")
373                .add("icon_url", "https://josm.openstreetmap.de/export/7770/josm/trunk/images/logo_16x16x8.png")
374                .add("contact_name", "JOSM developer team")
375                .add("contact_email", "josm-dev@openstreetmap.org")
376            def jsonTags = Json.createArrayBuilder()
377            for (def t : tags) {
378                def o = Json.createObjectBuilder()
379                for (def e : t.entrySet()) {
380                    def val = e.getValue()
381                    if (e.getValue() instanceof List) {
382                        def arr = Json.createArrayBuilder()
383                        for (def v : e.getValue()) {
384                            arr.add(v)
385                        }
386                        val = arr.build()
387                    }
388                    o.add(e.getKey(), val)
389                }
390                jsonTags.add(o.build())
391            }
392            json.writeObject(Json.createObjectBuilder()
393                .add("data_format", 1)
394                .add("data_updated", DateTimeFormatter.ofPattern("yyyyMMdd'T'hhmmss'Z'").withZone(ZoneId.of("Z")).format(Instant.now()))
395                .add("project", project.build())
396                .add("tags", jsonTags.build())
397                .build())
398        } finally {
399            json.close()
400        }
401
402        if (output_file != null) {
403            output_file.close()
404        } else {
405            print writer.toString()
406        }
407    }
408
409    /**
410     * Initialize the script.
411     */
412    def init() {
413        Main.determinePlatformHook()
414        Logging.setLogLevel(Logging.LEVEL_INFO)
415        Main.pref.enableSaveOnPut(false)
416        Config.setPreferencesInstance(Main.pref)
417        Config.setBaseDirectoriesProvider(JosmBaseDirectories.getInstance());
418        Main.setProjection(Projections.getProjectionByCode("EPSG:3857"))
419        Path tmpdir = Files.createTempDirectory(FileSystems.getDefault().getPath(base_dir), "pref")
420        tmpdir.toFile().deleteOnExit()
421        System.setProperty("josm.home", tmpdir.toString())
422        DeleteCommand.setDeletionCallback(DeleteAction.defaultDeletionCallback)
423        Territories.initialize()
424        RightAndLefthandTraffic.initialize()
425
426        josm_svn_revision = Version.getInstance().getVersion()
427        assert josm_svn_revision != Version.JOSM_UNKNOWN_VERSION
428
429        if (options.arguments().size() == 0 && (!options.t || options.t == 'mappaint')) {
430            input_file = "resource://styles/standard/elemstyles.mapcss"
431        } else if (options.arguments().size() == 0 && options.t == 'presets') {
432            input_file = "resource://data/defaultpresets.xml"
433        } else {
434            input_file = options.arguments()[0]
435        }
436
437        output_file = null
438        if (options.o && options.o != "-") {
439            output_file = new FileWriter(options.o)
440        }
441    }
442
443    /**
444     * Determine full image url (can refer to JOSM or OSM repository).
445     */
446    def find_image_url(String path) {
447        def f = new File("${base_dir}/images/styles/standard/${path}")
448        if (f.exists()) {
449            def rev = osm_svn_revision()
450            return "https://trac.openstreetmap.org/export/${rev}/subversion/applications/share/map-icons/classic.small/${path}"
451        }
452        f = new File("${base_dir}/images/${path}")
453        if (f.exists()) {
454            if (path.startsWith("images/styles/standard/")) {
455                path = path.substring("images/styles/standard/".length())
456                def rev = osm_svn_revision()
457                return "https://trac.openstreetmap.org/export/${rev}/subversion/applications/share/map-icons/classic.small/${path}"
458            } else if (path.startsWith("styles/standard/")) {
459                path = path.substring("styles/standard/".length())
460                def rev = osm_svn_revision()
461                return "https://trac.openstreetmap.org/export/${rev}/subversion/applications/share/map-icons/classic.small/${path}"
462            } else {
463                return "https://josm.openstreetmap.de/export/${josm_svn_revision}/josm/trunk/images/${path}"
464            }
465        }
466        assert false, "Cannot find image url for ${path}"
467    }
468
469    /**
470     * Get revision for the repository https://svn.openstreetmap.org.
471     */
472    def osm_svn_revision() {
473        if (cached_svnrev != null) return cached_svnrev
474        if (options.svnrev) {
475            cached_svnrev = Integer.parseInt(options.svnrev)
476            return cached_svnrev
477        }
478        def xml
479        if (options.svnweb) {
480            xml = "svn info --xml https://svn.openstreetmap.org/applications/share/map-icons/classic.small".execute().text
481        } else {
482            xml = "svn info --xml ${base_dir}/images/styles/standard/".execute().text
483        }
484
485        def svninfo = new XmlParser().parseText(xml)
486        def rev = svninfo.entry.'@revision'[0]
487        cached_svnrev = Integer.parseInt(rev)
488        assert cached_svnrev > 0
489        return cached_svnrev
490    }
491
492    /**
493     * Read the style sheet file and parse the MapCSS code.
494     */
495    def parse_style_sheet() {
496        def file = new CachedFile(input_file)
497        def stream = file.getInputStream()
498        def parser = new MapCSSParser(stream, "UTF-8", MapCSSParser.LexicalState.DEFAULT)
499        style_source = new MapCSSStyleSource("")
500        style_source.url = ""
501        parser.sheet(style_source)
502    }
503
504    /**
505     * Collect all the tag from the style sheet.
506     */
507    def collect_tags() {
508        for (rule in style_source.rules) {
509            def selector = rule.selector
510            if (selector instanceof GeneralSelector) {
511                def conditions = selector.getConditions()
512                for (cond in conditions) {
513                    if (cond instanceof SimpleKeyValueCondition) {
514                        tags.add([cond.k, cond.v])
515                    }
516                }
517            }
518        }
519    }
520}
Note: See TracBrowser for help on using the repository browser.