source: josm/trunk/src/org/openstreetmap/josm/gui/mappaint/MapPaintStyles.java@ 8937

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

add unit tests to check validity of all map paint styles and tagging presets

  • Property svn:eol-style set to native
File size: 17.8 KB
Line 
1// License: GPL. For details, see LICENSE file.
2package org.openstreetmap.josm.gui.mappaint;
3
4import static org.openstreetmap.josm.tools.I18n.tr;
5
6import java.io.File;
7import java.io.IOException;
8import java.io.InputStreamReader;
9import java.nio.charset.StandardCharsets;
10import java.util.ArrayList;
11import java.util.Arrays;
12import java.util.Collection;
13import java.util.Collections;
14import java.util.HashSet;
15import java.util.LinkedList;
16import java.util.List;
17import java.util.Set;
18import java.util.concurrent.CopyOnWriteArrayList;
19
20import javax.swing.ImageIcon;
21import javax.swing.SwingUtilities;
22
23import org.openstreetmap.josm.Main;
24import org.openstreetmap.josm.data.coor.LatLon;
25import org.openstreetmap.josm.data.osm.DataSet;
26import org.openstreetmap.josm.data.osm.Node;
27import org.openstreetmap.josm.data.osm.Tag;
28import org.openstreetmap.josm.gui.PleaseWaitRunnable;
29import org.openstreetmap.josm.gui.mappaint.StyleCache.StyleList;
30import org.openstreetmap.josm.gui.mappaint.mapcss.MapCSSStyleSource;
31import org.openstreetmap.josm.gui.mappaint.xml.XmlStyleSource;
32import org.openstreetmap.josm.gui.preferences.SourceEntry;
33import org.openstreetmap.josm.gui.preferences.map.MapPaintPreference.MapPaintPrefHelper;
34import org.openstreetmap.josm.gui.progress.ProgressMonitor;
35import org.openstreetmap.josm.io.CachedFile;
36import org.openstreetmap.josm.tools.ImageProvider;
37import org.openstreetmap.josm.tools.Utils;
38
39/**
40 * This class manages the ElemStyles instance. The object you get with
41 * getStyles() is read only, any manipulation happens via one of
42 * the wrapper methods here. (readFromPreferences, moveStyles, ...)
43 *
44 * On change, mapPaintSylesUpdated() is fired for all listeners.
45 */
46public final class MapPaintStyles {
47
48 private static ElemStyles styles = new ElemStyles();
49
50 /**
51 * Returns the {@link ElemStyles} instance.
52 * @return the {@code ElemStyles} instance
53 */
54 public static ElemStyles getStyles() {
55 return styles;
56 }
57
58 private MapPaintStyles() {
59 // Hide default constructor for utils classes
60 }
61
62 /**
63 * Value holder for a reference to a tag name. A style instruction
64 * <pre>
65 * text: a_tag_name;
66 * </pre>
67 * results in a tag reference for the tag <tt>a_tag_name</tt> in the
68 * style cascade.
69 */
70 public static class TagKeyReference {
71 public final String key;
72
73 public TagKeyReference(String key) {
74 this.key = key;
75 }
76
77 @Override
78 public String toString() {
79 return "TagKeyReference{" + "key='" + key + "'}";
80 }
81 }
82
83 /**
84 * IconReference is used to remember the associated style source for
85 * each icon URL.
86 * This is necessary because image URLs can be paths relative
87 * to the source file and we have cascading of properties from different
88 * source files.
89 */
90 public static class IconReference {
91
92 public final String iconName;
93 public final StyleSource source;
94
95 public IconReference(String iconName, StyleSource source) {
96 this.iconName = iconName;
97 this.source = source;
98 }
99
100 @Override
101 public String toString() {
102 return "IconReference{" + "iconName='" + iconName + "' source='" + source.getDisplayString() + "'}";
103 }
104 }
105
106 /**
107 * Image provider for icon. Note that this is a provider only. A @link{ImageProvider#get()} call may still
108 * fail!
109 *
110 * @param ref reference to the requested icon
111 * @param test if <code>true</code> than the icon is request is tested
112 * @return image provider for icon (can be <code>null</code> when <code>test</code> is <code>true</code>).
113 * @see #getIcon(IconReference, int,int)
114 * @since 8097
115 */
116 public static ImageProvider getIconProvider(IconReference ref, boolean test) {
117 final String namespace = ref.source.getPrefName();
118 ImageProvider i = new ImageProvider(ref.iconName)
119 .setDirs(getIconSourceDirs(ref.source))
120 .setId("mappaint."+namespace)
121 .setArchive(ref.source.zipIcons)
122 .setInArchiveDir(ref.source.getZipEntryDirName())
123 .setOptional(true);
124 if (test && i.get() == null) {
125 Main.warn("Mappaint style \""+namespace+"\" ("+ref.source.getDisplayString()+") icon \"" + ref.iconName + "\" not found.");
126 return null;
127 }
128 return i;
129 }
130
131 /**
132 * Return scaled icon.
133 *
134 * @param ref reference to the requested icon
135 * @param width icon width or -1 for autoscale
136 * @param height icon height or -1 for autoscale
137 * @return image icon or <code>null</code>.
138 * @see #getIconProvider(IconReference, boolean)
139 */
140 public static ImageIcon getIcon(IconReference ref, int width, int height) {
141 final String namespace = ref.source.getPrefName();
142 ImageIcon i = getIconProvider(ref, false).setWidth(width).setHeight(height).get();
143 if (i == null) {
144 Main.warn("Mappaint style \""+namespace+"\" ("+ref.source.getDisplayString()+") icon \"" + ref.iconName + "\" not found.");
145 return null;
146 }
147 return i;
148 }
149
150 /**
151 * No icon with the given name was found, show a dummy icon instead
152 * @return the icon misc/no_icon.png, in descending priority:
153 * - relative to source file
154 * - from user icon paths
155 * - josm's default icon
156 * can be null if the defaults are turned off by user
157 */
158 public static ImageIcon getNoIcon_Icon(StyleSource source) {
159 return new ImageProvider("misc/no_icon")
160 .setDirs(getIconSourceDirs(source))
161 .setId("mappaint."+source.getPrefName())
162 .setArchive(source.zipIcons)
163 .setInArchiveDir(source.getZipEntryDirName())
164 .setOptional(true).get();
165 }
166
167 public static ImageIcon getNodeIcon(Tag tag) {
168 return getNodeIcon(tag, true);
169 }
170
171 /**
172 * Returns the node icon that would be displayed for the given tag.
173 * @param tag The tag to look an icon for
174 * @param includeDeprecatedIcon if {@code true}, the special deprecated icon will be returned if applicable
175 * @return {@code null} if no icon found, or if the icon is deprecated and not wanted
176 */
177 public static ImageIcon getNodeIcon(Tag tag, boolean includeDeprecatedIcon) {
178 if (tag != null) {
179 DataSet ds = new DataSet();
180 Node virtualNode = new Node(LatLon.ZERO);
181 virtualNode.put(tag.getKey(), tag.getValue());
182 StyleList styleList;
183 MapCSSStyleSource.STYLE_SOURCE_LOCK.readLock().lock();
184 try {
185 // Add primitive to dataset to avoid DataIntegrityProblemException when evaluating selectors
186 ds.addPrimitive(virtualNode);
187 styleList = getStyles().generateStyles(virtualNode, 0.5, false).a;
188 ds.removePrimitive(virtualNode);
189 } finally {
190 MapCSSStyleSource.STYLE_SOURCE_LOCK.readLock().unlock();
191 }
192 if (styleList != null) {
193 for (ElemStyle style : styleList) {
194 if (style instanceof NodeElemStyle) {
195 MapImage mapImage = ((NodeElemStyle) style).mapImage;
196 if (mapImage != null) {
197 if (includeDeprecatedIcon || mapImage.name == null || !"misc/deprecated.png".equals(mapImage.name)) {
198 return new ImageIcon(mapImage.getImage(false));
199 } else {
200 return null; // Deprecated icon found but not wanted
201 }
202 }
203 }
204 }
205 }
206 }
207 return null;
208 }
209
210 public static List<String> getIconSourceDirs(StyleSource source) {
211 List<String> dirs = new LinkedList<>();
212
213 File sourceDir = source.getLocalSourceDir();
214 if (sourceDir != null) {
215 dirs.add(sourceDir.getPath());
216 }
217
218 Collection<String> prefIconDirs = Main.pref.getCollection("mappaint.icon.sources");
219 for (String fileset : prefIconDirs) {
220 String[] a;
221 if (fileset.indexOf('=') >= 0) {
222 a = fileset.split("=", 2);
223 } else {
224 a = new String[] {"", fileset};
225 }
226
227 /* non-prefixed path is generic path, always take it */
228 if (a[0].isEmpty() || source.getPrefName().equals(a[0])) {
229 dirs.add(a[1]);
230 }
231 }
232
233 if (Main.pref.getBoolean("mappaint.icon.enable-defaults", true)) {
234 /* don't prefix icon path, as it should be generic */
235 dirs.add("resource://images/styles/standard/");
236 dirs.add("resource://images/styles/");
237 }
238
239 return dirs;
240 }
241
242 public static void readFromPreferences() {
243 styles.clear();
244
245 Collection<? extends SourceEntry> sourceEntries = MapPaintPrefHelper.INSTANCE.get();
246
247 for (SourceEntry entry : sourceEntries) {
248 StyleSource source = fromSourceEntry(entry);
249 if (source != null) {
250 styles.add(source);
251 }
252 }
253 for (StyleSource source : styles.getStyleSources()) {
254 loadStyleForFirstTime(source);
255 }
256 fireMapPaintSylesUpdated();
257 }
258
259 private static void loadStyleForFirstTime(StyleSource source) {
260 final long startTime = System.currentTimeMillis();
261 source.loadStyleSource();
262 if (Main.pref.getBoolean("mappaint.auto_reload_local_styles", true) && source.isLocal()) {
263 try {
264 Main.fileWatcher.registerStyleSource(source);
265 } catch (IOException e) {
266 Main.error(e);
267 }
268 }
269 if (Main.isDebugEnabled() || !source.getErrors().isEmpty()) {
270 final long elapsedTime = System.currentTimeMillis() - startTime;
271 String message = "Initializing map style " + source.url + " completed in " + Utils.getDurationString(elapsedTime);
272 if (!source.getErrors().isEmpty()) {
273 Main.warn(message + " (" + source.getErrors().size() + " errors)");
274 } else {
275 Main.debug(message);
276 }
277 }
278 }
279
280 private static StyleSource fromSourceEntry(SourceEntry entry) {
281 CachedFile cf = null;
282 try {
283 Set<String> mimes = new HashSet<>();
284 mimes.addAll(Arrays.asList(XmlStyleSource.XML_STYLE_MIME_TYPES.split(", ")));
285 mimes.addAll(Arrays.asList(MapCSSStyleSource.MAPCSS_STYLE_MIME_TYPES.split(", ")));
286 cf = new CachedFile(entry.url).setHttpAccept(Utils.join(", ", mimes));
287 String zipEntryPath = cf.findZipEntryPath("mapcss", "style");
288 if (zipEntryPath != null) {
289 entry.isZip = true;
290 entry.zipEntryPath = zipEntryPath;
291 return new MapCSSStyleSource(entry);
292 }
293 zipEntryPath = cf.findZipEntryPath("xml", "style");
294 if (zipEntryPath != null)
295 return new XmlStyleSource(entry);
296 if (Utils.hasExtension(entry.url, "mapcss"))
297 return new MapCSSStyleSource(entry);
298 if (Utils.hasExtension(entry.url, "xml"))
299 return new XmlStyleSource(entry);
300 else {
301 try (InputStreamReader reader = new InputStreamReader(cf.getInputStream(), StandardCharsets.UTF_8)) {
302 WHILE: while (true) {
303 int c = reader.read();
304 switch (c) {
305 case -1:
306 break WHILE;
307 case ' ':
308 case '\t':
309 case '\n':
310 case '\r':
311 continue;
312 case '<':
313 return new XmlStyleSource(entry);
314 default:
315 return new MapCSSStyleSource(entry);
316 }
317 }
318 }
319 Main.warn("Could not detect style type. Using default (xml).");
320 return new XmlStyleSource(entry);
321 }
322 } catch (IOException e) {
323 Main.warn(tr("Failed to load Mappaint styles from ''{0}''. Exception was: {1}", entry.url, e.toString()));
324 Main.error(e);
325 }
326 return null;
327 }
328
329 /**
330 * reload styles
331 * preferences are the same, but the file source may have changed
332 * @param sel the indices of styles to reload
333 */
334 public static void reloadStyles(final int... sel) {
335 List<StyleSource> toReload = new ArrayList<>();
336 List<StyleSource> data = styles.getStyleSources();
337 for (int i : sel) {
338 toReload.add(data.get(i));
339 }
340 Main.worker.submit(new MapPaintStyleLoader(toReload));
341 }
342
343 public static class MapPaintStyleLoader extends PleaseWaitRunnable {
344 private boolean canceled;
345 private Collection<StyleSource> sources;
346
347 public MapPaintStyleLoader(Collection<StyleSource> sources) {
348 super(tr("Reloading style sources"));
349 this.sources = sources;
350 }
351
352 @Override
353 protected void cancel() {
354 canceled = true;
355 }
356
357 @Override
358 protected void finish() {
359 SwingUtilities.invokeLater(new Runnable() {
360 @Override
361 public void run() {
362 fireMapPaintSylesUpdated();
363 styles.clearCached();
364 if (Main.isDisplayingMapView()) {
365 Main.map.mapView.preferenceChanged(null);
366 Main.map.mapView.repaint();
367 }
368 }
369 });
370 }
371
372 @Override
373 protected void realRun() {
374 ProgressMonitor monitor = getProgressMonitor();
375 monitor.setTicksCount(sources.size());
376 for (StyleSource s : sources) {
377 if (canceled)
378 return;
379 monitor.subTask(tr("loading style ''{0}''...", s.getDisplayString()));
380 s.loadStyleSource();
381 monitor.worked(1);
382 }
383 }
384 }
385
386 /**
387 * Move position of entries in the current list of StyleSources
388 * @param sel The indices of styles to be moved.
389 * @param delta The number of lines it should move. positive int moves
390 * down and negative moves up.
391 */
392 public static void moveStyles(int[] sel, int delta) {
393 if (!canMoveStyles(sel, delta))
394 return;
395 int[] selSorted = Utils.copyArray(sel);
396 Arrays.sort(selSorted);
397 List<StyleSource> data = new ArrayList<>(styles.getStyleSources());
398 for (int row: selSorted) {
399 StyleSource t1 = data.get(row);
400 StyleSource t2 = data.get(row + delta);
401 data.set(row, t2);
402 data.set(row + delta, t1);
403 }
404 styles.setStyleSources(data);
405 MapPaintPrefHelper.INSTANCE.put(data);
406 fireMapPaintSylesUpdated();
407 styles.clearCached();
408 Main.map.mapView.repaint();
409 }
410
411 public static boolean canMoveStyles(int[] sel, int i) {
412 if (sel.length == 0)
413 return false;
414 int[] selSorted = Utils.copyArray(sel);
415 Arrays.sort(selSorted);
416
417 if (i < 0) // Up
418 return selSorted[0] >= -i;
419 else if (i > 0) // Down
420 return selSorted[selSorted.length-1] <= styles.getStyleSources().size() - 1 - i;
421 else
422 return true;
423 }
424
425 public static void toggleStyleActive(int... sel) {
426 List<StyleSource> data = styles.getStyleSources();
427 for (int p : sel) {
428 StyleSource s = data.get(p);
429 s.active = !s.active;
430 }
431 MapPaintPrefHelper.INSTANCE.put(data);
432 if (sel.length == 1) {
433 fireMapPaintStyleEntryUpdated(sel[0]);
434 } else {
435 fireMapPaintSylesUpdated();
436 }
437 styles.clearCached();
438 Main.map.mapView.repaint();
439 }
440
441 /**
442 * Add a new map paint style.
443 * @param entry map paint style
444 * @return list of errors that occured during loading
445 */
446 public static Collection<Throwable> addStyle(SourceEntry entry) {
447 StyleSource source = fromSourceEntry(entry);
448 if (source != null) {
449 styles.add(source);
450 loadStyleForFirstTime(source);
451 MapPaintPrefHelper.INSTANCE.put(styles.getStyleSources());
452 fireMapPaintSylesUpdated();
453 styles.clearCached();
454 if (Main.isDisplayingMapView()) {
455 Main.map.mapView.repaint();
456 }
457 return source.getErrors();
458 }
459 return Collections.emptyList();
460 }
461
462 /***********************************
463 * MapPaintSylesUpdateListener &amp; related code
464 * (get informed when the list of MapPaint StyleSources changes)
465 */
466
467 public interface MapPaintSylesUpdateListener {
468 void mapPaintStylesUpdated();
469
470 void mapPaintStyleEntryUpdated(int idx);
471 }
472
473 private static final CopyOnWriteArrayList<MapPaintSylesUpdateListener> listeners
474 = new CopyOnWriteArrayList<>();
475
476 public static void addMapPaintSylesUpdateListener(MapPaintSylesUpdateListener listener) {
477 if (listener != null) {
478 listeners.addIfAbsent(listener);
479 }
480 }
481
482 public static void removeMapPaintSylesUpdateListener(MapPaintSylesUpdateListener listener) {
483 listeners.remove(listener);
484 }
485
486 public static void fireMapPaintSylesUpdated() {
487 for (MapPaintSylesUpdateListener l : listeners) {
488 l.mapPaintStylesUpdated();
489 }
490 }
491
492 public static void fireMapPaintStyleEntryUpdated(int idx) {
493 for (MapPaintSylesUpdateListener l : listeners) {
494 l.mapPaintStyleEntryUpdated(idx);
495 }
496 }
497}
Note: See TracBrowser for help on using the repository browser.