All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.geomajas.layer.tms.TmsLayer Maven / Gradle / Ivy

There is a newer version: 1.18.5
Show newest version
/*
 * This is part of Geomajas, a GIS framework, http://www.geomajas.org/.
 *
 * Copyright 2008-2015 Geosparc nv, http://www.geosparc.com/, Belgium.
 *
 * The program is available in open source according to the GNU Affero
 * General Public License. All contributions in this program are covered
 * by the Geomajas Contributors License Agreement. For full licensing
 * details, see LICENSE.txt in the project root.
 */

package org.geomajas.layer.tms;

import java.util.ArrayList;
import java.util.List;

import javax.annotation.PostConstruct;

import org.geomajas.annotation.Api;
import org.geomajas.configuration.RasterLayerInfo;
import org.geomajas.geometry.Bbox;
import org.geomajas.geometry.CrsTransform;
import org.geomajas.global.ExceptionCode;
import org.geomajas.global.GeomajasException;
import org.geomajas.layer.RasterLayer;
import org.geomajas.layer.common.proxy.LayerAuthentication;
import org.geomajas.layer.common.proxy.ProxyAuthentication;
import org.geomajas.layer.common.proxy.ProxyLayerSupport;
import org.geomajas.layer.tile.RasterTile;
import org.geomajas.layer.tile.TileCode;
import org.geomajas.layer.tms.mvc.TmsProxyController;
import org.geomajas.layer.tms.tile.SimpleTmsUrlBuilder;
import org.geomajas.layer.tms.tile.TileMapUrlBuilder;
import org.geomajas.layer.tms.tile.TileService;
import org.geomajas.layer.tms.tile.TileServiceState;
import org.geomajas.layer.tms.tile.TileUrlBuilder;
import org.geomajas.layer.tms.xml.TileMap;
import org.geomajas.plugin.caching.service.CacheManagerService;
import org.geomajas.security.SecurityContext;
import org.geomajas.service.DispatcherUrlService;
import org.geomajas.service.GeoService;
import org.opengis.geometry.MismatchedDimensionException;
import org.opengis.referencing.crs.CoordinateReferenceSystem;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;

import com.vividsolutions.jts.geom.Envelope;

/**
 * A raster layer implementation that supports the TMS "standard". This layer has the advantage that no
 * {@link RasterLayerInfo} needs to be configured, as the target TMS url should contain a description of the TMS layer.
 * 
 * @author Pieter De Graef
 * @author Kristof Heirwegh
 * @since 1.0.0
 */
@Api
public class TmsLayer implements RasterLayer, ProxyLayerSupport {

	private static final long DEFAULT_COOLDOWN_TIME = 60000; // millis
	
	// a dummy layer info in case the service is unavailable !
	private static final RasterLayerInfo UNUSABLE_LAYER_INFO = new RasterLayerInfo();
	
	static {
		UNUSABLE_LAYER_INFO.setTileWidth(256);
		UNUSABLE_LAYER_INFO.setTileHeight(256);
		UNUSABLE_LAYER_INFO.setCrs("EPSG:4326");
	}

	private final Logger log = LoggerFactory.getLogger(TmsLayer.class);

	private String id;

	private String baseTmsUrl;

	private String extension = "jpg";

	private String version = "1.0.0";

	private TileMap tileMap;

	private RasterLayerInfo layerInfo;

	private LayerAuthentication authentication;

	private boolean useProxy;

	private boolean useCache;

	private boolean usable;

	private long lastInitRetry;

	private long cooldownTimeBetweenInitializationRetries = DEFAULT_COOLDOWN_TIME;

	// Spring services:

	@Autowired
	private GeoService geoService;

	@Autowired
	private TmsConfigurationService configurationService;

	@Autowired(required = false)
	private DispatcherUrlService dispatcherUrlService;

	@Autowired(required = false)
	private CacheManagerService cacheManagerService;

	@Autowired
	private SecurityContext securityContext;

	@Autowired
	private TileService tileService;

	private TileServiceState state;

	private TileUrlBuilder urlBuilder;

	// ------------------------------------------------------------------------
	// Construction:
	// ------------------------------------------------------------------------

	/** Finish initializing the service. */
	@PostConstruct
	protected void postConstruct() throws GeomajasException {
		if (null == baseTmsUrl) {
			throw new GeomajasException(ExceptionCode.PARAMETER_MISSING, "baseTmsUrl");
		}

		// Make sure we have a base URL we can work with:
		if ((baseTmsUrl.startsWith("http://") || baseTmsUrl.startsWith("https://")) && !baseTmsUrl.endsWith("/")) {
			baseTmsUrl += "/";
		}

		// Make sure there is a correct RasterLayerInfo object:
		if (layerInfo == null || layerInfo == UNUSABLE_LAYER_INFO) {
			try {
				tileMap = configurationService.getCapabilities(this);
				version = tileMap.getVersion();
				extension = tileMap.getTileFormat().getExtension();
				layerInfo = configurationService.asLayerInfo(tileMap);
				usable = true;
			} catch (TmsLayerException e) {
				// a layer needs an info object to keep the DtoConfigurationPostProcessor happy !
				layerInfo = UNUSABLE_LAYER_INFO;
				usable = false;
				log.warn("The layer could not be correctly initialized: " + getId(), e);
			}
		} else if (extension == null) {
			throw new GeomajasException(ExceptionCode.PARAMETER_MISSING, "extension");
		}

		if (layerInfo != null) {
			// Finally prepare some often needed values:
			state = new TileServiceState(geoService, layerInfo);
			// when proxying the real url will be resolved later on, just use a simple one for now
			boolean proxying = useCache || useProxy || null != authentication;
			if (tileMap != null && !proxying) {
				urlBuilder = new TileMapUrlBuilder(tileMap);
			} else {
				urlBuilder = new SimpleTmsUrlBuilder(extension);
			}
		}
	}

	// ------------------------------------------------------------------------
	// RasterLayer implementation:
	// ------------------------------------------------------------------------

	@Override
	public RasterLayerInfo getLayerInfo() {
		return layerInfo;
	}

	@Override
	public CoordinateReferenceSystem getCrs() {
		return state.getCrs();
	}

	@Override
	public String getId() {
		return id;
	}
	
	/**
	 * The time to wait between initialization retries in case the service is unavailable.
	 * 
	 * @param cooldownTimeBetweenInitializationRetries cool down time in milliseconds
	 * @since 1.17.0
	 */
	@Api
	public void setCooldownTimeBetweenInitializationRetries(long cooldownTimeBetweenInitializationRetries) {
		this.cooldownTimeBetweenInitializationRetries = cooldownTimeBetweenInitializationRetries;
	}

	@Override
	public List paint(CoordinateReferenceSystem boundsCrs, Envelope bounds, double scale)
			throws GeomajasException {
		log.debug("Fetching TMS tiles for bounds : {}", bounds.toString());
		if (!usable) {
			retryInit();
		}
		Envelope layerBounds = bounds;
		double layerScale = scale;
		CrsTransform layerToMap = null;
		boolean needTransform = !state.getCrs().equals(boundsCrs);

		try {
			// We don't necessarily need to split into same CRS and different CRS cases, the latter implementation uses
			// identity transform if CRSs are equal for map and layer but might introduce bugs in rounding and/or
			// conversions.
			if (needTransform) {
				layerToMap = geoService.getCrsTransform(state.getCrs(), boundsCrs);
				CrsTransform mapToLayer = geoService.getCrsTransform(boundsCrs, state.getCrs());

				// Translate the map coordinates to layer coordinates, assumes equal x-y orientation
				layerBounds = geoService.transform(bounds, mapToLayer);
				layerScale = bounds.getWidth() * scale / layerBounds.getWidth();
			}
		} catch (MismatchedDimensionException e) {
			throw new GeomajasException(e, ExceptionCode.RENDER_DIMENSION_MISMATCH);
		}

		// Clip with maximum bounds and check for null:
		layerBounds = layerBounds.intersection(state.getMaxBounds());
		if (bounds.isNull()) {
			return new ArrayList();
		}

		// Get the tile level and it's size in world space:
		int tileLevel = tileService.getTileLevel(state, 1 / layerScale);
		double tileWidth = tileService.getTileWidth(state, tileLevel);
		double tileHeight = tileService.getTileHeight(state, tileLevel);

		int ymin = (int) Math.floor((layerBounds.getMinY() - state.getMaxBounds().getMinY()) / tileHeight);
		int ymax = (int) Math.ceil((layerBounds.getMaxY() - state.getMaxBounds().getMinY()) / tileHeight);
		int xmin = (int) Math.floor((layerBounds.getMinX() - state.getMaxBounds().getMinX()) / tileWidth);
		int xmax = (int) Math.ceil((layerBounds.getMaxX() - state.getMaxBounds().getMinX()) / tileWidth);

		double lowerLeftX = state.getMaxBounds().getMinX() + xmin * tileWidth;
		double lowerLeftY = state.getMaxBounds().getMinY() + ymin * tileHeight;

		List result = new ArrayList();
		for (int i = xmin; i < xmax; i++) {
			for (int j = ymin; j < ymax; j++) {
				// Get the lower-left corner of each tile:
				double x = lowerLeftX + (i - xmin) * tileWidth;
				double y = lowerLeftY + (j - ymin) * tileHeight;

				// Get the bounding box for each tile:
				Bbox tileBounds = new Bbox(x, y, tileWidth, tileHeight);
				if (needTransform) {
					// Transforming back to map coordinates will only result in a proper grid if the transformation
					// is nearly affine
					tileBounds = geoService.transform(tileBounds, layerToMap);
				}
				// Rounding to avoid white space between raster tiles lower-left becomes upper-left in inverted y-space
				Bbox screenBox = new Bbox(Math.round(scale * tileBounds.getX()), -Math.round(scale
						* tileBounds.getMaxY()), Math.round(scale * tileBounds.getMaxX())
						- Math.round(scale * tileBounds.getX()), Math.round(scale * tileBounds.getMaxY())
						- Math.round(scale * tileBounds.getY()));

				RasterTile image = new RasterTile(screenBox, getId() + "." + tileLevel + "." + i + "," + j);

				TileCode tileCode = new TileCode(tileLevel, i, j);
				image.setCode(tileCode);
				image.setUrl(formatUrl(urlBuilder.buildUrl(tileCode, getTmsTargetUrl())));
				result.add(image);
			}
		}
		return result;
	}
	
	protected boolean isUsable() {
		return usable;
	}

	// ---------------------------------------------------
	private void retryInit() throws GeomajasException {
		// do not hammer the service
		long now = System.currentTimeMillis();
		if (now > lastInitRetry + cooldownTimeBetweenInitializationRetries) {
			lastInitRetry = now;
			try {
				log.debug("Retrying to (re-)initialize layer {}", getId());
				postConstruct();
			} catch (Exception e) { // NOSONAR
				log.warn("Failed initializing layer: ", e.getMessage());
			}
		}
		if (!usable) {
			throw new GeomajasException(ExceptionCode.LAYER_CONFIGURATION_PROBLEM);
		}
	}

	private String getTmsTargetUrl() {
		if (useProxy || null != authentication || useCache) {
			if (null != dispatcherUrlService) {
				String url = dispatcherUrlService.getDispatcherUrl();
				if (!url.endsWith("/")) {
					url += "/";
				}
				return url + TmsProxyController.MAPPING_NAME + "/" + getId() + "/";
			} else {
				return "./d/" + TmsProxyController.MAPPING_NAME + "/" + getId() + "/";
			}
		} else {
			return baseTmsUrl;
		}
	}

	/**
	 * Adds userToken to url if we are proxying or caching (eg. indirect calls)
	 * 
	 * @param imageUrl
	 * @return
	 */
	private String formatUrl(String imageUrl) {
		if (useProxy || null != authentication || useCache) {
			String token = securityContext.getToken();
			if (null != token) {
				StringBuilder url = new StringBuilder(imageUrl);
				int pos = url.lastIndexOf("?");
				if (pos > 0) {
					url.append("&");
				} else {
					url.append("?");
				}
				url.append("userToken=");
				url.append(token);
				return url.toString();
			}
		}
		return imageUrl;
	}

	// ------------------------------------------------------------------------
	// Getters and setters:
	// ------------------------------------------------------------------------

	/**
	 * Set the configuration object for this layer. This is not a requirement for the TMS layer, as the target TMS URL
	 * should point to a TMS description file. If no layerInfo object is configured, it will be built using the TMS
	 * description.
	 * 
	 * @param layerInfo The new configuration object.
	 */
	public void setLayerInfo(RasterLayerInfo layerInfo) {
		this.layerInfo = layerInfo;
	}

	/**
	 * Set the layer identifier.
	 * 
	 * @param id layer id
	 */
	public void setId(String id) {
		this.id = id;
	}

	/**
	 * Get the base URL for the TMS layer.
	 * 
	 * @return The base URL for the TMS layer.
	 */
	public String getBaseTmsUrl() {
		return baseTmsUrl;
	}

	/**
	 * Set the base URL for the TMS layer.
	 * 
	 * @param baseTmsUrl The base URL for the TMS layer.
	 * @since 1.0.0
	 */
	@Api
	public void setBaseTmsUrl(String baseTmsUrl) {
		this.baseTmsUrl = baseTmsUrl;
	}

	/**
	 * Get the extension of the images/tiles to retrieve. Default value is "jpg".
	 * 
	 * @return The image extension.
	 */
	public String getExtension() {
		return extension;
	}

	/**
	 * The extension of the images/tiles to retrieve. This only needs to be supplied if you're using a
	 * {@link RasterLayerInfo} as configuration.
	 * 
	 * @param extension The extension. Default value is "jpg".
	 * @since 1.0.0
	 */
	@Api
	public void setExtension(String extension) {
		this.extension = extension;
	}

	/**
	 * Get the TMS layer version. Default value is "1.0.0".
	 * 
	 * @return The TMS version used.
	 */
	public String getVersion() {
		return version;
	}

	/**
	 * The TMS version used. This only needs to be supplied if you're using a {@link RasterLayerInfo} as configuration.
	 * 
	 * @param version The TMS version used. Default value is "1.0.0".
	 */
	public void setVersion(String version) {
		this.version = version;
	}

	/**
	 * Get the authentication object.
	 * 
	 * @return authentication object
	 * @since 1.1.0
	 */
	public LayerAuthentication getAuthentication() {
		return authentication;
	}

	/**
	 * 

* Set the authentication object. This configuration object provides support for basic and digest HTTP * authentication on the TMS server. If no HTTP authentication is required, leave this empty. *

*

* Note that there is still the option of adding a user name and password as HTTP parameters. To do that, just add * {@link #parameters}. *

* * @param authentication authentication object * @since 1.1.0 */ @Api public void setAuthentication(LayerAuthentication authentication) { this.authentication = authentication; } /** * Set whether the TMS request should use a proxy. This is automatically done when the authentication object is set. * When the TMS request is proxied, the credentials and TMS base address are hidden from the client. * * @param useProxy true when request needs to use the proxy * @since 1.1.0 */ @Api public void setUseProxy(boolean useProxy) { this.useProxy = useProxy; } /** * Set whether the TMS tiles should be cached for later use. This implies that the TMS tiles will be proxied. * * @param useCache true when request needs to be cached * @since 1.1.0 */ @Api public void setUseCache(boolean useCache) { if (null == cacheManagerService && useCache) { log.warn("The caching plugin needs to be available to cache TMS requests. Not setting useCache."); } else { this.useCache = useCache; } } @Override public ProxyAuthentication getProxyAuthentication() { return authentication; } /** * Set whether the TMS tiles should be cached for later use. This implies that the TMS tiles will be proxied. * * @return true when request needs to be cached * @since 1.1.0 */ @Api public boolean isUseCache() { return useCache; } /** * The tilemap is needed by the proxy to retrieve the correct url (when proxying / caching). * * @return tileMap * @since 1.1.0 */ public TileMap getTileMap() { return tileMap; } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy