source: josm/trunk/test/unit/org/openstreetmap/josm/gui/preferences/imagery/ImageryPreferenceTestIT.java@ 14550

Last change on this file since 14550 was 14550, checked in by Don-vip, 5 years ago

see #16073 - rework error handling

  • Property svn:eol-style set to native
File size: 13.4 KB
Line 
1// License: GPL. For details, see LICENSE file.
2package org.openstreetmap.josm.gui.preferences.imagery;
3
4import static org.junit.Assert.assertTrue;
5
6import java.io.ByteArrayInputStream;
7import java.io.IOException;
8import java.net.URL;
9import java.nio.charset.StandardCharsets;
10import java.util.ArrayList;
11import java.util.Collections;
12import java.util.List;
13import java.util.Map;
14import java.util.Optional;
15import java.util.TreeMap;
16import java.util.concurrent.TimeUnit;
17
18import javax.imageio.ImageIO;
19
20import org.apache.commons.jcs.access.CacheAccess;
21import org.junit.Before;
22import org.junit.Rule;
23import org.junit.Test;
24import org.openstreetmap.gui.jmapviewer.Coordinate;
25import org.openstreetmap.gui.jmapviewer.TileXY;
26import org.openstreetmap.gui.jmapviewer.interfaces.ICoordinate;
27import org.openstreetmap.gui.jmapviewer.tilesources.AbstractTileSource;
28import org.openstreetmap.gui.jmapviewer.tilesources.BingAerialTileSource;
29import org.openstreetmap.gui.jmapviewer.tilesources.ScanexTileSource;
30import org.openstreetmap.gui.jmapviewer.tilesources.TemplatedTMSTileSource;
31import org.openstreetmap.josm.TestUtils;
32import org.openstreetmap.josm.actions.AddImageryLayerAction;
33import org.openstreetmap.josm.actions.AddImageryLayerAction.LayerSelection;
34import org.openstreetmap.josm.data.Bounds;
35import org.openstreetmap.josm.data.coor.LatLon;
36import org.openstreetmap.josm.data.imagery.CoordinateConversion;
37import org.openstreetmap.josm.data.imagery.ImageryInfo;
38import org.openstreetmap.josm.data.imagery.ImageryInfo.ImageryBounds;
39import org.openstreetmap.josm.data.imagery.ImageryInfo.ImageryType;
40import org.openstreetmap.josm.data.imagery.ImageryLayerInfo;
41import org.openstreetmap.josm.data.imagery.LayerDetails;
42import org.openstreetmap.josm.data.imagery.Shape;
43import org.openstreetmap.josm.data.imagery.TMSCachedTileLoaderJob;
44import org.openstreetmap.josm.data.imagery.TemplatedWMSTileSource;
45import org.openstreetmap.josm.data.imagery.TileJobOptions;
46import org.openstreetmap.josm.data.imagery.WMTSTileSource;
47import org.openstreetmap.josm.data.imagery.WMTSTileSource.WMTSGetCapabilitiesException;
48import org.openstreetmap.josm.data.projection.Projection;
49import org.openstreetmap.josm.data.projection.ProjectionRegistry;
50import org.openstreetmap.josm.data.projection.Projections;
51import org.openstreetmap.josm.io.imagery.WMSImagery.WMSGetCapabilitiesException;
52import org.openstreetmap.josm.testutils.JOSMTestRules;
53import org.openstreetmap.josm.tools.HttpClient;
54import org.openstreetmap.josm.tools.HttpClient.Response;
55import org.openstreetmap.josm.tools.Logging;
56import org.openstreetmap.josm.tools.Utils;
57
58import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
59
60/**
61 * Integration tests of {@link ImageryPreference} class.
62 */
63public class ImageryPreferenceTestIT {
64
65 /**
66 * Setup rule
67 */
68 @Rule
69 @SuppressFBWarnings(value = "URF_UNREAD_PUBLIC_OR_PROTECTED_FIELD")
70 public JOSMTestRules test = new JOSMTestRules().https().preferences().projection().projectionNadGrids().timeout(10000*120);
71
72 private final Map<String, Map<ImageryInfo, List<String>>> errors = Collections.synchronizedMap(new TreeMap<>());
73 private final Map<String, byte[]> workingURLs = Collections.synchronizedMap(new TreeMap<>());
74
75 private TMSCachedTileLoaderJob helper;
76 private List<String> ignoredErrors;
77
78 /**
79 * Setup test
80 * @throws IOException in case of I/O error
81 */
82 @Before
83 public void before() throws IOException {
84 helper = new TMSCachedTileLoaderJob(null, null, new CacheAccess<>(null), new TileJobOptions(0, 0, null, 0), null);
85 ignoredErrors = TestUtils.getIgnoredErrorMessages(ImageryPreferenceTestIT.class);
86 }
87
88 private boolean addError(ImageryInfo info, String error) {
89 return !ignoredErrors.contains(error) &&
90 errors.computeIfAbsent(info.getCountryCode(), x -> Collections.synchronizedMap(new TreeMap<>()))
91 .computeIfAbsent(info, x -> Collections.synchronizedList(new ArrayList<>()))
92 .add(error);
93 }
94
95 private Optional<byte[]> checkUrl(ImageryInfo info, String url) {
96 if (url != null && !url.isEmpty()) {
97 if (workingURLs.containsKey(url)) {
98 return Optional.of(workingURLs.get(url));
99 }
100 try {
101 Response response = HttpClient.create(new URL(url))
102 .setHeaders(info.getCustomHttpHeaders())
103 .setConnectTimeout((int) TimeUnit.SECONDS.toMillis(30))
104 .setReadTimeout((int) TimeUnit.SECONDS.toMillis(60))
105 .connect();
106 if (response.getResponseCode() >= 400) {
107 addError(info, url + " -> HTTP " + response.getResponseCode());
108 } else if (response.getResponseCode() >= 300) {
109 Logging.warn(url + " -> HTTP " + response.getResponseCode());
110 }
111 try {
112 byte[] data = Utils.readBytesFromStream(response.getContent());
113 if (response.getResponseCode() < 300) {
114 workingURLs.put(url, data);
115 }
116 return Optional.of(data);
117 } catch (IOException e) {
118 if (response.getResponseCode() < 300) {
119 addError(info, url + " -> " + e);
120 }
121 } finally {
122 response.disconnect();
123 }
124 } catch (IOException e) {
125 addError(info, url + " -> " + e);
126 }
127 }
128 return Optional.empty();
129 }
130
131 private void checkLinkUrl(ImageryInfo info, String url) {
132 checkUrl(info, url).filter(x -> x.length == 0).ifPresent(x -> addError(info, url + " -> returned empty contents"));
133 }
134
135 private void checkTileUrl(ImageryInfo info, AbstractTileSource tileSource, ICoordinate center, int zoom)
136 throws IOException {
137 TileXY xy = tileSource.latLonToTileXY(center, zoom);
138 for (int i = 0; i < 3; i++) {
139 try {
140 String url = tileSource.getTileUrl(zoom, xy.getXIndex(), xy.getYIndex());
141 checkUrl(info, url).ifPresent(data -> {
142 try (ByteArrayInputStream bais = new ByteArrayInputStream(data)) {
143 if (ImageIO.read(bais) == null) {
144 addImageError(info, url, data, "did not return an image");
145 }
146 } catch (IOException e) {
147 addImageError(info, url, data, e.toString());
148 Logging.trace(e);
149 }
150 });
151 return;
152 } catch (IOException e) {
153 // Try up to three times max to allow Bing source to initialize itself
154 // and avoid random network errors
155 Logging.trace(e);
156 if (i == 2) {
157 throw e;
158 }
159 try {
160 Thread.sleep(500);
161 } catch (InterruptedException ex) {
162 Logging.warn(ex);
163 }
164 }
165 }
166 }
167
168 private void addImageError(ImageryInfo info, String url, byte[] data, String defaultMessage) {
169 // Check if we have received an error message
170 String error = helper.detectErrorMessage(new String(data, StandardCharsets.UTF_8));
171 addError(info, url + " -> " + (error != null ? error.split("\\n")[0] : defaultMessage));
172 }
173
174 private static LatLon getPointInShape(Shape shape) {
175 final Coordinate p1 = shape.getPoints().get(0);
176 final Bounds bounds = new Bounds(p1.getLat(), p1.getLon(), p1.getLat(), p1.getLon());
177 shape.getPoints().forEach(p -> bounds.extend(p.getLat(), p.getLon()));
178
179 final double w = bounds.getWidth();
180 final double h = bounds.getHeight();
181
182 final double x2 = bounds.getMinLon() + (w / 2.0);
183 final double y2 = bounds.getMinLat() + (h / 2.0);
184
185 final LatLon center = new LatLon(y2, x2);
186
187 // check to see if center is inside shape
188 if (shape.contains(center)) {
189 return center;
190 }
191
192 // if center position (C) is not inside shape, try naively some other positions as follows:
193 final double x1 = bounds.getMinLon() + (.25 * w);
194 final double x3 = bounds.getMinLon() + (.75 * w);
195 final double y1 = bounds.getMinLat() + (.25 * h);
196 final double y3 = bounds.getMinLat() + (.75 * h);
197 // +-----------+
198 // | 5 1 6 |
199 // | 4 C 2 |
200 // | 8 3 7 |
201 // +-----------+
202 for (LatLon candidate : new LatLon[] {
203 new LatLon(y1, x2),
204 new LatLon(y2, x3),
205 new LatLon(y3, x2),
206 new LatLon(y2, x1),
207 new LatLon(y1, x1),
208 new LatLon(y1, x3),
209 new LatLon(y3, x3),
210 new LatLon(y3, x1)
211 }) {
212 if (shape.contains(candidate)) {
213 return candidate;
214 }
215 }
216 return center;
217 }
218
219 private static LatLon getCenter(ImageryBounds bounds) {
220 List<Shape> shapes = bounds.getShapes();
221 return shapes != null && !shapes.isEmpty() ? getPointInShape(shapes.get(0)) : bounds.getCenter();
222 }
223
224 private void checkEntry(ImageryInfo info) {
225 Logging.info("Checking "+ info);
226
227 if (info.getAttributionImageRaw() != null && info.getAttributionImage() == null) {
228 addError(info, "Can't fetch attribution image: " + info.getAttributionImageRaw());
229 }
230
231 checkLinkUrl(info, info.getAttributionImageURL());
232 checkLinkUrl(info, info.getAttributionLinkURL());
233 String eula = info.getEulaAcceptanceRequired();
234 if (eula != null) {
235 checkLinkUrl(info, eula.replaceAll("\\{lang\\}", ""));
236 }
237 checkLinkUrl(info, info.getPermissionReferenceURL());
238 checkLinkUrl(info, info.getTermsOfUseURL());
239
240 try {
241 ImageryBounds bounds = info.getBounds();
242 // Some imagery sources do not define tiles at (0,0). So pickup Greenwich Royal Observatory for global sources
243 ICoordinate center = CoordinateConversion.llToCoor(bounds != null ? getCenter(bounds) : new LatLon(51.47810, -0.00170));
244 AbstractTileSource tileSource = getTileSource(info);
245 checkTileUrl(info, tileSource, center, info.getMinZoom());
246 // checking max zoom for real is complex, see https://josm.openstreetmap.de/ticket/16073#comment:27
247 if (info.getMaxZoom() > 0 && info.getImageryType() != ImageryType.SCANEX) {
248 checkTileUrl(info, tileSource, center, Utils.clamp(12, info.getMinZoom() + 1, info.getMaxZoom()));
249 }
250 } catch (IOException | RuntimeException | WMSGetCapabilitiesException | WMTSGetCapabilitiesException e) {
251 addError(info, info.getUrl() + " -> " + e.toString());
252 }
253
254 for (ImageryInfo mirror : info.getMirrors()) {
255 checkEntry(mirror);
256 }
257 }
258
259 private static Projection getProjection(ImageryInfo info) {
260 if (!info.getServerProjections().isEmpty()) {
261 Projection proj = Projections.getProjectionByCode(info.getServerProjections().get(0));
262 if (proj != null) {
263 return proj;
264 }
265 }
266 return ProjectionRegistry.getProjection();
267 }
268
269 private static AbstractTileSource getTileSource(ImageryInfo info)
270 throws IOException, WMTSGetCapabilitiesException, WMSGetCapabilitiesException {
271 switch (info.getImageryType()) {
272 case BING:
273 return new BingAerialTileSource(info);
274 case SCANEX:
275 return new ScanexTileSource(info);
276 case TMS:
277 return new TemplatedTMSTileSource(info);
278 case WMS_ENDPOINT:
279 info = convertWmsEndpointToWms(info); // fall-through
280 case WMS:
281 return new TemplatedWMSTileSource(info, getProjection(info));
282 case WMTS:
283 return new WMTSTileSource(info, getProjection(info));
284 default:
285 throw new UnsupportedOperationException(info.toString());
286 }
287 }
288
289 private static ImageryInfo convertWmsEndpointToWms(ImageryInfo info) throws IOException, WMSGetCapabilitiesException {
290 return Optional.ofNullable(AddImageryLayerAction.getWMSLayerInfo(
291 info, wms -> new LayerSelection(firstLeafLayer(wms.getLayers()), wms.getPreferredFormat(), true)))
292 .orElseThrow(() -> new IllegalStateException("Unable to convert WMS_ENDPOINT to WMS"));
293 }
294
295 private static List<LayerDetails> firstLeafLayer(List<LayerDetails> layers) {
296 for (LayerDetails layer : layers) {
297 if (layer.getChildren().isEmpty()) {
298 return Collections.singletonList(layer);
299 } else {
300 return firstLeafLayer(layer.getChildren());
301 }
302 }
303 return Collections.emptyList();
304 }
305
306 /**
307 * Test that available imagery entries are valid.
308 * @throws Exception in case of error
309 */
310 @Test
311 public void testValidityOfAvailableImageryEntries() throws Exception {
312 ImageryLayerInfo.instance.load(false);
313 ImageryLayerInfo.instance.getDefaultLayers().parallelStream().forEach(this::checkEntry);
314 assertTrue(errors.toString().replaceAll("\\}, ", "\n\\}, ").replaceAll(", ImageryInfo\\{", "\n ,ImageryInfo\\{"),
315 errors.isEmpty());
316 }
317}
Note: See TracBrowser for help on using the repository browser.