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

Last change on this file since 17318 was 17083, checked in by GerdP, 4 years ago

fix #19493: Switching to/from wireframe mode much slower than previous versions
Add mappaint.renderer-class-name to ignore list so that map paint styles are not reloaded each time when the renderer is changed

  • Property svn:eol-style set to native
File size: 17.0 KB
Line 
1// License: GPL. For details, see LICENSE file.
2package org.openstreetmap.josm.gui.mappaint;
3
4import java.io.File;
5import java.io.IOException;
6import java.util.ArrayList;
7import java.util.Arrays;
8import java.util.Collection;
9import java.util.LinkedList;
10import java.util.List;
11
12import javax.swing.ImageIcon;
13import javax.swing.SwingUtilities;
14
15import org.openstreetmap.josm.data.osm.OsmPrimitiveType;
16import org.openstreetmap.josm.data.osm.Tag;
17import org.openstreetmap.josm.data.preferences.sources.MapPaintPrefHelper;
18import org.openstreetmap.josm.data.preferences.sources.SourceEntry;
19import org.openstreetmap.josm.gui.mappaint.mapcss.MapCSSStyleSource;
20import org.openstreetmap.josm.io.CachedFile;
21import org.openstreetmap.josm.io.FileWatcher;
22import org.openstreetmap.josm.spi.preferences.Config;
23import org.openstreetmap.josm.spi.preferences.IPreferences;
24import org.openstreetmap.josm.spi.preferences.PreferenceChangeEvent;
25import org.openstreetmap.josm.spi.preferences.PreferenceChangedListener;
26import org.openstreetmap.josm.tools.ImageProvider;
27import org.openstreetmap.josm.tools.ListenerList;
28import org.openstreetmap.josm.tools.Logging;
29import org.openstreetmap.josm.tools.OsmPrimitiveImageProvider;
30import org.openstreetmap.josm.tools.Stopwatch;
31import org.openstreetmap.josm.tools.Utils;
32
33/**
34 * This class manages the list of available map paint styles and gives access to
35 * the ElemStyles singleton.
36 *
37 * On change, {@link MapPaintStylesUpdateListener#mapPaintStylesUpdated()} is fired
38 * for all listeners.
39 */
40public final class MapPaintStyles {
41
42 private static final Collection<String> DEPRECATED_IMAGE_NAMES = Arrays.asList(
43 "presets/misc/deprecated.svg",
44 "misc/deprecated.png");
45
46 private static final ListenerList<MapPaintStylesUpdateListener> listeners = ListenerList.createUnchecked();
47
48 private static final class MapPaintStylesPreferenceListener implements PreferenceChangedListener {
49 private final IPreferences pref;
50 /** Preferences to ignore (i.e., if they change, don't reload) */
51 private final List<String> preferenceIgnoreList;
52
53 MapPaintStylesPreferenceListener(IPreferences pref) {
54 this.pref = pref;
55 this.preferenceIgnoreList = Arrays.asList("mappaint.style.entries", "mappaint.style.known-defaults",
56 "mappaint.renderer-class-name");
57 }
58
59 @Override
60 public void preferenceChanged(PreferenceChangeEvent e) {
61 if (e.getKey().contains("mappaint") && !this.preferenceIgnoreList.contains(e.getKey())) {
62 // We need to remove this from the listeners, so that we don't recursively call ourselves.
63 pref.removePreferenceChangeListener(this);
64 MapPaintStyles.readFromPreferences();
65 pref.addPreferenceChangeListener(this);
66 }
67 }
68 }
69
70 static {
71 listeners.addListener(new MapPaintStylesUpdateListener() {
72 @Override
73 public void mapPaintStylesUpdated() {
74 SwingUtilities.invokeLater(styles::clearCached);
75 }
76
77 @Override
78 public void mapPaintStyleEntryUpdated(int index) {
79 mapPaintStylesUpdated();
80 }
81 });
82 Config.getPref().addPreferenceChangeListener(new MapPaintStylesPreferenceListener(Config.getPref()));
83 }
84
85 private static final ElemStyles styles = new ElemStyles();
86
87 /**
88 * Returns the {@link ElemStyles} singleton instance.
89 *
90 * The returned object is read only, any manipulation happens via one of
91 * the other wrapper methods in this class. ({@link #readFromPreferences},
92 * {@link #moveStyles}, ...)
93 * @return the {@code ElemStyles} singleton instance
94 */
95 public static ElemStyles getStyles() {
96 return styles;
97 }
98
99 private MapPaintStyles() {
100 // Hide default constructor for utils classes
101 }
102
103 /**
104 * Value holder for a reference to a tag name. A style instruction
105 * <pre>
106 * text: a_tag_name;
107 * </pre>
108 * results in a tag reference for the tag <code>a_tag_name</code> in the
109 * style cascade.
110 */
111 public static class TagKeyReference {
112 /**
113 * The tag name
114 */
115 public final String key;
116
117 /**
118 * Create a new {@link TagKeyReference}
119 * @param key The tag name
120 */
121 public TagKeyReference(String key) {
122 this.key = key.intern();
123 }
124
125 @Override
126 public String toString() {
127 return "TagKeyReference{" + "key='" + key + "'}";
128 }
129 }
130
131 /**
132 * IconReference is used to remember the associated style source for each icon URL.
133 * This is necessary because image URLs can be paths relative
134 * to the source file and we have cascading of properties from different source files.
135 */
136 public static class IconReference {
137
138 /**
139 * The name of the icon
140 */
141 public final String iconName;
142 /**
143 * The style source this reference occurred in
144 */
145 public final StyleSource source;
146
147 /**
148 * Create a new {@link IconReference}
149 * @param iconName The icon name
150 * @param source The current style source
151 */
152 public IconReference(String iconName, StyleSource source) {
153 this.iconName = iconName;
154 this.source = source;
155 }
156
157 @Override
158 public String toString() {
159 return "IconReference{" + "iconName='" + iconName + "' source='" + source.getDisplayString() + "'}";
160 }
161
162 /**
163 * Determines whether this icon represents a deprecated icon
164 * @return whether this icon represents a deprecated icon
165 * @since 10927
166 */
167 public boolean isDeprecatedIcon() {
168 return DEPRECATED_IMAGE_NAMES.contains(iconName);
169 }
170 }
171
172 /**
173 * Image provider for icon. Note that this is a provider only. A {@link ImageProvider#get()} call may still fail!
174 *
175 * @param ref reference to the requested icon
176 * @param test if <code>true</code> than the icon is request is tested
177 * @return image provider for icon (can be <code>null</code> when <code>test</code> is <code>true</code>).
178 * @see #getIcon(IconReference, int,int)
179 * @since 8097
180 */
181 public static ImageProvider getIconProvider(IconReference ref, boolean test) {
182 final String namespace = ref.source.getPrefName();
183 ImageProvider i = new ImageProvider(ref.iconName)
184 .setDirs(getIconSourceDirs(ref.source))
185 .setId("mappaint."+namespace)
186 .setArchive(ref.source.zipIcons)
187 .setInArchiveDir(ref.source.getZipEntryDirName())
188 .setOptional(true);
189 if (test && i.get() == null) {
190 String msg = "Mappaint style \""+namespace+"\" ("+ref.source.getDisplayString()+") icon \"" + ref.iconName + "\" not found.";
191 ref.source.logWarning(msg);
192 Logging.warn(msg);
193 return null;
194 }
195 return i;
196 }
197
198 /**
199 * Return scaled icon.
200 *
201 * @param ref reference to the requested icon
202 * @param width icon width or -1 for autoscale
203 * @param height icon height or -1 for autoscale
204 * @return image icon or <code>null</code>.
205 * @see #getIconProvider(IconReference, boolean)
206 */
207 public static ImageIcon getIcon(IconReference ref, int width, int height) {
208 final String namespace = ref.source.getPrefName();
209 ImageIcon i = getIconProvider(ref, false).setSize(width, height).get();
210 if (i == null) {
211 Logging.warn("Mappaint style \""+namespace+"\" ("+ref.source.getDisplayString()+") icon \"" + ref.iconName + "\" not found.");
212 return null;
213 }
214 return i;
215 }
216
217 /**
218 * No icon with the given name was found, show a dummy icon instead
219 * @param source style source
220 * @return the icon misc/no_icon.png, in descending priority:
221 * - relative to source file
222 * - from user icon paths
223 * - josm's default icon
224 * can be null if the defaults are turned off by user
225 */
226 public static ImageIcon getNoIconIcon(StyleSource source) {
227 return new ImageProvider("presets/misc/no_icon")
228 .setDirs(getIconSourceDirs(source))
229 .setId("mappaint."+source.getPrefName())
230 .setArchive(source.zipIcons)
231 .setInArchiveDir(source.getZipEntryDirName())
232 .setOptional(true).get();
233 }
234
235 /**
236 * Returns the node icon that would be displayed for the given tag.
237 * @param tag The tag to look an icon for
238 * @return {@code null} if no icon found
239 * @deprecated use {@link OsmPrimitiveImageProvider#getResource}
240 */
241 @Deprecated
242 public static ImageIcon getNodeIcon(Tag tag) {
243 if (tag != null) {
244 return OsmPrimitiveImageProvider.getResource(tag.getKey(), tag.getValue(), OsmPrimitiveType.NODE)
245 .map(resource -> resource.getPaddedIcon(ImageProvider.ImageSizes.SMALLICON.getImageDimension()))
246 .orElse(null);
247 }
248 return null;
249 }
250
251 /**
252 * Gets the directories that should be searched for icons
253 * @param source The style source the icon is from
254 * @return A list of directory names
255 */
256 public static List<String> getIconSourceDirs(StyleSource source) {
257 List<String> dirs = new LinkedList<>();
258
259 File sourceDir = source.getLocalSourceDir();
260 if (sourceDir != null) {
261 dirs.add(sourceDir.getPath());
262 }
263
264 Collection<String> prefIconDirs = Config.getPref().getList("mappaint.icon.sources");
265 for (String fileset : prefIconDirs) {
266 String[] a;
267 if (fileset.indexOf('=') >= 0) {
268 a = fileset.split("=", 2);
269 } else {
270 a = new String[] {"", fileset};
271 }
272
273 /* non-prefixed path is generic path, always take it */
274 if (a[0].isEmpty() || source.getPrefName().equals(a[0])) {
275 dirs.add(a[1]);
276 }
277 }
278
279 if (Config.getPref().getBoolean("mappaint.icon.enable-defaults", true)) {
280 /* don't prefix icon path, as it should be generic */
281 dirs.add("resource://images/");
282 }
283
284 return dirs;
285 }
286
287 /**
288 * Reloads all styles from the preferences.
289 */
290 public static void readFromPreferences() {
291 styles.clear();
292
293 Collection<? extends SourceEntry> sourceEntries = MapPaintPrefHelper.INSTANCE.get();
294
295 for (SourceEntry entry : sourceEntries) {
296 try {
297 styles.add(fromSourceEntry(entry));
298 } catch (IllegalArgumentException e) {
299 Logging.error("Failed to load map paint style {0}", entry);
300 Logging.error(e);
301 }
302 }
303 for (StyleSource source : styles.getStyleSources()) {
304 if (source.active) {
305 loadStyleForFirstTime(source);
306 } else {
307 source.loadStyleSource(true);
308 }
309 }
310 fireMapPaintStylesUpdated();
311 }
312
313 private static void loadStyleForFirstTime(StyleSource source) {
314 final Stopwatch stopwatch = Stopwatch.createStarted();
315 source.loadStyleSource();
316 if (Config.getPref().getBoolean("mappaint.auto_reload_local_styles", true) && source.isLocal()) {
317 try {
318 FileWatcher.getDefaultInstance().registerSource(source);
319 } catch (IOException | IllegalStateException | IllegalArgumentException e) {
320 Logging.error(e);
321 }
322 }
323 if (Logging.isDebugEnabled() || !source.isValid()) {
324 String message = stopwatch.toString("Initializing map style " + source.url);
325 if (!source.isValid()) {
326 Logging.warn(message + " (" + source.getErrors().size() + " errors, " + source.getWarnings().size() + " warnings)");
327 } else {
328 Logging.debug(message);
329 }
330 }
331 }
332
333 private static StyleSource fromSourceEntry(SourceEntry entry) {
334 if (entry.url == null && entry instanceof MapCSSStyleSource) {
335 return (MapCSSStyleSource) entry;
336 }
337 try (CachedFile cf = new CachedFile(entry.url).setHttpAccept(MapCSSStyleSource.MAPCSS_STYLE_MIME_TYPES)) {
338 String zipEntryPath = cf.findZipEntryPath("mapcss", "style");
339 if (zipEntryPath != null) {
340 entry.isZip = true;
341 entry.zipEntryPath = zipEntryPath;
342 }
343 return new MapCSSStyleSource(entry);
344 }
345 }
346
347 /**
348 * Move position of entries in the current list of StyleSources
349 * @param sel The indices of styles to be moved.
350 * @param delta The number of lines it should move. positive int moves
351 * down and negative moves up.
352 */
353 public static void moveStyles(int[] sel, int delta) {
354 if (!canMoveStyles(sel, delta))
355 return;
356 int[] selSorted = Utils.copyArray(sel);
357 Arrays.sort(selSorted);
358 List<StyleSource> data = new ArrayList<>(styles.getStyleSources());
359 for (int row: selSorted) {
360 StyleSource t1 = data.get(row);
361 StyleSource t2 = data.get(row + delta);
362 data.set(row, t2);
363 data.set(row + delta, t1);
364 }
365 styles.setStyleSources(data);
366 MapPaintPrefHelper.INSTANCE.put(data);
367 fireMapPaintStylesUpdated();
368 }
369
370 /**
371 * Check if the styles can be moved
372 * @param sel The indexes of the selected styles
373 * @param i The number of places to move the styles
374 * @return <code>true</code> if that movement is possible
375 */
376 public static boolean canMoveStyles(int[] sel, int i) {
377 if (sel.length == 0)
378 return false;
379 int[] selSorted = Utils.copyArray(sel);
380 Arrays.sort(selSorted);
381
382 if (i < 0) // Up
383 return selSorted[0] >= -i;
384 else if (i > 0) // Down
385 return selSorted[selSorted.length-1] <= styles.getStyleSources().size() - 1 - i;
386 else
387 return true;
388 }
389
390 /**
391 * Toggles the active state of several styles
392 * @param sel The style indexes
393 */
394 public static void toggleStyleActive(int... sel) {
395 List<StyleSource> data = styles.getStyleSources();
396 for (int p : sel) {
397 StyleSource s = data.get(p);
398 s.active = !s.active;
399 if (s.active && !s.isLoaded()) {
400 loadStyleForFirstTime(s);
401 }
402 }
403 MapPaintPrefHelper.INSTANCE.put(data);
404 if (sel.length == 1) {
405 fireMapPaintStyleEntryUpdated(sel[0]);
406 } else {
407 fireMapPaintStylesUpdated();
408 }
409 }
410
411 /**
412 * Add a new map paint style.
413 * @param entry map paint style
414 * @return loaded style source
415 */
416 public static StyleSource addStyle(SourceEntry entry) {
417 StyleSource source = fromSourceEntry(entry);
418 styles.add(source);
419 loadStyleForFirstTime(source);
420 refreshStyles();
421 return source;
422 }
423
424 /**
425 * Remove a map paint style.
426 * @param entry map paint style
427 * @since 11493
428 */
429 public static void removeStyle(SourceEntry entry) {
430 StyleSource source = fromSourceEntry(entry);
431 if (styles.remove(source)) {
432 refreshStyles();
433 }
434 }
435
436 private static void refreshStyles() {
437 MapPaintPrefHelper.INSTANCE.put(styles.getStyleSources());
438 fireMapPaintStylesUpdated();
439 }
440
441 /***********************************
442 * MapPaintStylesUpdateListener &amp; related code
443 * (get informed when the list of MapPaint StyleSources changes)
444 */
445 public interface MapPaintStylesUpdateListener {
446 /**
447 * Called on any style source changes that are not handled by {@link #mapPaintStyleEntryUpdated(int)}
448 */
449 void mapPaintStylesUpdated();
450
451 /**
452 * Called whenever a single style source entry was changed.
453 * @param index The index of the entry.
454 */
455 void mapPaintStyleEntryUpdated(int index);
456 }
457
458 /**
459 * Add a listener that listens to global style changes.
460 * @param listener The listener
461 */
462 public static void addMapPaintStylesUpdateListener(MapPaintStylesUpdateListener listener) {
463 listeners.addListener(listener);
464 }
465
466 /**
467 * Removes a listener that listens to global style changes.
468 * @param listener The listener
469 */
470 public static void removeMapPaintStylesUpdateListener(MapPaintStylesUpdateListener listener) {
471 listeners.removeListener(listener);
472 }
473
474 /**
475 * Notifies all listeners that there was any update to the map paint styles
476 */
477 public static void fireMapPaintStylesUpdated() {
478 listeners.fireEvent(MapPaintStylesUpdateListener::mapPaintStylesUpdated);
479 }
480
481 /**
482 * Notifies all listeners that there was an update to a specific map paint style
483 * @param index The style index
484 */
485 public static void fireMapPaintStyleEntryUpdated(int index) {
486 listeners.fireEvent(l -> l.mapPaintStyleEntryUpdated(index));
487 }
488}
Note: See TracBrowser for help on using the repository browser.