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

Last change on this file since 11610 was 11589, checked in by Don-vip, 7 years ago

fix #14391 - robustness to Linux systems preventing file watcher to start because of incorrect kernel inotify watch configuration

  • Property svn:eol-style set to native
File size: 16.9 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 public static void readFromPreferences() {
257 styles.clear();
258
259 Collection<? extends SourceEntry> sourceEntries = MapPaintPrefHelper.INSTANCE.get();
260
261 for (SourceEntry entry : sourceEntries) {
262 styles.add(fromSourceEntry(entry));
263 }
264 for (StyleSource source : styles.getStyleSources()) {
265 loadStyleForFirstTime(source);
266 }
267 fireMapPaintSylesUpdated();
268 }
269
270 private static void loadStyleForFirstTime(StyleSource source) {
271 final long startTime = System.currentTimeMillis();
272 source.loadStyleSource();
273 if (Main.pref.getBoolean("mappaint.auto_reload_local_styles", true) && source.isLocal()) {
274 try {
275 Main.fileWatcher.registerStyleSource(source);
276 } catch (IOException | IllegalStateException e) {
277 Main.error(e);
278 }
279 }
280 if (Main.isDebugEnabled() || !source.isValid()) {
281 final long elapsedTime = System.currentTimeMillis() - startTime;
282 String message = "Initializing map style " + source.url + " completed in " + Utils.getDurationString(elapsedTime);
283 if (!source.isValid()) {
284 Main.warn(message + " (" + source.getErrors().size() + " errors, " + source.getWarnings().size() + " warnings)");
285 } else {
286 Main.debug(message);
287 }
288 }
289 }
290
291 private static StyleSource fromSourceEntry(SourceEntry entry) {
292 if (entry.url == null && entry instanceof MapCSSStyleSource) {
293 return (MapCSSStyleSource) entry;
294 }
295 Set<String> mimes = new HashSet<>(Arrays.asList(MapCSSStyleSource.MAPCSS_STYLE_MIME_TYPES.split(", ")));
296 try (CachedFile cf = new CachedFile(entry.url).setHttpAccept(Utils.join(", ", mimes))) {
297 String zipEntryPath = cf.findZipEntryPath("mapcss", "style");
298 if (zipEntryPath != null) {
299 entry.isZip = true;
300 entry.zipEntryPath = zipEntryPath;
301 }
302 return new MapCSSStyleSource(entry);
303 }
304 }
305
306 /**
307 * reload styles
308 * preferences are the same, but the file source may have changed
309 * @param sel the indices of styles to reload
310 */
311 public static void reloadStyles(final int... sel) {
312 List<StyleSource> toReload = new ArrayList<>();
313 List<StyleSource> data = styles.getStyleSources();
314 for (int i : sel) {
315 toReload.add(data.get(i));
316 }
317 Main.worker.submit(new MapPaintStyleLoader(toReload));
318 }
319
320 public static class MapPaintStyleLoader extends PleaseWaitRunnable {
321 private boolean canceled;
322 private final Collection<StyleSource> sources;
323
324 public MapPaintStyleLoader(Collection<StyleSource> sources) {
325 super(tr("Reloading style sources"));
326 this.sources = sources;
327 }
328
329 @Override
330 protected void cancel() {
331 canceled = true;
332 }
333
334 @Override
335 protected void finish() {
336 SwingUtilities.invokeLater(() -> {
337 fireMapPaintSylesUpdated();
338 styles.clearCached();
339 if (Main.isDisplayingMapView()) {
340 Main.map.mapView.preferenceChanged(null);
341 Main.map.mapView.repaint();
342 }
343 });
344 }
345
346 @Override
347 protected void realRun() {
348 ProgressMonitor monitor = getProgressMonitor();
349 monitor.setTicksCount(sources.size());
350 for (StyleSource s : sources) {
351 if (canceled)
352 return;
353 monitor.subTask(tr("loading style ''{0}''...", s.getDisplayString()));
354 s.loadStyleSource();
355 monitor.worked(1);
356 }
357 }
358 }
359
360 /**
361 * Move position of entries in the current list of StyleSources
362 * @param sel The indices of styles to be moved.
363 * @param delta The number of lines it should move. positive int moves
364 * down and negative moves up.
365 */
366 public static void moveStyles(int[] sel, int delta) {
367 if (!canMoveStyles(sel, delta))
368 return;
369 int[] selSorted = Utils.copyArray(sel);
370 Arrays.sort(selSorted);
371 List<StyleSource> data = new ArrayList<>(styles.getStyleSources());
372 for (int row: selSorted) {
373 StyleSource t1 = data.get(row);
374 StyleSource t2 = data.get(row + delta);
375 data.set(row, t2);
376 data.set(row + delta, t1);
377 }
378 styles.setStyleSources(data);
379 MapPaintPrefHelper.INSTANCE.put(data);
380 fireMapPaintSylesUpdated();
381 styles.clearCached();
382 Main.map.mapView.repaint();
383 }
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 public static void toggleStyleActive(int... sel) {
400 List<StyleSource> data = styles.getStyleSources();
401 for (int p : sel) {
402 StyleSource s = data.get(p);
403 s.active = !s.active;
404 }
405 MapPaintPrefHelper.INSTANCE.put(data);
406 if (sel.length == 1) {
407 fireMapPaintStyleEntryUpdated(sel[0]);
408 } else {
409 fireMapPaintSylesUpdated();
410 }
411 styles.clearCached();
412 Main.map.mapView.repaint();
413 }
414
415 /**
416 * Add a new map paint style.
417 * @param entry map paint style
418 * @return loaded style source, or {@code null}
419 */
420 public static StyleSource addStyle(SourceEntry entry) {
421 StyleSource source = fromSourceEntry(entry);
422 styles.add(source);
423 loadStyleForFirstTime(source);
424 refreshStyles();
425 return source;
426 }
427
428 /**
429 * Remove a map paint style.
430 * @param entry map paint style
431 * @since 11493
432 */
433 public static void removeStyle(SourceEntry entry) {
434 StyleSource source = fromSourceEntry(entry);
435 if (styles.remove(source)) {
436 refreshStyles();
437 }
438 }
439
440 private static void refreshStyles() {
441 MapPaintPrefHelper.INSTANCE.put(styles.getStyleSources());
442 fireMapPaintSylesUpdated();
443 styles.clearCached();
444 if (Main.isDisplayingMapView()) {
445 Main.map.mapView.repaint();
446 }
447 }
448
449 /***********************************
450 * MapPaintSylesUpdateListener &amp; related code
451 * (get informed when the list of MapPaint StyleSources changes)
452 */
453
454 public interface MapPaintSylesUpdateListener {
455 void mapPaintStylesUpdated();
456
457 void mapPaintStyleEntryUpdated(int idx);
458 }
459
460 private static final CopyOnWriteArrayList<MapPaintSylesUpdateListener> listeners
461 = new CopyOnWriteArrayList<>();
462
463 public static void addMapPaintSylesUpdateListener(MapPaintSylesUpdateListener listener) {
464 if (listener != null) {
465 listeners.addIfAbsent(listener);
466 }
467 }
468
469 public static void removeMapPaintSylesUpdateListener(MapPaintSylesUpdateListener listener) {
470 listeners.remove(listener);
471 }
472
473 public static void fireMapPaintSylesUpdated() {
474 for (MapPaintSylesUpdateListener l : listeners) {
475 l.mapPaintStylesUpdated();
476 }
477 }
478
479 public static void fireMapPaintStyleEntryUpdated(int idx) {
480 for (MapPaintSylesUpdateListener l : listeners) {
481 l.mapPaintStyleEntryUpdated(idx);
482 }
483 }
484}
Note: See TracBrowser for help on using the repository browser.