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

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

more changes required by Groovy 2.5.0

  • Property svn:eol-style set to native
File size: 18.6 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
20
21import org.openstreetmap.josm.Main
22import org.openstreetmap.josm.actions.DeleteAction
23import org.openstreetmap.josm.command.DeleteCommand
24import org.openstreetmap.josm.data.Version
25import org.openstreetmap.josm.data.coor.LatLon
26import org.openstreetmap.josm.data.osm.Node
27import org.openstreetmap.josm.data.osm.OsmPrimitive
28import org.openstreetmap.josm.data.osm.Way
29import org.openstreetmap.josm.data.osm.visitor.paint.MapPaintSettings
30import org.openstreetmap.josm.data.osm.visitor.paint.StyledMapRenderer
31import org.openstreetmap.josm.data.preferences.JosmBaseDirectories;
32import org.openstreetmap.josm.data.projection.Projections
33import org.openstreetmap.josm.gui.NavigatableComponent
34import org.openstreetmap.josm.gui.mappaint.Environment
35import org.openstreetmap.josm.gui.mappaint.MultiCascade
36import org.openstreetmap.josm.gui.mappaint.MapPaintStyles.IconReference
37import org.openstreetmap.josm.gui.mappaint.mapcss.MapCSSStyleSource
38import org.openstreetmap.josm.gui.mappaint.mapcss.ConditionFactory.SimpleKeyValueCondition
39import org.openstreetmap.josm.gui.mappaint.mapcss.Selector.GeneralSelector
40import org.openstreetmap.josm.gui.mappaint.mapcss.parsergen.MapCSSParser
41import org.openstreetmap.josm.gui.mappaint.styleelement.AreaElement
42import org.openstreetmap.josm.gui.mappaint.styleelement.LineElement
43import org.openstreetmap.josm.gui.mappaint.styleelement.StyleElement
44import org.openstreetmap.josm.gui.preferences.map.TaggingPresetPreference
45import org.openstreetmap.josm.gui.tagging.presets.TaggingPreset
46import org.openstreetmap.josm.gui.tagging.presets.TaggingPresetReader
47import org.openstreetmap.josm.gui.tagging.presets.TaggingPresetType
48import org.openstreetmap.josm.gui.tagging.presets.items.KeyedItem
49import org.openstreetmap.josm.gui.tagging.presets.items.KeyedItem.MatchType
50import org.openstreetmap.josm.io.CachedFile
51import org.openstreetmap.josm.spi.preferences.Config
52import org.openstreetmap.josm.tools.Logging
53import org.openstreetmap.josm.tools.RightAndLefthandTraffic
54import org.openstreetmap.josm.tools.Territories
55import org.openstreetmap.josm.tools.Utils
56
57import edu.umd.cs.findbugs.annotations.SuppressFBWarnings
58import groovy.json.JsonBuilder
59
60class TagInfoExtract {
61
62 static def options
63 static String image_dir
64 int josm_svn_revision
65 String input_file
66 MapCSSStyleSource style_source
67 FileWriter output_file
68 String base_dir = "."
69 Set tags = []
70
71 private def cached_svnrev
72
73 /**
74 * Check if a certain tag is supported by the style as node / way / area.
75 */
76 abstract class Checker {
77
78 def tag
79 OsmPrimitive osm
80
81 Checker(tag) {
82 this.tag = tag
83 }
84
85 Environment apply_stylesheet(OsmPrimitive osm) {
86 osm.put(tag[0], tag[1])
87 MultiCascade mc = new MultiCascade()
88
89 Environment env = new Environment(osm, mc, null, style_source)
90 for (def r in style_source.rules) {
91 env.clearSelectorMatchingInformation()
92 if (r.selector.matches(env)) {
93 // ignore selector range
94 if (env.layer == null) {
95 env.layer = "default"
96 }
97 r.execute(env)
98 }
99 }
100 env.layer = "default"
101 return env
102 }
103
104 /**
105 * Create image file from StyleElement.
106 * @return the URL
107 */
108 def create_image(StyleElement elem_style, type, nc) {
109 def img = new BufferedImage(16, 16, BufferedImage.TYPE_INT_ARGB)
110 def g = img.createGraphics()
111 g.setClip(0, 0, 16, 16)
112 def renderer = new StyledMapRenderer(g, nc, false)
113 renderer.getSettings(false)
114 elem_style.paintPrimitive(osm, MapPaintSettings.INSTANCE, renderer, false, false, false)
115 def base_url = options.imgurlprefix ? options.imgurlprefix : image_dir
116 def image_name = "${type}_${tag[0]}=${tag[1]}.png"
117 ImageIO.write(img, "png", new File("${image_dir}/${image_name}"))
118 return "${base_url}/${image_name}"
119 }
120
121 /**
122 * Checks, if tag is supported and find URL for image icon in this case.
123 * @param generate_image if true, create or find a suitable image icon and return URL,
124 * if false, just check if tag is supported and return true or false
125 */
126 abstract def find_url(boolean generate_image)
127 }
128
129 @SuppressFBWarnings(value = "MF_CLASS_MASKS_FIELD")
130 class NodeChecker extends Checker {
131 NodeChecker(tag) {
132 super(tag)
133 }
134
135 @Override
136 def find_url(boolean generate_image) {
137 osm = new Node(LatLon.ZERO)
138 def env = apply_stylesheet(osm)
139 def c = env.mc.getCascade("default")
140 def image = c.get("icon-image")
141 if (image) {
142 if (image instanceof IconReference && !image.isDeprecatedIcon()) {
143 return find_image_url(image.iconName)
144 }
145 }
146 }
147 }
148
149 @SuppressFBWarnings(value = "MF_CLASS_MASKS_FIELD")
150 class WayChecker extends Checker {
151 WayChecker(tag) {
152 super(tag)
153 }
154
155 @Override
156 def find_url(boolean generate_image) {
157 osm = new Way()
158 def nc = new NavigatableComponent()
159 def n1 = new Node(nc.getLatLon(2,8))
160 def n2 = new Node(nc.getLatLon(14,8))
161 ((Way)osm).addNode(n1)
162 ((Way)osm).addNode(n2)
163 def env = apply_stylesheet(osm)
164 def les = LineElement.createLine(env)
165 if (les != null) {
166 if (!generate_image) return true
167 return create_image(les, 'way', nc)
168 }
169 }
170 }
171
172 @SuppressFBWarnings(value = "MF_CLASS_MASKS_FIELD")
173 class AreaChecker extends Checker {
174 AreaChecker(tag) {
175 super(tag)
176 }
177
178 @Override
179 def find_url(boolean generate_image) {
180 osm = new Way()
181 def nc = new NavigatableComponent()
182 def n1 = new Node(nc.getLatLon(2,2))
183 def n2 = new Node(nc.getLatLon(14,2))
184 def n3 = new Node(nc.getLatLon(14,14))
185 def n4 = new Node(nc.getLatLon(2,14))
186 ((Way)osm).addNode(n1)
187 ((Way)osm).addNode(n2)
188 ((Way)osm).addNode(n3)
189 ((Way)osm).addNode(n4)
190 ((Way)osm).addNode(n1)
191 def env = apply_stylesheet(osm)
192 def aes = AreaElement.create(env)
193 if (aes != null) {
194 if (!generate_image) return true
195 return create_image(aes, 'area', nc)
196 }
197 }
198 }
199
200 /**
201 * Main method.
202 */
203 static main(def args) {
204 parse_command_line_arguments(args)
205 def script = new TagInfoExtract()
206 if (!options.t || options.t == 'mappaint') {
207 script.run()
208 } else if (options.t == 'presets') {
209 script.run_presets()
210 } else if (options.t == 'external_presets') {
211 script.run_external_presets()
212 } else {
213 System.err.println 'Invalid type ' + options.t
214 if (!options.noexit) {
215 System.exit(1)
216 }
217 }
218
219 if (!options.noexit) {
220 System.exit(0)
221 }
222 }
223
224 /**
225 * Parse command line arguments.
226 */
227 static void parse_command_line_arguments(args) {
228 def cli = new CliBuilder(usage:'taginfoextract.groovy [options] [inputfile]',
229 header:"Options:",
230 footer:"[inputfile] the file to process (optional, default is 'resource://styles/standard/elemstyles.mapcss')")
231 cli.o(args:1, argName: "file", "output file (json), - prints to stdout (default: -)")
232 cli.t(args:1, argName: "type", "the project type to be generated")
233 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)")
234 cli._(longOpt:'imgdir', args:1, argName:"directory", "directory to put the generated images in (default: ./taginfo-img)")
235 cli._(longOpt:'noexit', "don't call System.exit(), for use from Ant script")
236 cli._(longOpt:'svnweb', 'fetch revision of the repository https://svn.openstreetmap.org/ from web and not from the local repository')
237 cli._(longOpt:'imgurlprefix', args:1, argName:'prefix', 'image URLs prefix for generated image files')
238 cli.h(longOpt:'help', "show this help")
239 options = cli.parse(args)
240
241 if (options.h) {
242 cli.usage()
243 System.exit(0)
244 }
245 if (options.arguments().size() > 1) {
246 System.err.println "Error: More than one input file given!"
247 cli.usage()
248 System.exit(-1)
249 }
250 if (options.svnrev) {
251 assert Integer.parseInt(options.svnrev) > 0
252 }
253 image_dir = 'taginfo-img'
254 if (options.imgdir) {
255 image_dir = options.imgdir
256 }
257 def image_dir_file = new File(image_dir)
258 if (!image_dir_file.exists()) {
259 image_dir_file.mkdirs()
260 }
261 }
262
263 void run_presets() {
264 init()
265 def presets = TaggingPresetReader.readAll(input_file, true)
266 def tags = convert_presets(presets, "", true)
267 write_json("JOSM main presets", "Tags supported by the default presets in the OSM editor JOSM", tags)
268 }
269
270 def convert_presets(Iterable<TaggingPreset> presets, String descriptionPrefix, boolean addImages) {
271 def tags = []
272 for (TaggingPreset preset : presets) {
273 for (KeyedItem item : Utils.filteredCollection(preset.data, KeyedItem.class)) {
274 def values
275 switch (MatchType.ofString(item.match)) {
276 case MatchType.KEY_REQUIRED: values = item.getValues(); break;
277 case MatchType.KEY_VALUE_REQUIRED: values = item.getValues(); break;
278 default: values = [];
279 }
280 for (String value : values) {
281 def tag = [
282 description: descriptionPrefix + preset.name,
283 key: item.key,
284 value: value,
285 ]
286 def otypes = preset.types.collect {
287 it == TaggingPresetType.CLOSEDWAY ? "area" :
288 (it == TaggingPresetType.MULTIPOLYGON ? "relation" : it.toString().toLowerCase(Locale.ENGLISH))
289 }
290 if (!otypes.isEmpty()) tag += [object_types: otypes]
291 if (addImages && preset.iconName) tag += [icon_url: find_image_url(preset.iconName)]
292 tags += tag
293 }
294 }
295 }
296 return tags
297 }
298
299 void run_external_presets() {
300 init()
301 TaggingPresetReader.setLoadIcons(false)
302 def sources = new TaggingPresetPreference.TaggingPresetSourceEditor().loadAndGetAvailableSources()
303 def tags = []
304 for (def source : sources) {
305 if (source.url.startsWith("resource")) {
306 // default presets
307 continue;
308 }
309 try {
310 println "Loading ${source.url}"
311 def presets = TaggingPresetReader.readAll(source.url, false)
312 def t = convert_presets(presets, source.title + " ", false)
313 println "Converting ${t.size()} presets of ${source.title}"
314 tags += t
315 } catch (Exception ex) {
316 System.err.println("Skipping ${source.url} due to error")
317 ex.printStackTrace()
318 }
319 }
320 write_json("JOSM user presets", "Tags supported by the user contributed presets in the OSM editor JOSM", tags)
321 }
322
323 void run() {
324 init()
325 parse_style_sheet()
326 collect_tags()
327
328 def tags = tags.collect {
329 def tag = it
330 def types = []
331 def final_url = null
332
333 def node_url = new NodeChecker(tag).find_url(true)
334 if (node_url) {
335 types += 'node'
336 final_url = node_url
337 }
338 def way_url = new WayChecker(tag).find_url(final_url == null)
339 if (way_url) {
340 types += 'way'
341 if (!final_url) {
342 final_url = way_url
343 }
344 }
345 def area_url = new AreaChecker(tag).find_url(final_url == null)
346 if (area_url) {
347 types += 'area'
348 if (!final_url) {
349 final_url = area_url
350 }
351 }
352
353 def obj = [key: tag[0], value: tag[1]]
354 if (types) obj += [object_types: types]
355 if (final_url) obj += [icon_url: final_url]
356 obj
357 }
358
359 write_json("JOSM main mappaint style", "Tags supported by the main mappaint style in the OSM editor JOSM", tags)
360 }
361
362 void write_json(name, description, tags) {
363 def json = new JsonBuilder()
364 def project = [
365 name: name,
366 description: description,
367 project_url: "https://josm.openstreetmap.de/",
368 icon_url: "https://josm.openstreetmap.de/export/7770/josm/trunk/images/logo_16x16x8.png",
369 contact_name: "JOSM developer team",
370 contact_email: "josm-dev@openstreetmap.org",
371 ]
372 json data_format: 1, data_updated: DateTimeFormatter.ofPattern("yyyyMMdd'T'hhmmss'Z'").withZone(ZoneId.of("Z")).format(Instant.now()), project: project, tags: tags
373
374 if (output_file != null) {
375 json.writeTo(output_file)
376 output_file.close()
377 } else {
378 print json.toPrettyString()
379 }
380 }
381
382 /**
383 * Initialize the script.
384 */
385 def init() {
386 Main.determinePlatformHook()
387 Logging.setLogLevel(Logging.LEVEL_INFO)
388 Main.pref.enableSaveOnPut(false)
389 Config.setPreferencesInstance(Main.pref)
390 Config.setBaseDirectoriesProvider(JosmBaseDirectories.getInstance());
391 Main.setProjection(Projections.getProjectionByCode("EPSG:3857"))
392 Path tmpdir = Files.createTempDirectory(FileSystems.getDefault().getPath(base_dir), "pref")
393 tmpdir.toFile().deleteOnExit()
394 System.setProperty("josm.home", tmpdir.toString())
395 DeleteCommand.setDeletionCallback(DeleteAction.defaultDeletionCallback)
396 Territories.initialize()
397 RightAndLefthandTraffic.initialize()
398
399 josm_svn_revision = Version.getInstance().getVersion()
400 assert josm_svn_revision != Version.JOSM_UNKNOWN_VERSION
401
402 if (options.arguments().size() == 0 && (!options.t || options.t == 'mappaint')) {
403 input_file = "resource://styles/standard/elemstyles.mapcss"
404 } else if (options.arguments().size() == 0 && options.t == 'presets') {
405 input_file = "resource://data/defaultpresets.xml"
406 } else {
407 input_file = options.arguments()[0]
408 }
409
410 output_file = null
411 if (options.o && options.o != "-") {
412 output_file = new FileWriter(options.o)
413 }
414 }
415
416 /**
417 * Determine full image url (can refer to JOSM or OSM repository).
418 */
419 def find_image_url(String path) {
420 def f = new File("${base_dir}/images/styles/standard/${path}")
421 if (f.exists()) {
422 def rev = osm_svn_revision()
423 return "https://trac.openstreetmap.org/export/${rev}/subversion/applications/share/map-icons/classic.small/${path}"
424 }
425 f = new File("${base_dir}/images/${path}")
426 if (f.exists()) {
427 if (path.startsWith("images/styles/standard/")) {
428 path = path.substring("images/styles/standard/".length())
429 def rev = osm_svn_revision()
430 return "https://trac.openstreetmap.org/export/${rev}/subversion/applications/share/map-icons/classic.small/${path}"
431 } else if (path.startsWith("styles/standard/")) {
432 path = path.substring("styles/standard/".length())
433 def rev = osm_svn_revision()
434 return "https://trac.openstreetmap.org/export/${rev}/subversion/applications/share/map-icons/classic.small/${path}"
435 } else {
436 return "https://josm.openstreetmap.de/export/${josm_svn_revision}/josm/trunk/images/${path}"
437 }
438 }
439 assert false, "Cannot find image url for ${path}"
440 }
441
442 /**
443 * Get revision for the repository https://svn.openstreetmap.org.
444 */
445 def osm_svn_revision() {
446 if (cached_svnrev != null) return cached_svnrev
447 if (options.svnrev) {
448 cached_svnrev = Integer.parseInt(options.svnrev)
449 return cached_svnrev
450 }
451 def xml
452 if (options.svnweb) {
453 xml = "svn info --xml https://svn.openstreetmap.org/applications/share/map-icons/classic.small".execute().text
454 } else {
455 xml = "svn info --xml ${base_dir}/images/styles/standard/".execute().text
456 }
457
458 def svninfo = new XmlParser().parseText(xml)
459 def rev = svninfo.entry.'@revision'[0]
460 cached_svnrev = Integer.parseInt(rev)
461 assert cached_svnrev > 0
462 return cached_svnrev
463 }
464
465 /**
466 * Read the style sheet file and parse the MapCSS code.
467 */
468 def parse_style_sheet() {
469 def file = new CachedFile(input_file)
470 def stream = file.getInputStream()
471 def parser = new MapCSSParser(stream, "UTF-8", MapCSSParser.LexicalState.DEFAULT)
472 style_source = new MapCSSStyleSource("")
473 style_source.url = ""
474 parser.sheet(style_source)
475 }
476
477 /**
478 * Collect all the tag from the style sheet.
479 */
480 def collect_tags() {
481 for (rule in style_source.rules) {
482 def selector = rule.selector
483 if (selector instanceof GeneralSelector) {
484 def conditions = selector.getConditions()
485 for (cond in conditions) {
486 if (cond instanceof SimpleKeyValueCondition) {
487 tags.add([cond.k, cond.v])
488 }
489 }
490 }
491 }
492 }
493}
Note: See TracBrowser for help on using the repository browser.