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

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