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

Last change on this file since 8832 was 8832, checked in by simon04, 9 years ago

fix #11942 - Make gamma correction more robust to mysterious image types

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