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

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

see #16073 - correct handling of server projections

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