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

Last change on this file since 12378 was 12378, checked in by michael2402, 7 years ago

Document the gui.mappaint package

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