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