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

Last change on this file since 8256 was 8097, checked in by stoecker, 9 years ago

see #10684, see #10688 - fix image scaling for mappaint

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