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

Last change on this file since 14188 was 14149, checked in by Don-vip, 15 months ago

see #15229 - deprecate Main.pref

  • Property svn:eol-style set to native
File size: 20.0 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.actions.DeleteAction
24import org.openstreetmap.josm.command.DeleteCommand
25import org.openstreetmap.josm.data.Preferences
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.preferences.JosmUrls
35import org.openstreetmap.josm.data.projection.ProjectionRegistry
36import org.openstreetmap.josm.data.projection.Projections
37import org.openstreetmap.josm.gui.NavigatableComponent
38import org.openstreetmap.josm.gui.mappaint.Environment
39import org.openstreetmap.josm.gui.mappaint.MultiCascade
40import org.openstreetmap.josm.gui.mappaint.MapPaintStyles.IconReference
41import org.openstreetmap.josm.gui.mappaint.mapcss.MapCSSStyleSource
42import org.openstreetmap.josm.gui.mappaint.mapcss.ConditionFactory.SimpleKeyValueCondition
43import org.openstreetmap.josm.gui.mappaint.mapcss.Selector.GeneralSelector
44import org.openstreetmap.josm.gui.mappaint.mapcss.parsergen.MapCSSParser
45import org.openstreetmap.josm.gui.mappaint.styleelement.AreaElement
46import org.openstreetmap.josm.gui.mappaint.styleelement.LineElement
47import org.openstreetmap.josm.gui.mappaint.styleelement.StyleElement
48import org.openstreetmap.josm.gui.preferences.map.TaggingPresetPreference
49import org.openstreetmap.josm.gui.tagging.presets.TaggingPreset
50import org.openstreetmap.josm.gui.tagging.presets.TaggingPresetReader
51import org.openstreetmap.josm.gui.tagging.presets.TaggingPresetType
52import org.openstreetmap.josm.gui.tagging.presets.items.KeyedItem
53import org.openstreetmap.josm.gui.tagging.presets.items.KeyedItem.MatchType
54import org.openstreetmap.josm.io.CachedFile
55import org.openstreetmap.josm.spi.preferences.Config
56import org.openstreetmap.josm.tools.Logging
57import org.openstreetmap.josm.tools.RightAndLefthandTraffic
58import org.openstreetmap.josm.tools.Territories
59import org.openstreetmap.josm.tools.Utils
60
61import edu.umd.cs.findbugs.annotations.SuppressFBWarnings
62
63class 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}
Note: See TracBrowser for help on using the repository browser.