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

Last change on this file since 7447 was 7447, checked in by bastiK, 10 years ago

fixed #10425 - ConcurrentModificationException when auto update mappaint file

  • Property svn:eol-style set to native
File size: 15.4 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.SwingUtilities;
21
22import org.openstreetmap.josm.Main;
23import org.openstreetmap.josm.data.coor.LatLon;
24import org.openstreetmap.josm.data.osm.Node;
25import org.openstreetmap.josm.data.osm.Tag;
26import org.openstreetmap.josm.gui.PleaseWaitRunnable;
27import org.openstreetmap.josm.gui.mappaint.StyleCache.StyleList;
28import org.openstreetmap.josm.gui.mappaint.mapcss.MapCSSStyleSource;
29import org.openstreetmap.josm.gui.mappaint.xml.XmlStyleSource;
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 ElemStyles instance. The object you get with
39 * getStyles() is read only, any manipulation happens via one of
40 * the wrapper methods here. (readFromPreferences, moveStyles, ...)
41 *
42 * On change, mapPaintSylesUpdated() is fired for all listeners.
43 */
44public final class MapPaintStyles {
45
46 private static ElemStyles styles = new ElemStyles();
47
48 /**
49 * Returns the {@link ElemStyles} instance.
50 * @return the {@code ElemStyles} instance
51 */
52 public static ElemStyles getStyles() {
53 return styles;
54 }
55
56 private MapPaintStyles() {
57 // Hide default constructor for utils classes
58 }
59
60 /**
61 * Value holder for a reference to a tag name. A style instruction
62 * <pre>
63 * text: a_tag_name;
64 * </pre>
65 * results in a tag reference for the tag <tt>a_tag_name</tt> in the
66 * style cascade.
67 */
68 public static class TagKeyReference {
69 public final String key;
70 public TagKeyReference(String key) {
71 this.key = key;
72 }
73
74 @Override
75 public String toString() {
76 return "TagKeyReference{" + "key='" + key + "'}";
77 }
78 }
79
80 /**
81 * IconReference is used to remember the associated style source for
82 * each icon URL.
83 * This is necessary because image URLs can be paths relative
84 * to the source file and we have cascading of properties from different
85 * source files.
86 */
87 public static class IconReference {
88
89 public final String iconName;
90 public final StyleSource source;
91
92 public IconReference(String iconName, StyleSource source) {
93 this.iconName = iconName;
94 this.source = source;
95 }
96
97 @Override
98 public String toString() {
99 return "IconReference{" + "iconName='" + iconName + "' source='" + source.getDisplayString() + "'}";
100 }
101 }
102
103 public static ImageIcon getIcon(IconReference ref, int width, int height) {
104 final String namespace = ref.source.getPrefName();
105 ImageIcon i = new ImageProvider(ref.iconName)
106 .setDirs(getIconSourceDirs(ref.source))
107 .setId("mappaint."+namespace)
108 .setArchive(ref.source.zipIcons)
109 .setInArchiveDir(ref.source.getZipEntryDirName())
110 .setWidth(width)
111 .setHeight(height)
112 .setOptional(true).get();
113 if (i == null) {
114 Main.warn("Mappaint style \""+namespace+"\" ("+ref.source.getDisplayString()+") icon \"" + ref.iconName + "\" not found.");
115 return null;
116 }
117 return i;
118 }
119
120 /**
121 * No icon with the given name was found, show a dummy icon instead
122 * @return the icon misc/no_icon.png, in descending priority:
123 * - relative to source file
124 * - from user icon paths
125 * - josm's default icon
126 * can be null if the defaults are turned off by user
127 */
128 public static ImageIcon getNoIcon_Icon(StyleSource source) {
129 return new ImageProvider("misc/no_icon.png")
130 .setDirs(getIconSourceDirs(source))
131 .setId("mappaint."+source.getPrefName())
132 .setArchive(source.zipIcons)
133 .setInArchiveDir(source.getZipEntryDirName())
134 .setOptional(true).get();
135 }
136
137 public static ImageIcon getNodeIcon(Tag tag) {
138 return getNodeIcon(tag, true);
139 }
140
141 public static ImageIcon getNodeIcon(Tag tag, boolean includeDeprecatedIcon) {
142 if (tag != null) {
143 Node virtualNode = new Node(LatLon.ZERO);
144 virtualNode.put(tag.getKey(), tag.getValue());
145 StyleList styleList;
146 synchronized (MapCSSStyleSource.STYLE_SOURCE_LOCK) {
147 styleList = getStyles().generateStyles(virtualNode, 0.5, null, false).a;
148 }
149 if (styleList != null) {
150 for (ElemStyle style : styleList) {
151 if (style instanceof NodeElemStyle) {
152 MapImage mapImage = ((NodeElemStyle) style).mapImage;
153 if (mapImage != null) {
154 if (includeDeprecatedIcon || mapImage.name == null || !"misc/deprecated.png".equals(mapImage.name)) {
155 return new ImageIcon(mapImage.getDisplayedNodeIcon(false));
156 } else {
157 return null; // Deprecated icon found but not wanted
158 }
159 }
160 }
161 }
162 }
163 }
164 return null;
165 }
166
167 public static List<String> getIconSourceDirs(StyleSource source) {
168 List<String> dirs = new LinkedList<>();
169
170 File sourceDir = source.getLocalSourceDir();
171 if (sourceDir != null) {
172 dirs.add(sourceDir.getPath());
173 }
174
175 Collection<String> prefIconDirs = Main.pref.getCollection("mappaint.icon.sources");
176 for (String fileset : prefIconDirs) {
177 String[] a;
178 if(fileset.indexOf('=') >= 0) {
179 a = fileset.split("=", 2);
180 } else {
181 a = new String[] {"", fileset};
182 }
183
184 /* non-prefixed path is generic path, always take it */
185 if(a[0].length() == 0 || source.getPrefName().equals(a[0])) {
186 dirs.add(a[1]);
187 }
188 }
189
190 if (Main.pref.getBoolean("mappaint.icon.enable-defaults", true)) {
191 /* don't prefix icon path, as it should be generic */
192 dirs.add("resource://images/styles/standard/");
193 dirs.add("resource://images/styles/");
194 }
195
196 return dirs;
197 }
198
199 public static void readFromPreferences() {
200 styles.clear();
201
202 Collection<? extends SourceEntry> sourceEntries = MapPaintPrefHelper.INSTANCE.get();
203
204 for (SourceEntry entry : sourceEntries) {
205 StyleSource source = fromSourceEntry(entry);
206 if (source != null) {
207 styles.add(source);
208 }
209 }
210 for (StyleSource source : styles.getStyleSources()) {
211 loadStyleForFirstTime(source);
212 }
213 fireMapPaintSylesUpdated();
214 }
215
216 private static void loadStyleForFirstTime(StyleSource source) {
217 final long startTime = System.currentTimeMillis();
218 source.loadStyleSource();
219 if (Main.pref.getBoolean("mappaint.auto_reload_local_styles", true) && source.isLocal()) {
220 try {
221 Main.fileWatcher.registerStyleSource(source);
222 } catch (IOException e) {
223 Main.error(e);
224 }
225 }
226 if (Main.isDebugEnabled()) {
227 final long elapsedTime = System.currentTimeMillis() - startTime;
228 Main.debug("Initializing map style " + source.url + " completed in " + Utils.getDurationString(elapsedTime));
229 }
230 }
231
232 private static StyleSource fromSourceEntry(SourceEntry entry) {
233 CachedFile cf = null;
234 try {
235 Set<String> mimes = new HashSet<>();
236 mimes.addAll(Arrays.asList(XmlStyleSource.XML_STYLE_MIME_TYPES.split(", ")));
237 mimes.addAll(Arrays.asList(MapCSSStyleSource.MAPCSS_STYLE_MIME_TYPES.split(", ")));
238 cf = new CachedFile(entry.url).setHttpAccept(Utils.join(", ", mimes));
239 String zipEntryPath = cf.findZipEntryPath("mapcss", "style");
240 if (zipEntryPath != null) {
241 entry.isZip = true;
242 entry.zipEntryPath = zipEntryPath;
243 return new MapCSSStyleSource(entry);
244 }
245 zipEntryPath = cf.findZipEntryPath("xml", "style");
246 if (zipEntryPath != null)
247 return new XmlStyleSource(entry);
248 if (entry.url.toLowerCase().endsWith(".mapcss"))
249 return new MapCSSStyleSource(entry);
250 if (entry.url.toLowerCase().endsWith(".xml"))
251 return new XmlStyleSource(entry);
252 else {
253 try (InputStreamReader reader = new InputStreamReader(cf.getInputStream(), StandardCharsets.UTF_8)) {
254 WHILE: while (true) {
255 int c = reader.read();
256 switch (c) {
257 case -1:
258 break WHILE;
259 case ' ':
260 case '\t':
261 case '\n':
262 case '\r':
263 continue;
264 case '<':
265 return new XmlStyleSource(entry);
266 default:
267 return new MapCSSStyleSource(entry);
268 }
269 }
270 }
271 Main.warn("Could not detect style type. Using default (xml).");
272 return new XmlStyleSource(entry);
273 }
274 } catch (IOException e) {
275 Main.warn(tr("Failed to load Mappaint styles from ''{0}''. Exception was: {1}", entry.url, e.toString()));
276 Main.error(e);
277 }
278 return null;
279 }
280
281 /**
282 * reload styles
283 * preferences are the same, but the file source may have changed
284 * @param sel the indices of styles to reload
285 */
286 public static void reloadStyles(final int... sel) {
287 List<StyleSource> toReload = new ArrayList<>();
288 List<StyleSource> data = styles.getStyleSources();
289 for (int i : sel) {
290 toReload.add(data.get(i));
291 }
292 Main.worker.submit(new MapPaintStyleLoader(toReload));
293 }
294
295 public static class MapPaintStyleLoader extends PleaseWaitRunnable {
296 private boolean canceled;
297 private Collection<StyleSource> sources;
298
299 public MapPaintStyleLoader(Collection<StyleSource> sources) {
300 super(tr("Reloading style sources"));
301 this.sources = sources;
302 }
303
304 @Override
305 protected void cancel() {
306 canceled = true;
307 }
308
309 @Override
310 protected void finish() {
311 SwingUtilities.invokeLater(new Runnable() {
312 @Override
313 public void run() {
314 fireMapPaintSylesUpdated();
315 styles.clearCached();
316 if (Main.isDisplayingMapView()) {
317 Main.map.mapView.preferenceChanged(null);
318 Main.map.mapView.repaint();
319 }
320 }
321 });
322 }
323
324 @Override
325 protected void realRun() {
326 ProgressMonitor monitor = getProgressMonitor();
327 monitor.setTicksCount(sources.size());
328 for (StyleSource s : sources) {
329 if (canceled)
330 return;
331 monitor.subTask(tr("loading style ''{0}''...", s.getDisplayString()));
332 s.loadStyleSource();
333 monitor.worked(1);
334 }
335 }
336 }
337
338 /**
339 * Move position of entries in the current list of StyleSources
340 * @param sel The indices of styles to be moved.
341 * @param delta The number of lines it should move. positive int moves
342 * down and negative moves up.
343 */
344 public static void moveStyles(int[] sel, int delta) {
345 if (!canMoveStyles(sel, delta))
346 return;
347 int[] selSorted = Utils.copyArray(sel);
348 Arrays.sort(selSorted);
349 List<StyleSource> data = new ArrayList<>(styles.getStyleSources());
350 for (int row: selSorted) {
351 StyleSource t1 = data.get(row);
352 StyleSource t2 = data.get(row + delta);
353 data.set(row, t2);
354 data.set(row + delta, t1);
355 }
356 styles.setStyleSources(data);
357 MapPaintPrefHelper.INSTANCE.put(data);
358 fireMapPaintSylesUpdated();
359 styles.clearCached();
360 Main.map.mapView.repaint();
361 }
362
363 public static boolean canMoveStyles(int[] sel, int i) {
364 if (sel.length == 0)
365 return false;
366 int[] selSorted = Utils.copyArray(sel);
367 Arrays.sort(selSorted);
368
369 if (i < 0) // Up
370 return selSorted[0] >= -i;
371 else if (i > 0) // Down
372 return selSorted[selSorted.length-1] <= styles.getStyleSources().size() - 1 - i;
373 else
374 return true;
375 }
376
377 public static void toggleStyleActive(int... sel) {
378 List<StyleSource> data = styles.getStyleSources();
379 for (int p : sel) {
380 StyleSource s = data.get(p);
381 s.active = !s.active;
382 }
383 MapPaintPrefHelper.INSTANCE.put(data);
384 if (sel.length == 1) {
385 fireMapPaintStyleEntryUpdated(sel[0]);
386 } else {
387 fireMapPaintSylesUpdated();
388 }
389 styles.clearCached();
390 Main.map.mapView.repaint();
391 }
392
393 public static void addStyle(SourceEntry entry) {
394 StyleSource source = fromSourceEntry(entry);
395 if (source != null) {
396 styles.add(source);
397 loadStyleForFirstTime(source);
398 MapPaintPrefHelper.INSTANCE.put(styles.getStyleSources());
399 fireMapPaintSylesUpdated();
400 styles.clearCached();
401 Main.map.mapView.repaint();
402 }
403 }
404
405 /***********************************
406 * MapPaintSylesUpdateListener &amp; related code
407 * (get informed when the list of MapPaint StyleSources changes)
408 */
409
410 public interface MapPaintSylesUpdateListener {
411 public void mapPaintStylesUpdated();
412 public void mapPaintStyleEntryUpdated(int idx);
413 }
414
415 protected static final CopyOnWriteArrayList<MapPaintSylesUpdateListener> listeners
416 = new CopyOnWriteArrayList<>();
417
418 public static void addMapPaintSylesUpdateListener(MapPaintSylesUpdateListener listener) {
419 if (listener != null) {
420 listeners.addIfAbsent(listener);
421 }
422 }
423
424 public static void removeMapPaintSylesUpdateListener(MapPaintSylesUpdateListener listener) {
425 listeners.remove(listener);
426 }
427
428 public static void fireMapPaintSylesUpdated() {
429 for (MapPaintSylesUpdateListener l : listeners) {
430 l.mapPaintStylesUpdated();
431 }
432 }
433
434 public static void fireMapPaintStyleEntryUpdated(int idx) {
435 for (MapPaintSylesUpdateListener l : listeners) {
436 l.mapPaintStyleEntryUpdated(idx);
437 }
438 }
439}
Note: See TracBrowser for help on using the repository browser.