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

Last change on this file since 10566 was 10566, checked in by stoecker, 8 years ago

see #13084 - fix some errors in icon move

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