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

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

see #15182 - deprecate Main.worker, replace it by gui.MainApplication.worker + code refactoring to make sure only editor packages use it

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