001// License: GPL. For details, see LICENSE file. 002package org.openstreetmap.josm.data.osm.visitor.paint; 003 004import static org.openstreetmap.josm.tools.I18n.tr; 005 006import java.awt.Graphics2D; 007import java.lang.reflect.Constructor; 008import java.text.MessageFormat; 009import java.util.ArrayList; 010import java.util.Collections; 011import java.util.List; 012 013import org.openstreetmap.josm.gui.NavigatableComponent; 014import org.openstreetmap.josm.plugins.PluginHandler; 015import org.openstreetmap.josm.spi.preferences.Config; 016import org.openstreetmap.josm.tools.CheckParameterUtil; 017import org.openstreetmap.josm.tools.Logging; 018 019/** 020 * <p>MapRendererFactory manages a list of map renderer classes and associated 021 * meta data (display name, description).</p> 022 * 023 * <p>Plugins can implement and supply their own map renderers.</p> 024 * <strong>Sample code in a plugin</strong> 025 * <pre> 026 * public class MyMapRenderer extends AbstractMapRenderer { 027 * // .... 028 * } 029 * 030 * // to be called when the plugin is created 031 * MapRendererFactory factory = MapRendererFactory.getInstance(); 032 * factory.register(MyMapRenderer.class, "My map renderer", "This is is a fast map renderer"); 033 * factory.activate(MyMapRenderer.class); 034 * 035 * </pre> 036 * @since 4087 037 */ 038public final class MapRendererFactory { 039 040 /** preference key for the renderer class name. Default: class name for {@link StyledMapRenderer} 041 * 042 */ 043 public static final String PREF_KEY_RENDERER_CLASS_NAME = "mappaint.renderer-class-name"; 044 045 /** 046 * An exception thrown while creating a map renderer 047 */ 048 public static class MapRendererFactoryException extends RuntimeException { 049 050 /** 051 * Create a new {@link MapRendererFactoryException} 052 * @param message The message 053 * @param cause The cause 054 */ 055 public MapRendererFactoryException(String message, Throwable cause) { 056 super(message, cause); 057 } 058 059 /** 060 * Create a new {@link MapRendererFactoryException} 061 * @param message The message 062 */ 063 public MapRendererFactoryException(String message) { 064 super(message); 065 } 066 067 /** 068 * Create a new {@link MapRendererFactoryException} 069 * @param cause The cause 070 */ 071 public MapRendererFactoryException(Throwable cause) { 072 super(cause); 073 } 074 } 075 076 /** 077 * A description of a possible renderer for the map 078 */ 079 public static class Descriptor { 080 private final Class<? extends AbstractMapRenderer> renderer; 081 private final String displayName; 082 private final String description; 083 084 /** 085 * Creates a new map renderer description 086 * @param renderer The renderer 087 * @param displayName The display name for the renderer 088 * @param description The longer description that should be displayed to the user. 089 */ 090 public Descriptor(Class<? extends AbstractMapRenderer> renderer, String displayName, String description) { 091 this.renderer = renderer; 092 this.displayName = displayName; 093 this.description = description; 094 } 095 096 /** 097 * Get the class of the renderer 098 * @return The class 099 */ 100 public Class<? extends AbstractMapRenderer> getRenderer() { 101 return renderer; 102 } 103 104 /** 105 * Get the display name 106 * @return The name 107 */ 108 public String getDisplayName() { 109 return displayName; 110 } 111 112 /** 113 * Get the description 114 * @return The description 115 */ 116 public String getDescription() { 117 return description; 118 } 119 } 120 121 private static MapRendererFactory instance; 122 123 /** 124 * Replies the unique instance 125 * @return instance of map rending class 126 */ 127 public static synchronized MapRendererFactory getInstance() { 128 if (instance == null) { 129 instance = new MapRendererFactory(); 130 } 131 return instance; 132 } 133 134 private static Class<?> loadRendererClass(String className) { 135 for (ClassLoader cl : PluginHandler.getResourceClassLoaders()) { 136 try { 137 return Class.forName(className, true, cl); 138 } catch (final NoClassDefFoundError | ClassNotFoundException e) { 139 Logging.trace(e); 140 } 141 } 142 Logging.error(tr("Failed to load map renderer class ''{0}''. The class wasn''t found.", className)); 143 return null; 144 } 145 146 private final List<Descriptor> descriptors = new ArrayList<>(); 147 private Class<? extends AbstractMapRenderer> activeRenderer; 148 149 private MapRendererFactory() { 150 registerDefaultRenderers(); 151 String rendererClassName = Config.getPref().get(PREF_KEY_RENDERER_CLASS_NAME, null); 152 if (rendererClassName != null) { 153 activateMapRenderer(rendererClassName); 154 } else { 155 activateDefault(); 156 } 157 } 158 159 private void activateMapRenderer(String rendererClassName) { 160 Class<?> c = loadRendererClass(rendererClassName); 161 if (c == null) { 162 Logging.error(tr("Can''t activate map renderer class ''{0}'', because the class wasn''t found.", rendererClassName)); 163 Logging.error(tr("Activating the standard map renderer instead.")); 164 activateDefault(); 165 } else if (!AbstractMapRenderer.class.isAssignableFrom(c)) { 166 Logging.error(tr("Can''t activate map renderer class ''{0}'', because it isn''t a subclass of ''{1}''.", 167 rendererClassName, AbstractMapRenderer.class.getName())); 168 Logging.error(tr("Activating the standard map renderer instead.")); 169 activateDefault(); 170 } else { 171 Class<? extends AbstractMapRenderer> renderer = c.asSubclass(AbstractMapRenderer.class); 172 if (!isRegistered(renderer)) { 173 Logging.error(tr("Can''t activate map renderer class ''{0}'', because it isn''t registered as map renderer.", 174 rendererClassName)); 175 Logging.error(tr("Activating the standard map renderer instead.")); 176 activateDefault(); 177 } else { 178 activate(renderer); 179 } 180 } 181 } 182 183 private void registerDefaultRenderers() { 184 register( 185 WireframeMapRenderer.class, 186 tr("Wireframe Map Renderer"), 187 tr("Renders the map as simple wire frame.") 188 ); 189 register( 190 StyledMapRenderer.class, 191 tr("Styled Map Renderer"), 192 tr("Renders the map using style rules in a set of style sheets.") 193 ); 194 } 195 196 /** 197 * <p>Replies true, if {@code Renderer} is already a registered map renderer class.</p> 198 * 199 * @param renderer the map renderer class. Must not be null. 200 * @return true, if {@code Renderer} is already a registered map renderer class 201 * @throws IllegalArgumentException if {@code renderer} is null 202 */ 203 public boolean isRegistered(Class<? extends AbstractMapRenderer> renderer) { 204 CheckParameterUtil.ensureParameterNotNull(renderer); 205 return descriptors.stream().anyMatch(d -> d.getRenderer().equals(renderer)); 206 } 207 208 /** 209 * <p>Registers a map renderer class.</p> 210 * 211 * @param renderer the map renderer class. Must not be null. 212 * @param displayName the display name to be displayed in UIs (i.e. in the preference dialog) 213 * @param description the description 214 * @throws IllegalArgumentException if {@code renderer} is null 215 * @throws IllegalStateException if {@code renderer} is already registered 216 */ 217 public void register(Class<? extends AbstractMapRenderer> renderer, String displayName, String description) { 218 CheckParameterUtil.ensureParameterNotNull(renderer); 219 if (isRegistered(renderer)) 220 throw new IllegalStateException( 221 // no I18n - this is a technical message 222 MessageFormat.format("Class ''{0}'' already registered a renderer", renderer.getName()) 223 ); 224 Descriptor d = new Descriptor(renderer, displayName, description); 225 descriptors.add(d); 226 } 227 228 /** 229 * <p>Unregisters a map renderer class.</p> 230 * 231 * <p>If the respective class is also the active renderer, the renderer is reset 232 * to the default renderer.</p> 233 * 234 * @param renderer the map renderer class. Must not be null. 235 * 236 */ 237 public void unregister(Class<? extends AbstractMapRenderer> renderer) { 238 if (renderer == null) return; 239 if (!isRegistered(renderer)) return; 240 descriptors.removeIf(d -> d.getRenderer().equals(renderer)); 241 if (activeRenderer != null && activeRenderer.equals(renderer)) { 242 activateDefault(); 243 } 244 } 245 246 /** 247 * <p>Activates a map renderer class.</p> 248 * 249 * <p>The renderer class must already be registered.</p> 250 * 251 * @param renderer the map renderer class. Must not be null. 252 * @throws IllegalArgumentException if {@code renderer} is null 253 * @throws IllegalStateException if {@code renderer} isn't registered yet 254 */ 255 public void activate(Class<? extends AbstractMapRenderer> renderer) { 256 CheckParameterUtil.ensureParameterNotNull(renderer); 257 if (!isRegistered(renderer)) 258 throw new IllegalStateException( 259 // no I18n required 260 MessageFormat.format("Class ''{0}'' not registered as renderer. Can''t activate it.", renderer.getName()) 261 ); 262 this.activeRenderer = renderer; 263 Config.getPref().put(PREF_KEY_RENDERER_CLASS_NAME, activeRenderer.getName()); 264 265 } 266 267 /** 268 * <p>Activates the default map renderer.</p> 269 * 270 * @throws IllegalStateException if the default renderer {@link StyledMapRenderer} isn't registered 271 */ 272 public void activateDefault() { 273 Class<? extends AbstractMapRenderer> defaultRenderer = StyledMapRenderer.class; 274 if (!isRegistered(defaultRenderer)) 275 throw new IllegalStateException( 276 MessageFormat.format("Class ''{0}'' not registered as renderer. Can''t activate default renderer.", 277 defaultRenderer.getName()) 278 ); 279 activate(defaultRenderer); 280 } 281 282 /** 283 * <p>Creates an instance of the currently active renderer.</p> 284 * @param g Graphics 285 * @param viewport Navigable component 286 * @param isInactiveMode {@code true} if the paint visitor shall render OSM objects such that they look inactive 287 * @return an instance of the currently active renderer 288 * 289 * @throws MapRendererFactoryException if creating an instance fails 290 * @see AbstractMapRenderer#AbstractMapRenderer(Graphics2D, NavigatableComponent, boolean) 291 */ 292 public AbstractMapRenderer createActiveRenderer(Graphics2D g, NavigatableComponent viewport, boolean isInactiveMode) { 293 try { 294 Constructor<?> c = activeRenderer.getConstructor(Graphics2D.class, NavigatableComponent.class, boolean.class); 295 return AbstractMapRenderer.class.cast(c.newInstance(g, viewport, isInactiveMode)); 296 } catch (ReflectiveOperationException | IllegalArgumentException e) { 297 throw new MapRendererFactoryException(e); 298 } 299 } 300 301 /** 302 * <p>Replies the (unmodifiable) list of map renderer descriptors.</p> 303 * 304 * @return the descriptors 305 */ 306 public List<Descriptor> getMapRendererDescriptors() { 307 return Collections.unmodifiableList(descriptors); 308 } 309 310 /** 311 * <p>Replies true, if currently the wireframe map renderer is active. Otherwise, false.</p> 312 * 313 * <p>There is a specific method for {@link WireframeMapRenderer} for legacy support. 314 * Until 03/2011 there were only two possible map renderers in JOSM: the wireframe 315 * renderer and the styled renderer. For the time being there are still UI elements 316 * (menu entries, etc.) which toggle between these two renderers only.</p> 317 * 318 * @return true, if currently the wireframe map renderer is active. Otherwise, false 319 */ 320 public boolean isWireframeMapRendererActive() { 321 return WireframeMapRenderer.class.equals(activeRenderer); 322 } 323}