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

Last change on this file since 11247 was 11137, checked in by bastiK, 8 years ago

javadoc

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