source: josm/trunk/src/org/openstreetmap/josm/tools/HiDPISupport.java@ 12781

Last change on this file since 12781 was 12732, checked in by Don-vip, 7 years ago

see #9995 - sonar - squid:S3655 - Optional value should only be accessed after calling isPresent()

File size: 11.7 KB
Line 
1// License: GPL. For details, see LICENSE file.
2package org.openstreetmap.josm.tools;
3
4import java.awt.Dimension;
5import java.awt.GraphicsConfiguration;
6import java.awt.GraphicsEnvironment;
7import java.awt.Image;
8import java.awt.geom.AffineTransform;
9import java.lang.reflect.Constructor;
10import java.lang.reflect.InvocationTargetException;
11import java.lang.reflect.Method;
12import java.util.Arrays;
13import java.util.Collections;
14import java.util.List;
15import java.util.Optional;
16import java.util.function.Function;
17import java.util.stream.Collectors;
18import java.util.stream.IntStream;
19
20import javax.swing.ImageIcon;
21
22/**
23 * Helper class for HiDPI support.
24 *
25 * Gives access to the class <code>BaseMultiResolutionImage</code> via reflection,
26 * in case it is on classpath. This is to be expected for Java 9, but not for Java 8 runtime.
27 *
28 * @since 12722
29 */
30public class HiDPISupport {
31
32 private static volatile Optional<Class<? extends Image>> baseMultiResolutionImageClass;
33 private static volatile Optional<Constructor<? extends Image>> baseMultiResolutionImageConstructor;
34 private static volatile Optional<Method> resolutionVariantsMethod;
35
36 /**
37 * Create a multi-resolution image from a base image and an {@link ImageResource}.
38 * <p>
39 * Will only return multi-resolution image, if HiDPI-mode is detected. Then
40 * the image stack will consist of the base image and one that fits the
41 * HiDPI scale of the main display.
42 * @param base the base image
43 * @param ir a corresponding image resource
44 * @return multi-resolution image if necessary and possible, the base image otherwise
45 */
46 public static Image getMultiResolutionImage(Image base, ImageResource ir) {
47 double uiScale = getHiDPIScale();
48 if (uiScale != 1.0 && getBaseMultiResolutionImageConstructor().isPresent()) {
49 ImageIcon zoomed = ir.getImageIcon(new Dimension(
50 (int) Math.round(base.getWidth(null) * uiScale),
51 (int) Math.round(base.getHeight(null) * uiScale)), false);
52 Image mrImg = getMultiResolutionImage(Arrays.asList(base, zoomed.getImage()));
53 if (mrImg != null) return mrImg;
54 }
55 return base;
56 }
57
58 /**
59 * Create a multi-resolution image from a list of images.
60 * @param imgs the images, supposedly the same image at different resolutions,
61 * must not be empty
62 * @return corresponding multi-resolution image, if possible, the first image
63 * in the list otherwise
64 */
65 public static Image getMultiResolutionImage(List<Image> imgs) {
66 CheckParameterUtil.ensure(imgs, "imgs", "not empty", ls -> !ls.isEmpty());
67 Optional<Constructor<? extends Image>> baseMrImageConstructor = getBaseMultiResolutionImageConstructor();
68 if (baseMrImageConstructor.isPresent()) {
69 try {
70 return baseMrImageConstructor.get().newInstance((Object) imgs.toArray(new Image[0]));
71 } catch (InstantiationException | IllegalAccessException | InvocationTargetException ex) {
72 Logging.error("Unexpected error while instantiating object of class BaseMultiResolutionImage: " + ex);
73 }
74 }
75 return imgs.get(0);
76 }
77
78 /**
79 * Wrapper for the method <code>java.awt.image.BaseMultiResolutionImage#getBaseImage()</code>.
80 * <p>
81 * Will return the argument <code>img</code> unchanged, if it is not a multi-resolution image.
82 * @param img the image
83 * @return if <code>img</code> is a <code>java.awt.image.BaseMultiResolutionImage</code>,
84 * then the base image, otherwise the image itself
85 */
86 public static Image getBaseImage(Image img) {
87 Optional<Class<? extends Image>> baseMrImageClass = getBaseMultiResolutionImageClass();
88 Optional<Method> resVariantsMethod = getResolutionVariantsMethod();
89 if (!baseMrImageClass.isPresent() || !resVariantsMethod.isPresent()) {
90 return img;
91 }
92 if (baseMrImageClass.get().isInstance(img)) {
93 try {
94 @SuppressWarnings("unchecked")
95 List<Image> imgVars = (List<Image>) resVariantsMethod.get().invoke(img);
96 if (!imgVars.isEmpty()) {
97 return imgVars.get(0);
98 }
99 } catch (IllegalAccessException | InvocationTargetException ex) {
100 Logging.error("Unexpected error while calling method: " + ex);
101 }
102 }
103 return img;
104 }
105
106 /**
107 * Wrapper for the method <code>java.awt.image.MultiResolutionImage#getResolutionVariants()</code>.
108 * <p>
109 * Will return the argument as a singleton list, in case it is not a multi-resolution image.
110 * @param img the image
111 * @return if <code>img</code> is a <code>java.awt.image.BaseMultiResolutionImage</code>,
112 * then the result of the method <code>#getResolutionVariants()</code>, otherwise the image
113 * itself as a singleton list
114 */
115 public static List<Image> getResolutionVariants(Image img) {
116 Optional<Class<? extends Image>> baseMrImageClass = getBaseMultiResolutionImageClass();
117 Optional<Method> resVariantsMethod = getResolutionVariantsMethod();
118 if (!baseMrImageClass.isPresent() || !resVariantsMethod.isPresent()) {
119 return Collections.singletonList(img);
120 }
121 if (baseMrImageClass.get().isInstance(img)) {
122 try {
123 @SuppressWarnings("unchecked")
124 List<Image> imgVars = (List<Image>) resVariantsMethod.get().invoke(img);
125 if (!imgVars.isEmpty()) {
126 return imgVars;
127 }
128 } catch (IllegalAccessException | InvocationTargetException ex) {
129 Logging.error("Unexpected error while calling method: " + ex);
130 }
131 }
132 return Collections.singletonList(img);
133 }
134
135 /**
136 * Detect the GUI scale for HiDPI mode.
137 * <p>
138 * This method may not work as expected for a multi-monitor setup. It will
139 * only take the default screen device into account.
140 * @return the GUI scale for HiDPI mode, a value of 1.0 means standard mode.
141 */
142 private static double getHiDPIScale() {
143 if (GraphicsEnvironment.isHeadless())
144 return 1.0;
145 GraphicsConfiguration gc = GraphicsEnvironment
146 .getLocalGraphicsEnvironment()
147 .getDefaultScreenDevice().
148 getDefaultConfiguration();
149 AffineTransform transform = gc.getDefaultTransform();
150 if (!Utils.equalsEpsilon(transform.getScaleX(), transform.getScaleY())) {
151 Logging.warn("Unexpected ui transform: " + transform);
152 }
153 return transform.getScaleX();
154 }
155
156 /**
157 * Perform an operation on multi-resolution images.
158 *
159 * When input image is not multi-resolution, it will simply apply the processor once.
160 * Otherwise, the processor will be called for each resolution variant and the
161 * resulting images assembled to become the output multi-resolution image.
162 * @param img input image, possibly multi-resolution
163 * @param processor processor taking a plain image as input and returning a single
164 * plain image as output
165 * @return multi-resolution image assembled from the output of calls to <code>processor</code>
166 * for each resolution variant
167 */
168 public static Image processMRImage(Image img, Function<Image, Image> processor) {
169 return processMRImages(Collections.singletonList(img), imgs -> processor.apply(imgs.get(0)));
170 }
171
172 /**
173 * Perform an operation on multi-resolution images.
174 *
175 * When input images are not multi-resolution, it will simply apply the processor once.
176 * Otherwise, the processor will be called for each resolution variant and the
177 * resulting images assembled to become the output multi-resolution image.
178 * @param imgs input images, possibly multi-resolution
179 * @param processor processor taking a list of plain images as input and returning
180 * a single plain image as output
181 * @return multi-resolution image assembled from the output of calls to <code>processor</code>
182 * for each resolution variant
183 */
184 public static Image processMRImages(List<Image> imgs, Function<List<Image>, Image> processor) {
185 CheckParameterUtil.ensureThat(imgs.size() >= 1, "at least on element expected");
186 if (!getBaseMultiResolutionImageClass().isPresent()) {
187 return processor.apply(imgs);
188 }
189 List<List<Image>> allVars = imgs.stream().map(HiDPISupport::getResolutionVariants).collect(Collectors.toList());
190 int maxVariants = allVars.stream().mapToInt(lst -> lst.size()).max().getAsInt();
191 if (maxVariants == 1)
192 return processor.apply(imgs);
193 List<Image> imgsProcessed = IntStream.range(0, maxVariants)
194 .mapToObj(
195 k -> processor.apply(
196 allVars.stream().map(vars -> vars.get(k)).collect(Collectors.toList())
197 )
198 ).collect(Collectors.toList());
199 return getMultiResolutionImage(imgsProcessed);
200 }
201
202 private static Optional<Class<? extends Image>> getBaseMultiResolutionImageClass() {
203 if (baseMultiResolutionImageClass == null) {
204 synchronized (HiDPISupport.class) {
205 if (baseMultiResolutionImageClass == null) {
206 try {
207 @SuppressWarnings("unchecked")
208 Class<? extends Image> c = (Class<? extends Image>) Class.forName("java.awt.image.BaseMultiResolutionImage");
209 baseMultiResolutionImageClass = Optional.ofNullable(c);
210 } catch (ClassNotFoundException ex) {
211 // class is not present in Java 8
212 baseMultiResolutionImageClass = Optional.empty();
213 }
214 }
215 }
216 }
217 return baseMultiResolutionImageClass;
218 }
219
220 private static Optional<Constructor<? extends Image>> getBaseMultiResolutionImageConstructor() {
221 if (baseMultiResolutionImageConstructor == null) {
222 synchronized (HiDPISupport.class) {
223 if (baseMultiResolutionImageConstructor == null) {
224 getBaseMultiResolutionImageClass().ifPresent(klass -> {
225 try {
226 Constructor<? extends Image> constr = klass.getConstructor(Image[].class);
227 baseMultiResolutionImageConstructor = Optional.ofNullable(constr);
228 } catch (NoSuchMethodException ex) {
229 Logging.error("Cannot find expected constructor: " + ex);
230 }
231 });
232 if (baseMultiResolutionImageConstructor == null) {
233 baseMultiResolutionImageConstructor = Optional.empty();
234 }
235 }
236 }
237 }
238 return baseMultiResolutionImageConstructor;
239 }
240
241 private static Optional<Method> getResolutionVariantsMethod() {
242 if (resolutionVariantsMethod == null) {
243 synchronized (HiDPISupport.class) {
244 if (resolutionVariantsMethod == null) {
245 getBaseMultiResolutionImageClass().ifPresent(klass -> {
246 try {
247 Method m = klass.getMethod("getResolutionVariants");
248 resolutionVariantsMethod = Optional.ofNullable(m);
249 } catch (NoSuchMethodException ex) {
250 Logging.error("Cannot find expected method: "+ex);
251 }
252 });
253 if (resolutionVariantsMethod == null) {
254 resolutionVariantsMethod = Optional.empty();
255 }
256 }
257 }
258 }
259 return resolutionVariantsMethod;
260 }
261}
Note: See TracBrowser for help on using the repository browser.