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

Last change on this file since 14024 was 14024, checked in by Don-vip, 7 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.