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

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

see #19334 - javadoc fixes + protected constructors for abstract classes

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