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

Last change on this file since 13691 was 13493, checked in by Don-vip, 6 years ago

see #11924, see #15560, see #16048 - tt HTML tag is deprecated in HTML5: use code instead

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