Index: /applications/viewer/jmapviewer/src/org/openstreetmap/gui/jmapviewer/AttributionSupport.java
===================================================================
--- /applications/viewer/jmapviewer/src/org/openstreetmap/gui/jmapviewer/AttributionSupport.java	(revision 26783)
+++ /applications/viewer/jmapviewer/src/org/openstreetmap/gui/jmapviewer/AttributionSupport.java	(revision 26783)
@@ -0,0 +1,120 @@
+package org.openstreetmap.gui.jmapviewer;
+
+//License: GPL.
+
+import java.awt.Color;
+import java.awt.Font;
+import java.awt.Graphics;
+import java.awt.Image;
+import java.awt.Point;
+import java.awt.Rectangle;
+import java.awt.font.TextAttribute;
+import java.awt.geom.Rectangle2D;
+import java.awt.image.ImageObserver;
+import java.util.HashMap;
+
+import org.openstreetmap.gui.jmapviewer.interfaces.TileSource;
+
+public class AttributionSupport {
+
+    private TileSource tileSource;
+
+    private Image attrImage;
+    private String attrTermsUrl;
+    public static final Font ATTR_FONT = new Font("Arial", Font.PLAIN, 10);
+    public static final Font ATTR_LINK_FONT;
+
+    protected Rectangle attrTextBounds = null;
+    protected Rectangle attrToUBounds = null;
+    protected Rectangle attrImageBounds = null;
+
+    static {
+        HashMap<TextAttribute, Integer> aUnderline = new HashMap<TextAttribute, Integer>();
+        aUnderline.put(TextAttribute.UNDERLINE, TextAttribute.UNDERLINE_ON);
+        ATTR_LINK_FONT = ATTR_FONT.deriveFont(aUnderline);
+    }
+
+    public void initialize(TileSource tileSource) {
+        this.tileSource = tileSource;
+        boolean requireAttr = tileSource.requiresAttribution();
+        if (requireAttr) {
+            attrImage = tileSource.getAttributionImage();
+            attrTermsUrl = tileSource.getTermsOfUseURL();
+        } else {
+            attrImage = null;
+            attrTermsUrl = null;
+        }
+    }
+
+    public void paintAttribution(Graphics g, int width, int height, Coordinate topLeft, Coordinate bottomRight, int zoom, ImageObserver observer) {
+        if (!tileSource.requiresAttribution())
+            return;
+        // Draw attribution
+        Font font = g.getFont();
+        g.setFont(ATTR_LINK_FONT);
+
+        // Draw terms of use text
+        Rectangle2D termsStringBounds = g.getFontMetrics().getStringBounds("Background Terms of Use", g);
+        int textRealHeight = (int) termsStringBounds.getHeight();
+        int textHeight = textRealHeight - 5;
+        int textWidth = (int) termsStringBounds.getWidth();
+        int termsTextY = height - textHeight;
+        if (attrTermsUrl != null) {
+            int x = 2;
+            int y = height - textHeight;
+            attrToUBounds = new Rectangle(x, y-textHeight, textWidth, textRealHeight);
+            g.setColor(Color.black);
+            g.drawString("Background Terms of Use", x + 1, y + 1);
+            g.setColor(Color.white);
+            g.drawString("Background Terms of Use", x, y);
+        }
+
+        // Draw attribution logo
+        if (attrImage != null) {
+            int x = 2;
+            int imgWidth = attrImage.getWidth(observer);
+            int imgHeight = attrImage.getHeight(observer);
+            int y = termsTextY - imgHeight - textHeight - 5;
+            attrImageBounds = new Rectangle(x, y, imgWidth, imgHeight);
+            g.drawImage(attrImage, x, y, null);
+        }
+
+        g.setFont(ATTR_FONT);
+        String attributionText = tileSource.getAttributionText(zoom, topLeft, bottomRight);
+        if (attributionText != null) {
+            Rectangle2D stringBounds = g.getFontMetrics().getStringBounds(attributionText, g);
+            int x = width - (int) stringBounds.getWidth();
+            int y = height - textHeight;
+            g.setColor(Color.black);
+            g.drawString(attributionText, x + 1, y + 1);
+            g.setColor(Color.white);
+            g.drawString(attributionText, x, y);
+            attrTextBounds = new Rectangle(x, y-textHeight, textWidth, textRealHeight);
+        }
+
+        g.setFont(font);
+    }
+
+    public boolean handleAttribution(Point p, boolean click) {
+        if (!tileSource.requiresAttribution())
+            return false;
+
+        /* TODO: Somehow indicate the link is clickable state to user */
+
+        if ((attrImageBounds != null && attrImageBounds.contains(p))
+                || (attrTextBounds != null && attrTextBounds.contains(p))) {
+            if (click) {
+                FeatureAdapter.openLink(tileSource.getAttributionLinkURL());
+            }
+            return true;
+        } else if (attrToUBounds != null && attrToUBounds.contains(p)) {
+            if (click) {
+                FeatureAdapter.openLink(tileSource.getTermsOfUseURL());
+            }
+            return true;
+        }
+        return false;
+    }
+
+}
+
Index: /applications/viewer/jmapviewer/src/org/openstreetmap/gui/jmapviewer/FeatureAdapter.java
===================================================================
--- /applications/viewer/jmapviewer/src/org/openstreetmap/gui/jmapviewer/FeatureAdapter.java	(revision 26783)
+++ /applications/viewer/jmapviewer/src/org/openstreetmap/gui/jmapviewer/FeatureAdapter.java	(revision 26783)
@@ -0,0 +1,64 @@
+package org.openstreetmap.gui.jmapviewer;
+
+//License: GPL.
+
+import java.awt.Desktop;
+import java.io.IOException;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.text.MessageFormat;
+
+public class FeatureAdapter {
+
+    public static interface BrowserAdapter {
+        void openLink(String url);
+    }
+
+    public static interface TranslationAdapter {
+        String tr(String text, Object... objects);
+        // TODO: more i18n functions
+    }
+
+    private static BrowserAdapter browserAdapter = new DefaultBrowserAdapter();
+    private static TranslationAdapter translationAdapter = new DefaultTranslationAdapter();
+
+    public static void registerBrowserAdapter(BrowserAdapter browserAdapter) {
+        FeatureAdapter.browserAdapter = browserAdapter;
+    }
+
+    public static void registerTranslationAdapter(TranslationAdapter translationAdapter) {
+        FeatureAdapter.translationAdapter = translationAdapter;
+    }
+
+    public static void openLink(String url) {
+        browserAdapter.openLink(url);
+    }
+
+    public static String tr(String text, Object... objects) {
+        return translationAdapter.tr(text, objects);
+    }
+
+    public static class DefaultBrowserAdapter implements BrowserAdapter {
+        @Override
+        public void openLink(String url) {
+            if (Desktop.isDesktopSupported() && Desktop.getDesktop().isSupported(Desktop.Action.BROWSE)) {
+                try {
+                    Desktop.getDesktop().browse(new URI(url));
+                } catch (IOException e) {
+                    e.printStackTrace();
+                } catch (URISyntaxException e) {
+                    e.printStackTrace();
+                }
+            } else {
+                System.err.println(tr("Opening link not supported on current platform (''{0}'')", url));
+            }
+        }
+    }
+
+    public static class DefaultTranslationAdapter implements TranslationAdapter {
+        @Override
+        public String tr(String text, Object... objects) {
+            return MessageFormat.format(text, objects);
+        }
+    }
+}
Index: /applications/viewer/jmapviewer/src/org/openstreetmap/gui/jmapviewer/JMapViewer.java
===================================================================
--- /applications/viewer/jmapviewer/src/org/openstreetmap/gui/jmapviewer/JMapViewer.java	(revision 26782)
+++ /applications/viewer/jmapviewer/src/org/openstreetmap/gui/jmapviewer/JMapViewer.java	(revision 26783)
@@ -89,19 +89,5 @@
     private TileSource tileSource;
 
-    // Attribution
-    private Image attrImage;
-    private String attrTermsUrl;
-    public static final Font ATTR_FONT = new Font("Arial", Font.PLAIN, 10);
-    public static final Font ATTR_LINK_FONT;
-
-    protected Rectangle attrTextBounds = null;
-    protected Rectangle attrToUBounds = null;
-    protected Rectangle attrImageBounds = null;
-
-    static {
-        HashMap<TextAttribute, Integer> aUnderline = new HashMap<TextAttribute, Integer>();
-        aUnderline.put(TextAttribute.UNDERLINE, TextAttribute.UNDERLINE_ON);
-        ATTR_LINK_FONT = ATTR_FONT.deriveFont(aUnderline);
-    }
+    protected AttributionSupport attribution = new AttributionSupport();
 
     /**
@@ -584,5 +570,5 @@
         }
 
-        paintAttribution(g);
+        attribution.paintAttribution(g, getWidth(), getHeight(), getPosition(0, 0), getPosition(getWidth(), getHeight()), zoom, this);
     }
 
@@ -832,12 +818,6 @@
             setZoom(tileSource.getMaxZoom());
         }
-        boolean requireAttr = tileSource.requiresAttribution();
-        if (requireAttr) {
-            attrImage = tileSource.getAttributionImage();
-            attrTermsUrl = tileSource.getTermsOfUseURL();
-        } else {
-            attrImage = null;
-            attrTermsUrl = null;
-        }
+
+        attribution.initialize(tileSource);
         repaint();
     }
@@ -894,54 +874,4 @@
     }
 
-    private void paintAttribution(Graphics g) {
-        if (!tileSource.requiresAttribution())
-            return;
-        // Draw attribution
-        Font font = g.getFont();
-        g.setFont(ATTR_LINK_FONT);
-
-        Rectangle2D termsStringBounds = g.getFontMetrics().getStringBounds("Background Terms of Use", g);
-        int textRealHeight = (int) termsStringBounds.getHeight();
-        int textHeight = textRealHeight - 5;
-        int textWidth = (int) termsStringBounds.getWidth();
-        int termsTextY = getHeight() - textHeight;
-        if (attrTermsUrl != null) {
-            int x = 2;
-            int y = getHeight() - textHeight;
-            attrToUBounds = new Rectangle(x, y-textHeight, textWidth, textRealHeight);
-            g.setColor(Color.black);
-            g.drawString("Background Terms of Use", x + 1, y + 1);
-            g.setColor(Color.white);
-            g.drawString("Background Terms of Use", x, y);
-        }
-
-        // Draw attribution logo
-        if (attrImage != null) {
-            int x = 2;
-            int imgWidth = attrImage.getWidth(this);
-            int height = attrImage.getHeight(null);
-            int y = termsTextY - height - textHeight - 5;
-            attrImageBounds = new Rectangle(x, y, imgWidth, height);
-            g.drawImage(attrImage, x, y, null);
-        }
-
-        g.setFont(ATTR_FONT);
-        Coordinate topLeft = getPosition(0, 0);
-        Coordinate bottomRight = getPosition(getWidth(), getHeight());
-        String attributionText = tileSource.getAttributionText(zoom, topLeft, bottomRight);
-        if (attributionText != null) {
-            Rectangle2D stringBounds = g.getFontMetrics().getStringBounds(attributionText, g);
-            int x = getWidth() - (int) stringBounds.getWidth();
-            int y = getHeight() - textHeight;
-            g.setColor(Color.black);
-            g.drawString(attributionText, x + 1, y + 1);
-            g.setColor(Color.white);
-            g.drawString(attributionText, x, y);
-            attrTextBounds = new Rectangle(x, y-textHeight, textWidth, textRealHeight);
-        }
-
-        g.setFont(font);
-    }
-
     protected EventListenerList listenerList = new EventListenerList();
 
@@ -962,5 +892,5 @@
     /**
      * Send an update to all objects registered with viewer
-     * 
+     *
      * @param event to dispatch
      */
