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

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

see #15229 - deprecate all Main methods related to projections. New ProjectionRegistry class

  • 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.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.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        Main.determinePlatformHook()
416        Logging.setLogLevel(Logging.LEVEL_INFO)
417        Main.pref.enableSaveOnPut(false)
418        Config.setPreferencesInstance(Main.pref)
419        Config.setBaseDirectoriesProvider(JosmBaseDirectories.getInstance())
420        Config.setUrlsProvider(JosmUrls.getInstance())
421        ProjectionRegistry.setProjection(Projections.getProjectionByCode("EPSG:3857"))
422        Path tmpdir = Files.createTempDirectory(FileSystems.getDefault().getPath(base_dir), "pref")
423        tmpdir.toFile().deleteOnExit()
424        System.setProperty("josm.home", tmpdir.toString())
425        DeleteCommand.setDeletionCallback(DeleteAction.defaultDeletionCallback)
426        Territories.initialize()
427        RightAndLefthandTraffic.initialize()
428
429        josm_svn_revision = Version.getInstance().getVersion()
430        assert josm_svn_revision != Version.JOSM_UNKNOWN_VERSION
431
432        if (options.arguments().size() == 0 && (!options.t || options.t == 'mappaint')) {
433            input_file = "resource://styles/standard/elemstyles.mapcss"
434        } else if (options.arguments().size() == 0 && options.t == 'presets') {
435            input_file = "resource://data/defaultpresets.xml"
436        } else {
437            input_file = options.arguments()[0]
438        }
439
440        output_file = null
441        if (options.o && options.o != "-") {
442            output_file = new FileWriter(options.o)
443        }
444    }
445
446    /**
447     * Determine full image url (can refer to JOSM or OSM repository).
448     */
449    def find_image_url(String path) {
450        def f = new File("${base_dir}/images/styles/standard/${path}")
451        if (f.exists()) {
452            def rev = osm_svn_revision()
453            return "https://trac.openstreetmap.org/export/${rev}/subversion/applications/share/map-icons/classic.small/${path}"
454        }
455        f = new File("${base_dir}/images/${path}")
456        if (f.exists()) {
457            if (path.startsWith("images/styles/standard/")) {
458                path = path.substring("images/styles/standard/".length())
459                def rev = osm_svn_revision()
460                return "https://trac.openstreetmap.org/export/${rev}/subversion/applications/share/map-icons/classic.small/${path}"
461            } else if (path.startsWith("styles/standard/")) {
462                path = path.substring("styles/standard/".length())
463                def rev = osm_svn_revision()
464                return "https://trac.openstreetmap.org/export/${rev}/subversion/applications/share/map-icons/classic.small/${path}"
465            } else {
466                return "https://josm.openstreetmap.de/export/${josm_svn_revision}/josm/trunk/images/${path}"
467            }
468        }
469        assert false, "Cannot find image url for ${path}"
470    }
471
472    /**
473     * Get revision for the repository https://svn.openstreetmap.org.
474     */
475    def osm_svn_revision() {
476        if (cached_svnrev != null) return cached_svnrev
477        if (options.svnrev) {
478            cached_svnrev = Integer.parseInt(options.svnrev)
479            return cached_svnrev
480        }
481        def xml
482        if (options.svnweb) {
483            xml = "svn info --xml https://svn.openstreetmap.org/applications/share/map-icons/classic.small".execute().text
484        } else {
485            xml = "svn info --xml ${base_dir}/images/styles/standard/".execute().text
486        }
487
488        def svninfo = new XmlParser().parseText(xml)
489        def rev = svninfo.entry.'@revision'[0]
490        cached_svnrev = Integer.parseInt(rev)
491        assert cached_svnrev > 0
492        return cached_svnrev
493    }
494
495    /**
496     * Read the style sheet file and parse the MapCSS code.
497     */
498    def parse_style_sheet() {
499        def file = new CachedFile(input_file)
500        def stream = file.getInputStream()
501        def parser = new MapCSSParser(stream, "UTF-8", MapCSSParser.LexicalState.DEFAULT)
502        style_source = new MapCSSStyleSource("")
503        style_source.url = ""
504        parser.sheet(style_source)
505    }
506
507    /**
508     * Collect all the tag from the style sheet.
509     */
510    def collect_tags() {
511        for (rule in style_source.rules) {
512            def selector = rule.selector
513            if (selector instanceof GeneralSelector) {
514                def conditions = selector.getConditions()
515                for (cond in conditions) {
516                    if (cond instanceof SimpleKeyValueCondition) {
517                        tags.add([cond.k, cond.v])
518                    }
519                }
520            }
521        }
522    }
523}
Note: See TracBrowser for help on using the repository browser.