source: josm/trunk/src/org/openstreetmap/josm/gui/layer/ImageryLayer.java@ 8733

Last change on this file since 8733 was 8733, checked in by simon04, 10 years ago

fix #11836 - Zoom out with Gamma adjusted on imagery causes Exception

  • Property svn:eol-style set to native
File size: 14.9 KB
Line 
1// License: GPL. For details, see LICENSE file.
2package org.openstreetmap.josm.gui.layer;
3
4import static org.openstreetmap.josm.tools.I18n.marktr;
5import static org.openstreetmap.josm.tools.I18n.tr;
6import static org.openstreetmap.josm.tools.I18n.trc;
7
8import java.awt.Color;
9import java.awt.Component;
10import java.awt.Font;
11import java.awt.Graphics2D;
12import java.awt.GridBagLayout;
13import java.awt.event.ActionEvent;
14import java.awt.font.FontRenderContext;
15import java.awt.font.LineBreakMeasurer;
16import java.awt.font.TextAttribute;
17import java.awt.font.TextLayout;
18import java.awt.image.BufferedImage;
19import java.awt.image.BufferedImageOp;
20import java.awt.image.ConvolveOp;
21import java.awt.image.Kernel;
22import java.awt.image.LookupOp;
23import java.awt.image.ShortLookupTable;
24import java.text.AttributedCharacterIterator;
25import java.text.AttributedString;
26import java.util.ArrayList;
27import java.util.Hashtable;
28import java.util.List;
29import java.util.Map;
30
31import javax.swing.AbstractAction;
32import javax.swing.Icon;
33import javax.swing.JCheckBoxMenuItem;
34import javax.swing.JComponent;
35import javax.swing.JLabel;
36import javax.swing.JMenu;
37import javax.swing.JMenuItem;
38import javax.swing.JPanel;
39import javax.swing.JPopupMenu;
40import javax.swing.JSeparator;
41
42import org.openstreetmap.josm.Main;
43import org.openstreetmap.josm.actions.ImageryAdjustAction;
44import org.openstreetmap.josm.data.ProjectionBounds;
45import org.openstreetmap.josm.data.imagery.ImageryInfo;
46import org.openstreetmap.josm.data.imagery.OffsetBookmark;
47import org.openstreetmap.josm.data.preferences.ColorProperty;
48import org.openstreetmap.josm.data.preferences.IntegerProperty;
49import org.openstreetmap.josm.gui.MenuScroller;
50import org.openstreetmap.josm.gui.widgets.UrlLabel;
51import org.openstreetmap.josm.tools.GBC;
52import org.openstreetmap.josm.tools.ImageProvider;
53import org.openstreetmap.josm.tools.Utils;
54
55public abstract class ImageryLayer extends Layer {
56
57 public static final ColorProperty PROP_FADE_COLOR = new ColorProperty(marktr("Imagery fade"), Color.white);
58 public static final IntegerProperty PROP_FADE_AMOUNT = new IntegerProperty("imagery.fade_amount", 0);
59 public static final IntegerProperty PROP_SHARPEN_LEVEL = new IntegerProperty("imagery.sharpen_level", 0);
60
61 private final List<ImageProcessor> imageProcessors = new ArrayList<>();
62
63 public static Color getFadeColor() {
64 return PROP_FADE_COLOR.get();
65 }
66
67 public static Color getFadeColorWithAlpha() {
68 Color c = PROP_FADE_COLOR.get();
69 return new Color(c.getRed(), c.getGreen(), c.getBlue(), PROP_FADE_AMOUNT.get()*255/100);
70 }
71
72 protected final ImageryInfo info;
73
74 protected Icon icon;
75
76 protected double dx = 0.0;
77 protected double dy = 0.0;
78
79 protected GammaImageProcessor gammaImageProcessor = new GammaImageProcessor();
80
81 private final ImageryAdjustAction adjustAction = new ImageryAdjustAction(this);
82
83 /**
84 * Constructs a new {@code ImageryLayer}.
85 * @param info imagery info
86 */
87 public ImageryLayer(ImageryInfo info) {
88 super(info.getName());
89 this.info = info;
90 if (info.getIcon() != null) {
91 icon = new ImageProvider(info.getIcon()).setOptional(true).
92 setMaxHeight(ICON_SIZE).setMaxWidth(ICON_SIZE).get();
93 }
94 if (icon == null) {
95 icon = ImageProvider.get("imagery_small");
96 }
97 addImageProcessor(createSharpener(PROP_SHARPEN_LEVEL.get()));
98 addImageProcessor(gammaImageProcessor);
99 }
100
101 public double getPPD() {
102 if (!Main.isDisplayingMapView()) return Main.getProjection().getDefaultZoomInPPD();
103 ProjectionBounds bounds = Main.map.mapView.getProjectionBounds();
104 return Main.map.mapView.getWidth() / (bounds.maxEast - bounds.minEast);
105 }
106
107 public double getDx() {
108 return dx;
109 }
110
111 public double getDy() {
112 return dy;
113 }
114
115 public void setOffset(double dx, double dy) {
116 this.dx = dx;
117 this.dy = dy;
118 }
119
120 public void displace(double dx, double dy) {
121 setOffset(this.dx += dx, this.dy += dy);
122 }
123
124 public ImageryInfo getInfo() {
125 return info;
126 }
127
128 @Override
129 public Icon getIcon() {
130 return icon;
131 }
132
133 @Override
134 public boolean isMergable(Layer other) {
135 return false;
136 }
137
138 @Override
139 public void mergeFrom(Layer from) {
140 }
141
142 @Override
143 public Object getInfoComponent() {
144 JPanel panel = new JPanel(new GridBagLayout());
145 panel.add(new JLabel(getToolTipText()), GBC.eol());
146 if (info != null) {
147 String url = info.getUrl();
148 if (url != null) {
149 panel.add(new JLabel(tr("URL: ")), GBC.std().insets(0, 5, 2, 0));
150 panel.add(new UrlLabel(url), GBC.eol().insets(2, 5, 10, 0));
151 }
152 if (dx != 0 || dy != 0) {
153 panel.add(new JLabel(tr("Offset: ") + dx + ";" + dy), GBC.eol().insets(0, 5, 10, 0));
154 }
155 }
156 return panel;
157 }
158
159 public static ImageryLayer create(ImageryInfo info) {
160 switch(info.getImageryType()) {
161 case WMS:
162 case HTML:
163 return new WMSLayer(info);
164 case WMTS:
165 return new WMTSLayer(info);
166 case TMS:
167 case BING:
168 case SCANEX:
169 return new TMSLayer(info);
170 default:
171 throw new AssertionError(tr("Unsupported imagery type: {0}", info.getImageryType()));
172 }
173 }
174
175 class ApplyOffsetAction extends AbstractAction {
176 private transient OffsetBookmark b;
177
178 ApplyOffsetAction(OffsetBookmark b) {
179 super(b.name);
180 this.b = b;
181 }
182
183 @Override
184 public void actionPerformed(ActionEvent ev) {
185 setOffset(b.dx, b.dy);
186 Main.main.menu.imageryMenu.refreshOffsetMenu();
187 Main.map.repaint();
188 }
189 }
190
191 public class OffsetAction extends AbstractAction implements LayerAction {
192 @Override
193 public void actionPerformed(ActionEvent e) {
194 }
195
196 @Override
197 public Component createMenuComponent() {
198 return getOffsetMenuItem();
199 }
200
201 @Override
202 public boolean supportLayers(List<Layer> layers) {
203 return false;
204 }
205 }
206
207 public JMenuItem getOffsetMenuItem() {
208 JMenu subMenu = new JMenu(trc("layer", "Offset"));
209 subMenu.setIcon(ImageProvider.get("mapmode", "adjustimg"));
210 return (JMenuItem) getOffsetMenuItem(subMenu);
211 }
212
213 public JComponent getOffsetMenuItem(JComponent subMenu) {
214 JMenuItem adjustMenuItem = new JMenuItem(adjustAction);
215 if (OffsetBookmark.allBookmarks.isEmpty()) return adjustMenuItem;
216
217 subMenu.add(adjustMenuItem);
218 subMenu.add(new JSeparator());
219 boolean hasBookmarks = false;
220 int menuItemHeight = 0;
221 for (OffsetBookmark b : OffsetBookmark.allBookmarks) {
222 if (!b.isUsable(this)) {
223 continue;
224 }
225 JCheckBoxMenuItem item = new JCheckBoxMenuItem(new ApplyOffsetAction(b));
226 if (Utils.equalsEpsilon(b.dx, dx) && Utils.equalsEpsilon(b.dy, dy)) {
227 item.setSelected(true);
228 }
229 subMenu.add(item);
230 menuItemHeight = item.getPreferredSize().height;
231 hasBookmarks = true;
232 }
233 if (menuItemHeight > 0) {
234 if (subMenu instanceof JMenu) {
235 MenuScroller.setScrollerFor((JMenu) subMenu);
236 } else if (subMenu instanceof JPopupMenu) {
237 MenuScroller.setScrollerFor((JPopupMenu) subMenu);
238 }
239 }
240 return hasBookmarks ? subMenu : adjustMenuItem;
241 }
242
243 public ImageProcessor createSharpener(int sharpenLevel) {
244 final Kernel kernel;
245 if (sharpenLevel == 1) {
246 kernel = new Kernel(3, 3, new float[]{-0.25f, -0.5f, -0.25f, -0.5f, 4, -0.5f, -0.25f, -0.5f, -0.25f});
247 } else if (sharpenLevel == 2) {
248 kernel = new Kernel(3, 3, new float[]{-0.5f, -1, -0.5f, -1, 7, -1, -0.5f, -1, -0.5f});
249 } else {
250 return null;
251 }
252 BufferedImageOp op = new ConvolveOp(kernel, ConvolveOp.EDGE_NO_OP, null);
253 return createImageProcessor(op, false);
254 }
255
256 /**
257 * An image processor which adjusts the gamma value of an image.
258 */
259 public static class GammaImageProcessor implements ImageProcessor {
260 private double gamma = 1;
261 final short[] gammaChange = new short[256];
262 private LookupOp op3 = new LookupOp(new ShortLookupTable(0, new short[][]{gammaChange, gammaChange, gammaChange}), null);
263 private LookupOp op4 = new LookupOp(new ShortLookupTable(0, new short[][]{gammaChange, gammaChange, gammaChange, gammaChange}), null);
264
265 /**
266 * Returns the currently set gamma value.
267 */
268 public double getGamma() {
269 return gamma;
270 }
271
272 /**
273 * Sets a new gamma value, {@code 1} stands for no correction.
274 */
275 public void setGamma(double gamma) {
276 this.gamma = gamma;
277 for (int i = 0; i < 256; i++) {
278 gammaChange[i] = (short) (255 * Math.pow(i / 255., gamma));
279 }
280 }
281
282 private LookupOp getOp(int bands) {
283 if (gamma == 1) {
284 return null;
285 } else if (bands == 3) {
286 return op3;
287 } else if (bands == 4) {
288 return op4;
289 } else {
290 return null;
291 }
292 }
293
294 @Override
295 public BufferedImage process(BufferedImage image) {
296 final LookupOp op = getOp(image.getRaster().getNumBands());
297 final BufferedImage to = new BufferedImage(image.getWidth(), image.getHeight(), image.getType());
298 return op == null ? image : op.filter(image, to);
299 }
300 }
301
302 /**
303 * Returns the currently set gamma value.
304 */
305 public double getGamma() {
306 return gammaImageProcessor.getGamma();
307 }
308
309 /**
310 * Sets a new gamma value, {@code 1} stands for no correction.
311 */
312 public void setGamma(double gamma) {
313 gammaImageProcessor.setGamma(gamma);
314 }
315
316 /**
317 * This method adds the {@link ImageProcessor} to this Layer if it is not {@code null}.
318 *
319 * @param processor that processes the image
320 *
321 * @return true if processor was added, false otherwise
322 */
323 public boolean addImageProcessor(ImageProcessor processor) {
324 return processor != null && imageProcessors.add(processor);
325 }
326
327 /**
328 * This method removes given {@link ImageProcessor} from this layer
329 *
330 * @param processor which is needed to be removed
331 *
332 * @return true if processor was removed
333 */
334 public boolean removeImageProcessor(ImageProcessor processor) {
335 return imageProcessors.remove(processor);
336 }
337
338 /**
339 * Wraps a {@link BufferedImageOp} to be used as {@link ImageProcessor}.
340 * @param op the {@link BufferedImageOp}
341 * @param inPlace true to apply filter in place, i.e., not create a new {@link BufferedImage} for the result
342 * (the {@code op} needs to support this!)
343 * @return the {@link ImageProcessor} wrapper
344 */
345 public static ImageProcessor createImageProcessor(final BufferedImageOp op, final boolean inPlace) {
346 return new ImageProcessor() {
347 @Override
348 public BufferedImage process(BufferedImage image) {
349 return op.filter(image, inPlace ? image : null);
350 }
351 };
352 }
353
354 /**
355 * This method gets all {@link ImageProcessor}s of the layer
356 *
357 * @return list of image processors without removed one
358 */
359 public List<ImageProcessor> getImageProcessors() {
360 return imageProcessors;
361 }
362
363 /**
364 * Applies all the chosen {@link ImageProcessor}s to the image
365 *
366 * @param img - image which should be changed
367 *
368 * @return the new changed image
369 */
370 public BufferedImage applyImageProcessors(BufferedImage img) {
371 for (ImageProcessor processor : imageProcessors) {
372 img = processor.process(img);
373 }
374 return img;
375 }
376
377 /**
378 * Draws a red error tile when imagery tile cannot be fetched.
379 * @param img The buffered image
380 * @param message Additional error message to display
381 */
382 public void drawErrorTile(BufferedImage img, String message) {
383 Graphics2D g = (Graphics2D) img.getGraphics();
384 g.setColor(Color.RED);
385 g.fillRect(0, 0, img.getWidth(), img.getHeight());
386 g.setFont(g.getFont().deriveFont(Font.PLAIN).deriveFont(24.0f));
387 g.setColor(Color.BLACK);
388
389 String text = tr("ERROR");
390 g.drawString(text, (img.getWidth() - g.getFontMetrics().stringWidth(text)) / 2, g.getFontMetrics().getHeight()+5);
391 if (message != null) {
392 float drawPosY = 2.5f*g.getFontMetrics().getHeight()+10;
393 if (!message.contains(" ")) {
394 g.setFont(g.getFont().deriveFont(Font.PLAIN).deriveFont(18.0f));
395 g.drawString(message, 5, (int) drawPosY);
396 } else {
397 // Draw message on several lines
398 Map<TextAttribute, Object> map = new Hashtable<TextAttribute, Object>();
399 map.put(TextAttribute.FAMILY, "Serif");
400 map.put(TextAttribute.SIZE, new Float(18.0));
401 AttributedString vanGogh = new AttributedString(message, map);
402 // Create a new LineBreakMeasurer from the text
403 AttributedCharacterIterator paragraph = vanGogh.getIterator();
404 int paragraphStart = paragraph.getBeginIndex();
405 int paragraphEnd = paragraph.getEndIndex();
406 FontRenderContext frc = g.getFontRenderContext();
407 LineBreakMeasurer lineMeasurer = new LineBreakMeasurer(paragraph, frc);
408 // Set break width to width of image with some margin
409 float breakWidth = img.getWidth()-10;
410 // Set position to the index of the first character in the text
411 lineMeasurer.setPosition(paragraphStart);
412 // Get lines until the entire paragraph has been displayed
413 while (lineMeasurer.getPosition() < paragraphEnd) {
414 // Retrieve next layout
415 TextLayout layout = lineMeasurer.nextLayout(breakWidth);
416
417 // Compute pen x position
418 float drawPosX = layout.isLeftToRight() ? 0 : breakWidth - layout.getAdvance();
419
420 // Move y-coordinate by the ascent of the layout
421 drawPosY += layout.getAscent();
422
423 // Draw the TextLayout at (drawPosX, drawPosY)
424 layout.draw(g, drawPosX, drawPosY);
425
426 // Move y-coordinate in preparation for next layout
427 drawPosY += layout.getDescent() + layout.getLeading();
428 }
429 }
430 }
431 }
432
433 @Override
434 public void destroy() {
435 super.destroy();
436 adjustAction.destroy();
437 }
438}
Note: See TracBrowser for help on using the repository browser.