001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.gui.layer;
003
004import static org.openstreetmap.josm.tools.I18n.marktr;
005import static org.openstreetmap.josm.tools.I18n.tr;
006import static org.openstreetmap.josm.tools.I18n.trc;
007
008import java.awt.Color;
009import java.awt.Component;
010import java.awt.Font;
011import java.awt.Graphics2D;
012import java.awt.GridBagLayout;
013import java.awt.event.ActionEvent;
014import java.awt.font.FontRenderContext;
015import java.awt.font.LineBreakMeasurer;
016import java.awt.font.TextAttribute;
017import java.awt.font.TextLayout;
018import java.awt.image.BufferedImage;
019import java.awt.image.BufferedImageOp;
020import java.awt.image.ConvolveOp;
021import java.awt.image.Kernel;
022import java.text.AttributedCharacterIterator;
023import java.text.AttributedString;
024import java.util.Hashtable;
025import java.util.List;
026import java.util.Map;
027
028import javax.swing.AbstractAction;
029import javax.swing.Icon;
030import javax.swing.JCheckBoxMenuItem;
031import javax.swing.JComponent;
032import javax.swing.JLabel;
033import javax.swing.JMenu;
034import javax.swing.JMenuItem;
035import javax.swing.JPanel;
036import javax.swing.JPopupMenu;
037import javax.swing.JSeparator;
038
039import org.openstreetmap.josm.Main;
040import org.openstreetmap.josm.actions.ImageryAdjustAction;
041import org.openstreetmap.josm.data.ProjectionBounds;
042import org.openstreetmap.josm.data.imagery.ImageryInfo;
043import org.openstreetmap.josm.data.imagery.ImageryInfo.ImageryType;
044import org.openstreetmap.josm.data.imagery.OffsetBookmark;
045import org.openstreetmap.josm.data.preferences.ColorProperty;
046import org.openstreetmap.josm.data.preferences.IntegerProperty;
047import org.openstreetmap.josm.gui.MenuScroller;
048import org.openstreetmap.josm.gui.widgets.UrlLabel;
049import org.openstreetmap.josm.tools.GBC;
050import org.openstreetmap.josm.tools.ImageProvider;
051
052public abstract class ImageryLayer extends Layer {
053
054    public static final ColorProperty PROP_FADE_COLOR = new ColorProperty(marktr("Imagery fade"), Color.white);
055    public static final IntegerProperty PROP_FADE_AMOUNT = new IntegerProperty("imagery.fade_amount", 0);
056    public static final IntegerProperty PROP_SHARPEN_LEVEL = new IntegerProperty("imagery.sharpen_level", 0);
057
058    public static Color getFadeColor() {
059        return PROP_FADE_COLOR.get();
060    }
061
062    public static Color getFadeColorWithAlpha() {
063        Color c = PROP_FADE_COLOR.get();
064        return new Color(c.getRed(),c.getGreen(),c.getBlue(),PROP_FADE_AMOUNT.get()*255/100);
065    }
066
067    protected final ImageryInfo info;
068
069    protected Icon icon;
070
071    protected double dx = 0.0;
072    protected double dy = 0.0;
073
074    protected int sharpenLevel;
075
076    private final ImageryAdjustAction adjustAction = new ImageryAdjustAction(this);
077
078    /**
079     * Constructs a new {@code ImageryLayer}.
080     */
081    public ImageryLayer(ImageryInfo info) {
082        super(info.getName());
083        this.info = info;
084        if (info.getIcon() != null) {
085            icon = new ImageProvider(info.getIcon()).setOptional(true).
086                    setMaxHeight(ICON_SIZE).setMaxWidth(ICON_SIZE).get();
087        }
088        if (icon == null) {
089            icon = ImageProvider.get("imagery_small");
090        }
091        this.sharpenLevel = PROP_SHARPEN_LEVEL.get();
092    }
093
094    public double getPPD() {
095        if (!Main.isDisplayingMapView()) return Main.getProjection().getDefaultZoomInPPD();
096        ProjectionBounds bounds = Main.map.mapView.getProjectionBounds();
097        return Main.map.mapView.getWidth() / (bounds.maxEast - bounds.minEast);
098    }
099
100    public double getDx() {
101        return dx;
102    }
103
104    public double getDy() {
105        return dy;
106    }
107
108    public void setOffset(double dx, double dy) {
109        this.dx = dx;
110        this.dy = dy;
111    }
112
113    public void displace(double dx, double dy) {
114        setOffset(this.dx += dx, this.dy += dy);
115    }
116
117    public ImageryInfo getInfo() {
118        return info;
119    }
120
121    @Override
122    public Icon getIcon() {
123        return icon;
124    }
125
126    @Override
127    public boolean isMergable(Layer other) {
128        return false;
129    }
130
131    @Override
132    public void mergeFrom(Layer from) {
133    }
134
135    @Override
136    public Object getInfoComponent() {
137        JPanel panel = new JPanel(new GridBagLayout());
138        panel.add(new JLabel(getToolTipText()), GBC.eol());
139        if (info != null) {
140            String url = info.getUrl();
141            if (url != null) {
142                panel.add(new JLabel(tr("URL: ")), GBC.std().insets(0, 5, 2, 0));
143                panel.add(new UrlLabel(url), GBC.eol().insets(2, 5, 10, 0));
144            }
145            if (dx != 0.0 || dy != 0.0) {
146                panel.add(new JLabel(tr("Offset: ") + dx + ";" + dy), GBC.eol().insets(0, 5, 10, 0));
147            }
148        }
149        return panel;
150    }
151
152    public static ImageryLayer create(ImageryInfo info) {
153        if (info.getImageryType() == ImageryType.WMS || info.getImageryType() == ImageryType.HTML)
154            return new WMSLayer(info);
155        else if (info.getImageryType() == ImageryType.TMS || info.getImageryType() == ImageryType.BING || info.getImageryType() == ImageryType.SCANEX)
156            return new TMSLayer(info);
157        else throw new AssertionError();
158    }
159
160    class ApplyOffsetAction extends AbstractAction {
161        private OffsetBookmark b;
162        ApplyOffsetAction(OffsetBookmark b) {
163            super(b.name);
164            this.b = b;
165        }
166
167        @Override
168        public void actionPerformed(ActionEvent ev) {
169            setOffset(b.dx, b.dy);
170            Main.main.menu.imageryMenu.refreshOffsetMenu();
171            Main.map.repaint();
172        }
173    }
174
175    public class OffsetAction extends AbstractAction implements LayerAction {
176        @Override
177        public void actionPerformed(ActionEvent e) {
178        }
179
180        @Override
181        public Component createMenuComponent() {
182            return getOffsetMenuItem();
183        }
184
185        @Override
186        public boolean supportLayers(List<Layer> layers) {
187            return false;
188        }
189    }
190
191    public JMenuItem getOffsetMenuItem() {
192        JMenu subMenu = new JMenu(trc("layer", "Offset"));
193        subMenu.setIcon(ImageProvider.get("mapmode", "adjustimg"));
194        return (JMenuItem)getOffsetMenuItem(subMenu);
195    }
196
197    public JComponent getOffsetMenuItem(JComponent subMenu) {
198        JMenuItem adjustMenuItem = new JMenuItem(adjustAction);
199        if (OffsetBookmark.allBookmarks.isEmpty()) return adjustMenuItem;
200
201        subMenu.add(adjustMenuItem);
202        subMenu.add(new JSeparator());
203        boolean hasBookmarks = false;
204        int menuItemHeight = 0;
205        for (OffsetBookmark b : OffsetBookmark.allBookmarks) {
206            if (!b.isUsable(this)) {
207                continue;
208            }
209            JCheckBoxMenuItem item = new JCheckBoxMenuItem(new ApplyOffsetAction(b));
210            if (b.dx == dx && b.dy == dy) {
211                item.setSelected(true);
212            }
213            subMenu.add(item);
214            menuItemHeight = item.getPreferredSize().height;
215            hasBookmarks = true;
216        }
217        if (menuItemHeight > 0) {
218            if (subMenu instanceof JMenu) {
219                MenuScroller.setScrollerFor((JMenu) subMenu);
220            } else if (subMenu instanceof JPopupMenu) {
221                MenuScroller.setScrollerFor((JPopupMenu)subMenu);
222            }
223        }
224        return hasBookmarks ? subMenu : adjustMenuItem;
225    }
226
227    public BufferedImage sharpenImage(BufferedImage img) {
228        if (sharpenLevel <= 0) return img;
229        int width = img.getWidth(null);
230        int height = img.getHeight(null);
231        BufferedImage tmp = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
232        tmp.getGraphics().drawImage(img, 0, 0, null);
233        Kernel kernel;
234        if (sharpenLevel == 1) {
235            kernel = new Kernel(3, 3, new float[] { -0.25f, -0.5f, -0.25f, -0.5f, 4, -0.5f, -0.25f, -0.5f, -0.25f});
236        } else {
237            kernel = new Kernel(3, 3, new float[] { -0.5f, -1, -0.5f, -1, 7, -1, -0.5f, -1, -0.5f});
238        }
239        BufferedImageOp op = new ConvolveOp(kernel, ConvolveOp.EDGE_NO_OP, null);
240        return op.filter(tmp, null);
241    }
242
243    /**
244     * Draws a red error tile when imagery tile cannot be fetched.
245     * @param img The buffered image
246     * @param message Additional error message to display
247     */
248    public void drawErrorTile(BufferedImage img, String message) {
249        Graphics2D g = (Graphics2D) img.getGraphics();
250        g.setColor(Color.RED);
251        g.fillRect(0, 0, img.getWidth(), img.getHeight());
252        g.setFont(g.getFont().deriveFont(Font.PLAIN).deriveFont(24.0f));
253        g.setColor(Color.BLACK);
254
255        String text = tr("ERROR");
256        g.drawString(text, (img.getWidth() - g.getFontMetrics().stringWidth(text)) / 2, g.getFontMetrics().getHeight()+5);
257        if (message != null) {
258            float drawPosY = 2.5f*g.getFontMetrics().getHeight()+10;
259            if (!message.contains(" ")) {
260                g.setFont(g.getFont().deriveFont(Font.PLAIN).deriveFont(18.0f));
261                g.drawString(message, 5, (int)drawPosY);
262            } else {
263                // Draw message on several lines
264                Map<TextAttribute, Object> map = new Hashtable<TextAttribute, Object>();
265                map.put(TextAttribute.FAMILY, "Serif");
266                map.put(TextAttribute.SIZE, new Float(18.0));
267                AttributedString vanGogh = new AttributedString(message, map);
268                // Create a new LineBreakMeasurer from the text
269                AttributedCharacterIterator paragraph = vanGogh.getIterator();
270                int paragraphStart = paragraph.getBeginIndex();
271                int paragraphEnd = paragraph.getEndIndex();
272                FontRenderContext frc = g.getFontRenderContext();
273                LineBreakMeasurer lineMeasurer = new LineBreakMeasurer(paragraph, frc);
274                // Set break width to width of image with some margin
275                float breakWidth = img.getWidth()-10;
276                // Set position to the index of the first character in the text
277                lineMeasurer.setPosition(paragraphStart);
278                // Get lines until the entire paragraph has been displayed
279                while (lineMeasurer.getPosition() < paragraphEnd) {
280                    // Retrieve next layout
281                    TextLayout layout = lineMeasurer.nextLayout(breakWidth);
282
283                    // Compute pen x position
284                    float drawPosX = layout.isLeftToRight() ? 0 : breakWidth - layout.getAdvance();
285
286                    // Move y-coordinate by the ascent of the layout
287                    drawPosY += layout.getAscent();
288
289                    // Draw the TextLayout at (drawPosX, drawPosY)
290                    layout.draw(g, drawPosX, drawPosY);
291
292                    // Move y-coordinate in preparation for next layout
293                    drawPosY += layout.getDescent() + layout.getLeading();
294                }
295            }
296        }
297    }
298
299    @Override
300    public void destroy() {
301        super.destroy();
302        adjustAction.destroy();
303    }
304}