View Javadoc
1   /*
2    * Copyright (c) 2012-2013, Dienst Landelijk Gebied - Ministerie van Economische Zaken
3    *
4    * Gepubliceerd onder de BSD 2-clause licentie,
5    * zie https://github.com/MinELenI/CBSviewer/blob/master/LICENSE.md voor de volledige licentie.
6    */
7   package nl.mineleni.cbsviewer.servlet.wms;
8   
9   import static nl.mineleni.cbsviewer.util.CookieNamesConstants.COOKIE_S;
10  import static nl.mineleni.cbsviewer.util.CookieNamesConstants.COOKIE_X;
11  import static nl.mineleni.cbsviewer.util.CookieNamesConstants.COOKIE_Y;
12  import static nl.mineleni.cbsviewer.util.CookieNamesConstants.COOKIE_baselyr;
13  import static nl.mineleni.cbsviewer.util.CookieNamesConstants.COOKIE_mapid;
14  import static nl.mineleni.cbsviewer.util.StringConstants.MAP_CACHE_DIR;
15  import static nl.mineleni.cbsviewer.util.StringConstants.REQ_PARAM_BGMAP;
16  import static nl.mineleni.cbsviewer.util.StringConstants.REQ_PARAM_CACHEDIR;
17  import static nl.mineleni.cbsviewer.util.StringConstants.REQ_PARAM_DOWNLOADLINK;
18  import static nl.mineleni.cbsviewer.util.StringConstants.REQ_PARAM_FEATUREINFO;
19  import static nl.mineleni.cbsviewer.util.StringConstants.REQ_PARAM_FGMAP_ALPHA;
20  import static nl.mineleni.cbsviewer.util.StringConstants.REQ_PARAM_KAART;
21  import static nl.mineleni.cbsviewer.util.StringConstants.REQ_PARAM_LEGENDAS;
22  import static nl.mineleni.cbsviewer.util.StringConstants.REQ_PARAM_MAPID;
23  
24  import java.awt.BasicStroke;
25  import java.awt.Color;
26  import java.awt.Font;
27  import java.awt.FontMetrics;
28  import java.awt.Graphics2D;
29  import java.awt.RenderingHints;
30  import java.awt.image.BufferedImage;
31  import java.awt.image.RescaleOp;
32  import java.io.File;
33  import java.io.IOException;
34  import java.net.MalformedURLException;
35  import java.net.URL;
36  import java.util.HashSet;
37  import java.util.Map;
38  import java.util.Set;
39  import java.util.concurrent.ConcurrentHashMap;
40  
41  import javax.imageio.ImageIO;
42  import javax.servlet.ServletConfig;
43  import javax.servlet.ServletContext;
44  import javax.servlet.ServletException;
45  import javax.servlet.http.HttpServletRequest;
46  import javax.servlet.http.HttpServletResponse;
47  
48  import nl.mineleni.cbsviewer.servlet.AbstractWxSServlet;
49  import nl.mineleni.cbsviewer.servlet.wms.FeatureInfoResponseConverter.CONVERTER_TYPE;
50  import nl.mineleni.cbsviewer.servlet.wms.cache.BboxLayerCacheKey;
51  import nl.mineleni.cbsviewer.servlet.wms.cache.CachableString;
52  import nl.mineleni.cbsviewer.servlet.wms.cache.Cache;
53  import nl.mineleni.cbsviewer.servlet.wms.cache.CacheImage;
54  import nl.mineleni.cbsviewer.servlet.wms.cache.WMSCache;
55  import nl.mineleni.cbsviewer.util.AvailableLayersBean;
56  import nl.mineleni.cbsviewer.util.SpatialUtil;
57  import nl.mineleni.cbsviewer.util.xml.LayerDescriptor;
58  
59  import org.geotools.data.ows.Layer;
60  import org.geotools.data.ows.StyleImpl;
61  import org.geotools.data.ows.WMSCapabilities;
62  import org.geotools.data.wms.WMSUtils;
63  import org.geotools.data.wms.WebMapServer;
64  import org.geotools.data.wms.request.GetFeatureInfoRequest;
65  import org.geotools.data.wms.request.GetLegendGraphicRequest;
66  import org.geotools.data.wms.request.GetMapRequest;
67  import org.geotools.data.wms.response.GetFeatureInfoResponse;
68  import org.geotools.data.wms.response.GetLegendGraphicResponse;
69  import org.geotools.data.wms.response.GetMapResponse;
70  import org.geotools.ows.ServiceException;
71  import org.opengis.geometry.BoundingBox;
72  import org.slf4j.Logger;
73  import org.slf4j.LoggerFactory;
74  
75  /**
76   * WMS client voor de applicatie.
77   *
78   * @author prinsmc
79   * @since 1.7
80   */
81  public class WMSClientServlet extends AbstractWxSServlet {
82  
83  	/** maximum aantal elementen per cache. {@value} */
84  	private static final int NUMBER_CACHE_ELEMENTS = 1000;
85  
86  	/** logger. */
87  	private static final Logger LOGGER = LoggerFactory
88  	        .getLogger(WMSClientServlet.class);
89  
90  	/**
91  	 * type featureinfo response.
92  	 */
93  	private static CONVERTER_TYPE type = CONVERTER_TYPE.GMLTYPE;
94  
95  	/**
96  	 * vaste afmeting van de kaart (hoogte en breedte). {@value}
97  	 *
98  	 * @see #MAP_DIMENSION_MIDDLE
99  	 */
100 	private static final int MAP_DIMENSION = 512;
101 
102 	/**
103 	 * helft van de afmeting van de kaart (hoogte en breedte). {@value}
104 	 *
105 	 * @see #MAP_DIMENSION
106 	 */
107 	private static final int MAP_DIMENSION_MIDDLE = MAP_DIMENSION / 2;
108 
109 	/** maximum aantal features voor feauture infor request. */
110 	private static final int MAX_FEATURE_COUNT = 10;
111 
112 	/** time-to-live voor cache elementen. {@value} seconden. */
113 	private static final long SECONDS_TO_CACHE_ELEMENTS = 60 * 60/* 1 uur */;
114 
115 	/** time-to-live voor cache elementen. {@value} milliseconden. */
116 	private static final long MILLISECONDS_TO_CACHE_ELEMENTS = SECONDS_TO_CACHE_ELEMENTS * 1000;
117 
118 	/** serialVersionUID. */
119 	private static final long serialVersionUID = 4958212343847516071L;
120 
121 	/** De achtergrond kaart WMS. */
122 	private transient WebMapServer bgWMS;
123 
124 	/** cache voor legenda afbeeldingen. */
125 	private volatile Cache<String, CacheImage, BufferedImage> legendCache;
126 
127 	/** cache voor voorgrond WMS afbeeldingen. */
128 	private volatile Cache<BboxLayerCacheKey, CacheImage, BufferedImage> fgWMSCache;
129 
130 	/** cache voor feature info. */
131 	private volatile Cache<BboxLayerCacheKey, CachableString, String> featInfoCache;
132 
133 	/** verzameling lagen voor de achtergrondkaart. */
134 	private transient String[] bgWMSlayers;
135 
136 	/**
137 	 * voorgrond wms request.
138 	 *
139 	 * @todo refactor naar lokale variabele
140 	 */
141 	private transient GetMapRequest getMapRequest;
142 
143 	/** layers bean. */
144 	private final transient AvailableLayersBean layers = new AvailableLayersBean();
145 
146 	/** cache voor achtergrond kaartjes. */
147 	private transient WMSCache bgWMSCache;
148 
149 	/** De achtergrond luchtfoto WMS. */
150 	private transient WebMapServer lufoWMS;
151 
152 	/** cache voor achtergrond kaartjes. */
153 	private transient WMSCache bgWMSLuFoCache;
154 
155 	/** verzameling lagen voor de achtergrondkaart. */
156 	private transient String[] lufoWMSlayers;
157 
158 	/**
159 	 * de verzameling met (voorgrond) WMSsen die we benaderen. Het opstarten van
160 	 * een WMS duurt lang vanwege de capabilities uitvraag en versie
161 	 * onderhandeling.
162 	 */
163 	private transient Map<String, WebMapServer> wmsServersCache;
164 
165 	/*
166 	 * (non-Javadoc)
167 	 *
168 	 * @see javax.servlet.GenericServlet#destroy()
169 	 */
170 	@Override
171 	public void destroy() {
172 		this.bgWMSCache.clear();
173 		this.bgWMSLuFoCache.clear();
174 		this.wmsServersCache.clear();
175 		this.legendCache.clear();
176 		this.fgWMSCache.clear();
177 		this.featInfoCache.clear();
178 		super.destroy();
179 	}
180 
181 	/**
182 	 * Teken een schaalbalk in de (kaart) afbeelding. meter based CRS only.
183 	 *
184 	 * @param bbox
185 	 *            the bbox
186 	 * @param image
187 	 *            afbeelding waarin de schaalbalk wordt getekend
188 	 *
189 	 * @todo only works for CRS in meters
190 	 */
191 	private void drawScaleBar(final BufferedImage image, final BoundingBox bbox) {
192 		// CHECKSTYLE.OFF: MagicNumber - pixel layout
193 		// start positie in px
194 		final int xOffset = 10;
195 		final int fontSize = 12;
196 		final int yOffset = MAP_DIMENSION - xOffset;
197 		final Graphics2D g = image.createGraphics();
198 		g.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
199 		        RenderingHints.VALUE_ANTIALIAS_ON);
200 		g.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING,
201 		        RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
202 
203 		// scale crs units per px
204 		final double scale = bbox.getWidth() / MAP_DIMENSION;
205 		// max lengte in px van schaalbalk
206 		int barLength = MAP_DIMENSION / 2;
207 		// crs units
208 		int dist = (int) (barLength * scale);
209 		// logaritmisch lengte afronden
210 		final int digits = (int) (Math.log(dist) / Math.log(10));
211 		final double pow10 = Math.pow(10, digits);
212 		final int iRounded = (int) (dist / pow10);
213 		int barLen = 1;
214 		if (iRounded > 5) {
215 			barLen = 5;
216 		} else if (iRounded > 2) {
217 			barLen = 2;
218 		}
219 		dist = (int) (barLen * pow10);
220 		barLength = (int) (dist / scale);
221 
222 		// TODO CRS units gebruiken
223 		// bbox.getCoordinateReferenceSystem().getCoordinateSystem().getAxis(0).getUnit()...
224 		String units = "m";
225 		if (dist >= 1000) {
226 			dist = dist / 1000;
227 			units = "km";
228 		}
229 
230 		g.setColor(Color.BLACK);
231 		g.setStroke(new BasicStroke(2, BasicStroke.CAP_ROUND,
232 		        BasicStroke.JOIN_ROUND));
233 		g.drawLine(xOffset, yOffset, xOffset + barLength, yOffset);
234 		g.drawLine(xOffset, yOffset, xOffset, yOffset - fontSize);
235 		g.drawLine(xOffset + barLength, yOffset, xOffset + barLength, yOffset
236 		        - fontSize);
237 
238 		final Font font = new Font(Font.SANS_SERIF, Font.BOLD, fontSize);
239 		final FontMetrics metrics = g.getFontMetrics(font);
240 		g.setFont(font);
241 		g.drawString(
242 		        dist + units,
243 		        (xOffset + (barLength / 2))
244 		                - (metrics.stringWidth(dist + units) / 2), yOffset
245 		                - metrics.getDescent() - 2);
246 		// CHECKSTYLE.ON: MagicNumber
247 	}
248 
249 	/**
250 	 * Achtergrondkaart ophalen en opslaan in de cache.
251 	 *
252 	 * @param bbox
253 	 *            the bbox
254 	 * @param baseMapType
255 	 *            het type basemap, {@code "luchtfoto"} of (default)
256 	 *            {@code "topografie"}
257 	 * @param response
258 	 *            de servlet response die terug gaat naar de client
259 	 * @return background/basemap image
260 	 * @throws ServletException
261 	 *             Geeft aan dat er een fout is opgetreden bij het benaderen van
262 	 *             de achtergrondgrond WMS service
263 	 */
264 	private BufferedImage getBackGroundMap(final BoundingBox bbox,
265 	        final String baseMapType, HttpServletResponse response)
266 	        throws ServletException {
267 
268 		GetMapRequest map;
269 
270 		switch (baseMapType.toLowerCase()) {
271 		case "luchtfoto":
272 			this.setCookie(response, COOKIE_baselyr, "luchtfoto");
273 			if (this.bgWMSLuFoCache.containsKey(bbox)) {
274 				// ophalen uit cache
275 				LOGGER.debug("Achtergrond " + baseMapType
276 				        + " afbeelding uit de cache serveren.");
277 				return this.bgWMSLuFoCache.getImage(bbox);
278 			}
279 			map = this.lufoWMS.createGetMapRequest();
280 			if (this.lufoWMSlayers != null) {
281 				for (final String lyr : this.lufoWMSlayers) {
282 					// per laag toevoegen met de default style
283 					map.addLayer(lyr, "");
284 				}
285 			} else {
286 				// alle lagen toevoegen
287 				for (final Layer layer : WMSUtils.getNamedLayers(this.lufoWMS
288 				        .getCapabilities())) {
289 					map.addLayer(layer);
290 				}
291 			}
292 			map.setFormat("image/jpeg");
293 			break;
294 		case "topografie":
295 			// implicit fall thru naar default
296 		default:
297 			this.setCookie(response, COOKIE_baselyr, "topografie");
298 			if (this.bgWMSCache.containsKey(bbox)) {
299 				// ophalen uit cache
300 				LOGGER.debug("Achtergrond " + baseMapType
301 				        + " afbeelding uit de cache serveren.");
302 				return this.bgWMSCache.getImage(bbox);
303 			}
304 			map = this.bgWMS.createGetMapRequest();
305 			if (this.bgWMSlayers != null) {
306 				for (final String lyr : this.bgWMSlayers) {
307 					// per laag toevoegen met de default style
308 					map.addLayer(lyr, "");
309 				}
310 			} else {
311 				// alle lagen toevoegen
312 				for (final Layer layer : WMSUtils.getNamedLayers(this.bgWMS
313 				        .getCapabilities())) {
314 					map.addLayer(layer);
315 				}
316 			}
317 			map.setFormat("image/png");
318 		}
319 
320 		map.setDimensions(MAP_DIMENSION, MAP_DIMENSION);
321 		map.setTransparent(true);
322 		map.setBGColour("0xffffff");
323 		map.setExceptions("application/vnd.ogc.se_inimage");
324 		map.setSRS("EPSG:28992");
325 		map.setBBox(bbox);
326 
327 		LOGGER.debug("Achtergrond WMS url is: " + map.getFinalURL());
328 
329 		try {
330 			final GetMapResponse mapResponse = this.bgWMS.issueRequest(map);
331 			final BufferedImage image = ImageIO.read(mapResponse
332 			        .getInputStream());
333 			switch (baseMapType.toLowerCase()) {
334 			case "luchtfoto":
335 				this.bgWMSLuFoCache.put(bbox, image, SECONDS_TO_CACHE_ELEMENTS);
336 				break;
337 			case "topografie":
338 				// implicit fall thru naar default
339 			default:
340 				this.bgWMSCache.put(bbox, image, SECONDS_TO_CACHE_ELEMENTS);
341 				break;
342 			}
343 
344 			if (LOGGER.isDebugEnabled()) {
345 				// achtergrond plaatje bewaren in debug modus
346 				final File temp = File.createTempFile(
347 				        "bgwms",
348 				        ".png",
349 				        new File(this.getServletContext().getRealPath(
350 				                MAP_CACHE_DIR.code)));
351 				temp.deleteOnExit();
352 				ImageIO.write(image, "png", temp);
353 			}
354 
355 			return image;
356 		} catch (ServiceException | IOException e) {
357 			LOGGER.error(
358 			        "Er is een fout opgetreden bij het benaderen van de achtergrond WMS service.",
359 			        e);
360 			throw new ServletException(e);
361 		}
362 
363 	}
364 
365 	/**
366 	 * zoekt of maakt de gevraagde WebMapServer.
367 	 *
368 	 * @param lyrDesc
369 	 *            de layerdescriptor met de WMS informatie
370 	 * @return the cached wms
371 	 * @throws ServiceException
372 	 *             the service exception
373 	 * @throws IOException
374 	 *             Signals that an I/O exception has occurred.
375 	 */
376 	private WebMapServer getCachedWMS(final LayerDescriptor lyrDesc)
377 	        throws ServiceException, IOException {
378 		if (this.wmsServersCache.containsKey(lyrDesc.getUrl())) {
379 			LOGGER.debug("WMS gevonden in cache.");
380 			return this.wmsServersCache.get(lyrDesc.getUrl());
381 		} else {
382 			LOGGER.debug("Aanmaken van nieuwe WMS (inclusief versie onderhandeling).");
383 			final WebMapServer fgWMS = new WebMapServer(new URL(
384 			        lyrDesc.getUrl()));
385 			this.wmsServersCache.put(lyrDesc.getUrl(), fgWMS);
386 			return fgWMS;
387 		}
388 	}
389 
390 	/**
391 	 * Haalt de feature info op.
392 	 *
393 	 * @param bbox
394 	 *            the bbox
395 	 * @param lyrDesc
396 	 *            de layerdescriptor met de WMS informatie
397 	 * @return Een string met feature info
398 	 * @throws ServiceException
399 	 *             Geeft aan dat er een fout is opgetreden tijden het benaderen
400 	 *             van de WMS
401 	 * @throws IOException
402 	 *             Signals that an I/O exception has occurred.
403 	 */
404 	private String getFeatureInfo(final BoundingBox bbox,
405 	        final LayerDescriptor lyrDesc) throws ServiceException, IOException {
406 		final BboxLayerCacheKey key = new BboxLayerCacheKey(bbox, lyrDesc);
407 		if (this.featInfoCache.containsKey(key)) {
408 			// ophalen uit cache
409 			final CachableString fInfo = this.featInfoCache.get(key);
410 			if (null != fInfo) {
411 				// dit kan null zijn in het geval het item verlopen is
412 				LOGGER.debug("FeatureInfo uit de cache serveren.");
413 				return fInfo.getItem();
414 			}
415 		}
416 
417 		try {
418 			final GetFeatureInfoRequest getFeatureInfoRequest = this
419 			        .getCachedWMS(lyrDesc).createGetFeatureInfoRequest(
420 			                this.getMapRequest);
421 
422 			final String[] layerNames = lyrDesc.getLayers().split(",\\s*");
423 			final Set<Layer> queryLayers = new HashSet<>();
424 			final WMSCapabilities caps = this.getCachedWMS(lyrDesc)
425 			        .getCapabilities();
426 
427 			for (final Layer wmsLyr : caps.getLayerList()) {
428 				if ((wmsLyr.getName() != null)
429 				        && (wmsLyr.getName().length() != 0)) {
430 					for (final String layerName : layerNames) {
431 						if (wmsLyr.getName().equalsIgnoreCase(layerName)) {
432 							queryLayers.add(wmsLyr);
433 						}
434 					}
435 				}
436 			}
437 			getFeatureInfoRequest.setQueryLayers(queryLayers);
438 			getFeatureInfoRequest.setFeatureCount(MAX_FEATURE_COUNT);
439 			getFeatureInfoRequest.setQueryPoint(MAP_DIMENSION_MIDDLE,
440 			        MAP_DIMENSION_MIDDLE);
441 			getFeatureInfoRequest.setInfoFormat(type.toString());
442 			LOGGER.debug("WMS feature info request url is: "
443 			        + getFeatureInfoRequest.getFinalURL());
444 			final GetFeatureInfoResponse response = this.getCachedWMS(lyrDesc)
445 			        .issueRequest(getFeatureInfoRequest);
446 
447 			final String html = FeatureInfoResponseConverter
448 			        .convertToHTMLTable(response.getInputStream(), type,
449 			                lyrDesc);
450 			this.featInfoCache.put(key,
451 			        new CachableString(html, System.currentTimeMillis()
452 			                + MILLISECONDS_TO_CACHE_ELEMENTS));
453 			return html;
454 
455 		} catch (final UnsupportedOperationException u) {
456 			LOGGER.warn("De WMS server ("
457 			        + this.getCachedWMS(lyrDesc).getInfo().getTitle()
458 			        + ") ondersteund geen GetFeatureInfoRequest.", u);
459 			return "";
460 		}
461 	}
462 
463 	/**
464 	 * voorgrondkaart ophalen.
465 	 *
466 	 * @param bbox
467 	 *            the bbox
468 	 * @param lyrDesc
469 	 *            de layerdescriptor met de WMS informatie
470 	 * @return voorgrond afbeelding
471 	 * @throws ServletException
472 	 *             Geeft aan dat er een fout is opgetreden bij het benaderen van
473 	 *             de voorgrond WMS service
474 	 */
475 	private BufferedImage getForeGroundMap(final BoundingBox bbox,
476 	        final LayerDescriptor lyrDesc) throws ServletException {
477 
478 		final BboxLayerCacheKey key = new BboxLayerCacheKey(bbox, lyrDesc);
479 		if (this.fgWMSCache.containsKey(key)) {
480 			// ophalen uit cache
481 			final CacheImage cImg = this.fgWMSCache.get(key);
482 			if (null != cImg) {
483 				LOGGER.debug("Voorgrond afbeelding uit de cache serveren.");
484 				return cImg.getImage();
485 			}
486 		}
487 
488 		// wms request doen
489 		try {
490 			this.getMapRequest = this.getCachedWMS(lyrDesc)
491 			        .createGetMapRequest();
492 			final String[] layerNames = lyrDesc.getLayers().split(",\\s*");
493 			final String[] styleNames = lyrDesc.getStyles().split(",\\s*");
494 
495 			for (int l = 0; l < layerNames.length; l++) {
496 				this.getMapRequest.addLayer(layerNames[l], styleNames[l]);
497 			}
498 			this.getMapRequest.setFormat("image/png");
499 			this.getMapRequest.setDimensions(MAP_DIMENSION, MAP_DIMENSION);
500 			this.getMapRequest.setTransparent(true);
501 			this.getMapRequest.setSRS("EPSG:28992");
502 			this.getMapRequest.setBBox(bbox);
503 			this.getMapRequest.setExceptions("application/vnd.ogc.se_inimage");
504 			this.getMapRequest.setBGColour("0xffffff");
505 			LOGGER.debug("Voorgrond WMS url is: "
506 			        + this.getMapRequest.getFinalURL());
507 
508 			// thema/voorgrond ophalen
509 			final GetMapResponse response = this.getCachedWMS(lyrDesc)
510 			        .issueRequest(this.getMapRequest);
511 			final BufferedImage image = ImageIO.read(response.getInputStream());
512 			this.drawScaleBar(image, bbox);
513 			this.fgWMSCache.put(key, new CacheImage(image,
514 			        SECONDS_TO_CACHE_ELEMENTS));
515 
516 			if (LOGGER.isDebugEnabled()) {
517 				// voorgrond plaatje bewaren in debug modus
518 				final File temp = File.createTempFile(
519 				        "fgwms",
520 				        ".png",
521 				        new File(this.getServletContext().getRealPath(
522 				                MAP_CACHE_DIR.code)));
523 				temp.deleteOnExit();
524 				ImageIO.write(image, "png", temp);
525 			}
526 			return image;
527 		} catch (ServiceException | IOException e) {
528 			LOGGER.error(
529 			        "Er is een fout opgetreden bij het benaderen van de voorgrond WMS service.",
530 			        e);
531 			throw new ServletException(e);
532 		}
533 	}
534 
535 	/**
536 	 * Haalt de legenda op voor de thema laag.
537 	 *
538 	 * @param lyrDesc
539 	 *            de layerdescriptor met de WMS informatie
540 	 * @return een array met legenda afbeeldings bestanden
541 	 * @throws ServiceException
542 	 *             Geeft aan dat er een fout is opgetreden tijdens het benaderen
543 	 *             van de WMS
544 	 * @throws IOException
545 	 *             Signals that an I/O exception has occurred.
546 	 */
547 	private File[] getLegends(final LayerDescriptor lyrDesc)
548 	        throws ServiceException, IOException {
549 		if (null == this.getCachedWMS(lyrDesc).getCapabilities().getRequest()
550 		        .getGetLegendGraphic()) {
551 			LOGGER.debug("getGetLegendGraphic tested null, server ondersteund geen getGetLegendGraphic request.");
552 			return this.getLegendsFromLayerStyles(lyrDesc);
553 		} else {
554 			return this.getLegendsFromService(lyrDesc);
555 		}
556 	}
557 
558 	/**
559 	 * get legenda afbeeldingen door gebruik te maken legendUrl.
560 	 *
561 	 * @param lyrDesc
562 	 *            de layerdescriptor met de WMS informatie
563 	 * @return een array met legenda afbeeldings bestanden
564 	 * @throws ServiceException
565 	 *             Geeft aan dat er een fout is opgetreden tijden het benaderen
566 	 *             van de WMS
567 	 * @throws IOException
568 	 *             Signals that an I/O exception has occurred.
569 	 */
570 	private File[] getLegendsFromLayerStyles(final LayerDescriptor lyrDesc)
571 	        throws ServiceException, IOException {
572 		final String[] layerNames = lyrDesc.getLayers().split(",\\s*");
573 		final String[] styleNames = lyrDesc.getStyles().split(",\\s*");
574 		final File[] legends = new File[layerNames.length];
575 
576 		for (int l = 0; l < layerNames.length; l++) {
577 			final String key = layerNames[l] + "::" + styleNames[l];
578 			if (this.legendCache.containsKey(key)) {
579 				// in de cache kijken of we deze legenda afbeelding nog
580 				// hebben
581 				final CacheImage cImg = this.legendCache.get(key);
582 				if (null != cImg) {
583 					// element zou verlopen kunnen zijn
584 					legends[l] = new File(cImg.getName());
585 					if (!legends[l].exists()) {
586 						// (mogelijk) is het bestand gewist..
587 						ImageIO.write(this.legendCache.get(key).getImage(),
588 						        "png", legends[l]);
589 					}
590 					LOGGER.debug("Legenda bestand uit cache: "
591 					        + legends[l].getAbsolutePath());
592 				}
593 			} else {
594 				for (final Layer layer : this.getCachedWMS(lyrDesc)
595 				        .getCapabilities().getLayerList()) {
596 					if (layerNames[l].equalsIgnoreCase(layer.getName())) {
597 						// layer gevonden
598 						for (final StyleImpl style : layer.getStyles()) {
599 							if (styleNames[l].equalsIgnoreCase(style.getName())) {
600 								// style gevonden, eerste legenda ophalen
601 								final String legendUrl = (String) style
602 								        .getLegendURLs().get(0);
603 								LOGGER.debug("Legenda url uit capabilities is: "
604 								        + legendUrl);
605 								if (this.isNotNullNotEmptyNotWhiteSpaceOnly(legendUrl)) {
606 									try {
607 										legends[l] = this.cacheLegend(ImageIO
608 										        .read(new URL(legendUrl)), key);
609 									} catch (final MalformedURLException e) {
610 										LOGGER.warn(
611 										        "Er werd geen geldige URL voor de legenda gevonden.",
612 										        e);
613 									}
614 								}
615 							}
616 						}
617 					}
618 				}
619 			}
620 		}
621 		return legends;
622 	}
623 
624 	/**
625 	 * get legenda afbeeldingen door gebruik te maken van
626 	 * GetLegendGraphicRequest.
627 	 *
628 	 * @param lyrDesc
629 	 *            de layerdescriptor met de WMS informatie
630 	 * @return een array met legenda afbeeldings bestanden
631 	 * @throws ServiceException
632 	 *             Geeft aan dat er een fout is opgetreden tijden het benaderen
633 	 *             van de WMS
634 	 * @throws IOException
635 	 *             Signals that an I/O exception has occurred.
636 	 */
637 	private File[] getLegendsFromService(final LayerDescriptor lyrDesc)
638 	        throws ServiceException, IOException {
639 		final String[] layerNames = lyrDesc.getLayers().split(",\\s*");
640 		final String[] styleNames = lyrDesc.getStyles().split(",\\s*");
641 		final File[] legends = new File[layerNames.length];
642 
643 		try {
644 			final GetLegendGraphicRequest legend = this.getCachedWMS(lyrDesc)
645 			        .createGetLegendGraphicRequest();
646 			for (int l = 0; l < layerNames.length; l++) {
647 				final String key = layerNames[l] + "::" + styleNames[l];
648 
649 				if (this.legendCache.containsKey(key)) {
650 					// in de cache kijken of we deze legenda afbeelding nog
651 					// hebben
652 					final CacheImage cImg = this.legendCache.get(key);
653 					if (null != cImg) {
654 						// element zou verlopen kunnen zijn
655 						legends[l] = new File(cImg.getName());
656 						if (!legends[l].exists()) {
657 							// (mogelijk) is het bestand gewist..
658 							ImageIO.write(this.legendCache.get(key).getImage(),
659 							        "png", legends[l]);
660 						}
661 						LOGGER.debug("Legenda bestand uit cache: "
662 						        + legends[l].getAbsolutePath());
663 					}
664 				} else {
665 					// legenda opvragen
666 					legend.setLayer(layerNames[l]);
667 					legend.setStyle(styleNames[l]);
668 					legend.setFormat("image/png");
669 					legend.setExceptions("application/vnd.ogc.se_inimage");
670 
671 					LOGGER.debug("Voorgrond WMS legenda url is: "
672 					        + legend.getFinalURL());
673 					final GetLegendGraphicResponse response = this
674 					        .getCachedWMS(lyrDesc).issueRequest(legend);
675 					legends[l] = this.cacheLegend(
676 					        ImageIO.read(response.getInputStream()), key);
677 				}
678 			}
679 		} catch (final UnsupportedOperationException u) {
680 			LOGGER.warn("De WMS server ("
681 			        + this.getCachedWMS(lyrDesc).getInfo().getTitle()
682 			        + ") ondersteund geen GetLegendGraphicRequest.", u);
683 			return null;
684 		}
685 		return legends;
686 	}
687 
688 	/**
689 	 * kaart maken op basis van de opgehaalde afbeeldingen.
690 	 *
691 	 * @param imageVoorgrond
692 	 *            de voorgrondkaart
693 	 * @param imageAchtergrond
694 	 *            de achtergrondgrondkaart
695 	 * @param alpha
696 	 *            alpha transparantie van de voorgrondkaart
697 	 * @return de file met de afbeelding
698 	 * @throws IOException
699 	 *             Signals that an I/O exception has occurred.
700 	 */
701 	private File getMap(final BufferedImage imageVoorgrond,
702 	        final BufferedImage imageAchtergrond, final float alpha)
703 	        throws IOException {
704 
705 		final BufferedImage composite = new BufferedImage(MAP_DIMENSION,
706 		        MAP_DIMENSION, BufferedImage.TYPE_INT_ARGB);
707 		final Graphics2D g = composite.createGraphics();
708 
709 		g.drawImage(imageAchtergrond, 0, 0, null);
710 		if (imageVoorgrond != null) {
711 			final float[] scales = { 1f, 1f, 1f, alpha };
712 			final RescaleOp rop = new RescaleOp(scales, new float[4], null);
713 			g.drawImage(imageVoorgrond, rop, 0, 0);
714 			// zoeklocatie intekenen met plaatje
715 			final BufferedImage infoImage = ImageIO.read(new File(this
716 			        .getClass().getClassLoader().getResource("info.png")
717 			        .getFile()));
718 			// CHECKSTYLE.OFF: MagicNumber - dit zijn midden en hoogte van het
719 			// plaatje "info.png"
720 			g.drawImage(infoImage, MAP_DIMENSION_MIDDLE - 16,
721 			        MAP_DIMENSION_MIDDLE - 36, null);
722 			// CHECKSTYLE.ON: MagicNumber
723 		}
724 		// opslaan van plaatje zodat de browser het op kan halen
725 		final File kaartAfbeelding = File.createTempFile(
726 		        "wmscombined",
727 		        ".png",
728 		        new File(this.getServletContext().getRealPath(
729 		                MAP_CACHE_DIR.code)));
730 		kaartAfbeelding.deleteOnExit();
731 		ImageIO.write(composite, "png", kaartAfbeelding);
732 		g.dispose();
733 		return kaartAfbeelding;
734 	}
735 
736 	/**
737 	 * cache een legenda plaatje.
738 	 *
739 	 * @param image
740 	 *            de afbeelding om op te slaan
741 	 * @param key
742 	 *            de sleutel
743 	 * @return het bestand met opgeslagen afbeelding
744 	 * @throws IOException
745 	 *             Signals that an I/O exception has occurred.
746 	 */
747 	private File cacheLegend(final BufferedImage image, final String key)
748 	        throws IOException {
749 		// op schijf opslaan
750 		final File legend = File.createTempFile("legenda", ".png", new File(
751 		        this.getServletContext().getRealPath(MAP_CACHE_DIR.code)));
752 		legend.deleteOnExit();
753 		ImageIO.write(image, "png", legend);
754 		// in de cache opslaan
755 		this.legendCache.put(
756 		        key,
757 		        new CacheImage(image, legend.getAbsolutePath(), System
758 		                .currentTimeMillis()
759 		                + (MILLISECONDS_TO_CACHE_ELEMENTS * 24)));
760 		LOGGER.debug("Legenda bestand opgeslagen: " + legend.getAbsolutePath());
761 		return legend;
762 	}
763 
764 	/*
765 	 * (non-Javadoc)
766 	 *
767 	 * @see
768 	 * nl.mineleni.cbsviewer.servlet.AbstractBaseServlet#init(javax.servlet.
769 	 * ServletConfig)
770 	 */
771 	@Override
772 	public void init(final ServletConfig config) throws ServletException {
773 		super.init(config);
774 		final ServletContext ctx = this.getServletContext();
775 		try {
776 			this.bgWMSCache = new WMSCache(ctx.getRealPath(MAP_CACHE_DIR.code),
777 			        NUMBER_CACHE_ELEMENTS);
778 		} catch (final IOException e) {
779 			LOGGER.error(
780 			        "Inititalisatie fout voor de achtergrond topografie cache.",
781 			        e);
782 		}
783 
784 		try {
785 			this.bgWMSLuFoCache = new WMSCache(
786 			        ctx.getRealPath(MAP_CACHE_DIR.code), NUMBER_CACHE_ELEMENTS);
787 		} catch (final IOException e) {
788 			LOGGER.error(
789 			        "Inititalisatie fout voor de achtergrond luchtfoto cache.",
790 			        e);
791 		}
792 
793 		this.legendCache = new Cache<>(NUMBER_CACHE_ELEMENTS);
794 
795 		this.featInfoCache = new Cache<>(NUMBER_CACHE_ELEMENTS);
796 
797 		this.fgWMSCache = new Cache<>(NUMBER_CACHE_ELEMENTS);
798 
799 		// achtergrond kaart
800 		final String bgCapabilitiesURL = config
801 		        .getInitParameter("bgCapabilitiesURL");
802 		LOGGER.debug("WMS capabilities url van achtergrond kaart: "
803 		        + bgCapabilitiesURL);
804 		try {
805 			this.bgWMS = new WebMapServer(new URL(bgCapabilitiesURL));
806 		} catch (final MalformedURLException e) {
807 			LOGGER.error(
808 			        "Een url die gebruikt wordt voor de topografie WMS capabilities is misvormd",
809 			        e);
810 			throw new ServletException(e);
811 		} catch (final ServiceException e) {
812 			LOGGER.error(
813 			        "Er is een service exception (WMS server fout) opgetreden bij het ophalen van de topografie WMS capabilities",
814 			        e);
815 			throw new ServletException(e);
816 		} catch (final IOException e) {
817 			LOGGER.error(
818 			        "Er is een I/O fout opgetreden bij benaderen van de topografie WMS services",
819 			        e);
820 			throw new ServletException(e);
821 		}
822 		final String bgWMSlyrs = config.getInitParameter("bgWMSlayers");
823 		LOGGER.debug("Achtergrond kaartlagen topografie: " + bgWMSlyrs);
824 		if ((bgWMSlyrs != null) && (bgWMSlyrs.length() > 0)) {
825 			this.bgWMSlayers = bgWMSlyrs.split("[,]\\s*");
826 		}
827 
828 		// achtergrond luchtfoto
829 		final String lufoCapabilitiesURL = config
830 		        .getInitParameter("lufoCapabilitiesURL");
831 		LOGGER.debug("WMS capabilities url van achtergrond luchtfoto: "
832 		        + lufoCapabilitiesURL);
833 		try {
834 			this.lufoWMS = new WebMapServer(new URL(lufoCapabilitiesURL));
835 		} catch (final MalformedURLException e) {
836 			LOGGER.error(
837 			        "De url die gebruikt wordt voor de luchtfoto WMS capabilities is misvormd",
838 			        e);
839 			throw new ServletException(e);
840 		} catch (final ServiceException e) {
841 			LOGGER.error(
842 			        "Er is een service exception (WMS server fout) opgetreden bij het ophalen van de luchtfoto WMS capabilities",
843 			        e);
844 			throw new ServletException(e);
845 		} catch (final IOException e) {
846 			LOGGER.error(
847 			        "Er is een I/O fout opgetreden bij benaderen van de luchtfoto WMS services",
848 			        e);
849 			throw new ServletException(e);
850 		}
851 		final String lufoWMSlyrs = config.getInitParameter("lufoWMSlayers");
852 		LOGGER.debug("Achtergrond kaartlagen luchtfoto: " + lufoWMSlyrs);
853 		if ((lufoWMSlyrs != null) && (lufoWMSlyrs.length() > 0)) {
854 			this.lufoWMSlayers = lufoWMSlyrs.split("[,]\\s*");
855 		}
856 
857 		// voorgond feature info response type
858 		final String mType = config.getInitParameter("featureInfoType");
859 		LOGGER.debug("voorgrond kaartlagen mimetype: " + mType);
860 		if ((mType != null) && (mType.length() > 0)) {
861 			type = CONVERTER_TYPE.valueOf(mType);
862 		}
863 		// init servers cache
864 		this.wmsServersCache = new ConcurrentHashMap<String, WebMapServer>();
865 	}
866 
867 	/*
868 	 * (non-Javadoc)
869 	 *
870 	 * @see javax.servlet.http.HttpServlet#service(javax.servlet.ServletRequest,
871 	 * javax.servlet.ServletResponse)
872 	 */
873 	@Override
874 	protected void service(final HttpServletRequest request,
875 	        final HttpServletResponse response) throws ServletException,
876 	        IOException {
877 
878 		final int[] dXcoordYCoordStraal = this.parseLocation(request);
879 		final int xcoord = dXcoordYCoordStraal[0];
880 		final int ycoord = dXcoordYCoordStraal[1];
881 		final int straal = dXcoordYCoordStraal[2];
882 		final BoundingBox bbox = SpatialUtil.calcRDBBOX(xcoord, ycoord, straal);
883 
884 		this.setCookie(response, COOKIE_X, xcoord);
885 		this.setCookie(response, COOKIE_Y, ycoord);
886 		this.setCookie(response, COOKIE_S, straal);
887 
888 		float alpha = 0.8f;
889 		final String trans = request.getParameter(REQ_PARAM_FGMAP_ALPHA.code);
890 		if (this.isNotNullNotEmptyNotWhiteSpaceOnly(trans)) {
891 			// CHECKSTYLE.OFF: MagicNumber - default alpha transparantie is 80%
892 			try {
893 				alpha = 1 - (Float.parseFloat(trans) / 100);
894 				if ((0 > alpha) && (alpha > 1)) {
895 					alpha = 0.8f;
896 				}
897 			} catch (final NumberFormatException n) {
898 				alpha = 0.8f;
899 			}
900 			// CHECKSTYLE.ON: MagicNumber
901 		}
902 		LOGGER.debug("Transparantie / alpha ingesteld op:" + alpha);
903 
904 		String basemaptype = "topografie";
905 		final String mType = request.getParameter(REQ_PARAM_BGMAP.code);
906 		if (this.isNotNullNotEmptyNotWhiteSpaceOnly(mType)) {
907 			basemaptype = mType;
908 		}
909 		final BufferedImage bg = this.getBackGroundMap(bbox, basemaptype,
910 		        response);
911 
912 		BufferedImage fg = null;
913 		final String mapid = request.getParameter(REQ_PARAM_MAPID.code);
914 		if (this.isNotNullNotEmptyNotWhiteSpaceOnly(mapid)) {
915 			this.setCookie(response, COOKIE_mapid, mapid);
916 			final LayerDescriptor layer = this.layers.getLayerByID(mapid);
917 			request.setAttribute("mapname", layer.getName());
918 			LOGGER.debug("LayerDescriptor::Name is: " + layer.getName());
919 
920 			final String fgCapabilitiesURL = layer.getUrl();
921 			LOGGER.debug("WMS capabilities url van voorgrond kaart: "
922 			        + fgCapabilitiesURL);
923 			try {
924 				fg = this.getForeGroundMap(bbox, layer);
925 				final File[] legendas = this.getLegends(layer);
926 				final String fInfo = this.getFeatureInfo(bbox, layer);
927 				request.setAttribute(REQ_PARAM_MAPID.code, mapid);
928 				request.setAttribute(REQ_PARAM_LEGENDAS.code, legendas);
929 				request.setAttribute(REQ_PARAM_FEATUREINFO.code, fInfo);
930 				request.setAttribute(REQ_PARAM_DOWNLOADLINK.code,
931 				        layer.getLink());
932 			} catch (final ServiceException e) {
933 				LOGGER.error(
934 				        "Er is een service exception opgetreden bij benaderen van de voorgrond WMS",
935 				        e);
936 				throw new ServletException(e);
937 			} catch (final MalformedURLException e) {
938 				LOGGER.error(
939 				        "De url die gebruikt wordt voor de WMS capabilities is misvormd.",
940 				        e);
941 				throw new ServletException(e);
942 			}
943 		}
944 
945 		final File kaart = this.getMap(fg, bg, alpha);
946 
947 		request.setAttribute(REQ_PARAM_CACHEDIR.code, MAP_CACHE_DIR.code);
948 		request.setAttribute(REQ_PARAM_KAART.code, kaart);
949 		request.setAttribute(REQ_PARAM_BGMAP.code, basemaptype);
950 	}
951 }