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

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

Update StyleCacheTest: Clear the style cache before each test. Add documentation.

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