/*
* Constellation - An open source and standard compliant SDI
* http://www.constellation-sdi.org
*
* Copyright 2014 Geomatys.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.constellation.wmts.ws;
import org.apache.sis.geometry.GeneralEnvelope;
import org.apache.sis.referencing.CommonCRS;
import org.apache.sis.referencing.IdentifiedObjects;
import org.apache.sis.xml.MarshallerPool;
import org.constellation.Cstl;
import org.constellation.ServiceDef;
import org.constellation.configuration.ConfigurationException;
import org.constellation.configuration.Layer;
import org.constellation.dto.Details;
import org.constellation.map.featureinfo.FeatureInfoFormat;
import org.constellation.map.featureinfo.FeatureInfoUtilities;
import org.constellation.portrayal.PortrayalUtil;
import org.constellation.provider.Data;
import org.constellation.util.DataReference;
import org.constellation.util.Util;
import org.constellation.ws.CstlServiceException;
import org.constellation.ws.LayerWorker;
import org.constellation.ws.MimeType;
import org.geotoolkit.coverage.finder.StrictlyCoverageFinder;
import org.geotoolkit.display.PortrayalException;
import org.geotoolkit.display2d.service.CanvasDef;
import org.geotoolkit.display2d.service.SceneDef;
import org.geotoolkit.display2d.service.ViewDef;
import org.geotoolkit.geometry.jts.JTSEnvelope2D;
import org.geotoolkit.map.MapContext;
import org.geotoolkit.ows.xml.AbstractCapabilitiesCore;
import org.geotoolkit.ows.xml.v110.AcceptFormatsType;
import org.geotoolkit.ows.xml.v110.AcceptVersionsType;
import org.geotoolkit.ows.xml.v110.BoundingBoxType;
import org.geotoolkit.ows.xml.v110.CodeType;
import org.geotoolkit.ows.xml.v110.OperationsMetadata;
import org.geotoolkit.ows.xml.v110.SectionsType;
import org.geotoolkit.ows.xml.v110.ServiceIdentification;
import org.geotoolkit.ows.xml.v110.ServiceProvider;
import org.geotoolkit.ows.xml.v110.WGS84BoundingBoxType;
import org.geotoolkit.referencing.CRS;
import org.geotoolkit.referencing.ReferencingUtilities;
import org.geotoolkit.style.MutableStyle;
import org.geotoolkit.temporal.util.TimeParser;
import org.geotoolkit.wmts.WMTSUtilities;
import org.geotoolkit.wmts.xml.WMTSMarshallerPool;
import org.geotoolkit.wmts.xml.v100.Capabilities;
import org.geotoolkit.wmts.xml.v100.ContentsType;
import org.geotoolkit.wmts.xml.v100.Dimension;
import org.geotoolkit.wmts.xml.v100.DimensionNameValue;
import org.geotoolkit.wmts.xml.v100.GetCapabilities;
import org.geotoolkit.wmts.xml.v100.GetFeatureInfo;
import org.geotoolkit.wmts.xml.v100.GetTile;
import org.geotoolkit.wmts.xml.v100.LayerType;
import org.geotoolkit.wmts.xml.v100.Themes;
import org.geotoolkit.wmts.xml.v100.TileMatrix;
import org.geotoolkit.wmts.xml.v100.TileMatrixSet;
import org.geotoolkit.wmts.xml.v100.TileMatrixSetLink;
import org.geotoolkit.wmts.xml.v100.URLTemplateType;
import org.opengis.coverage.Coverage;
import org.opengis.geometry.DirectPosition;
import org.opengis.geometry.Envelope;
import org.opengis.referencing.crs.CoordinateReferenceSystem;
import org.opengis.referencing.crs.SingleCRS;
import org.opengis.referencing.crs.TemporalCRS;
import org.opengis.referencing.crs.VerticalCRS;
import org.opengis.referencing.cs.CoordinateSystemAxis;
import org.opengis.referencing.operation.MathTransform;
import org.opengis.referencing.operation.TransformException;
import org.opengis.util.FactoryException;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.context.annotation.Scope;
import javax.imageio.ImageReader;
import javax.imageio.spi.ImageReaderSpi;
import javax.inject.Named;
import java.awt.Color;
import java.awt.Point;
import java.awt.Rectangle;
import java.io.IOException;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.AbstractMap;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.TimeZone;
import java.util.UUID;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import java.util.logging.Level;
import static org.geotoolkit.ows.xml.OWSExceptionCode.*;
import org.geotoolkit.storage.coverage.CoverageUtilities;
import org.geotoolkit.storage.coverage.GridMosaic;
import org.geotoolkit.storage.coverage.Pyramid;
import org.geotoolkit.storage.coverage.PyramidSet;
import org.geotoolkit.storage.coverage.PyramidalCoverageReference;
import org.geotoolkit.storage.coverage.TileReference;
import org.opengis.util.GenericName;
/**
* Working part of the WMTS service.
*
* @version $Id$
*
* @author Cédric Briançon (Geomatys)
* @author Guilhem Legal (Geomatys)
* @since 0.3
*/
@Named("WTMSWorker")
@Scope(BeanDefinition.SCOPE_PROTOTYPE)
public class DefaultWMTSWorker extends LayerWorker implements WMTSWorker {
public static final String TIME_NAME = "time";
public static final String TIME_UNIT = "ISO-8601";
public static final String ELEVATION_NAME = "elevation";
public static final String CURRENT_VALUE = "current";
public static final double RESOLUTION_EPSILON = 1E-9;
/** Default temporal CRS, used for comparison purposes. */
public static final TemporalCRS JAVA_TIME = CommonCRS.Temporal.JAVA.crs();
public static final SimpleDateFormat ISO_8601_FORMATTER = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'");
static {
ISO_8601_FORMATTER.setTimeZone(TimeZone.getTimeZone("UTC"));
}
/**
* A list of supported MIME type
*/
private static final List<String> ACCEPTED_OUTPUT_FORMATS;
static {
ACCEPTED_OUTPUT_FORMATS = Arrays.asList(MimeType.TEXT_XML,
MimeType.APP_XML,
MimeType.TEXT_PLAIN);
}
/**
* A map which contains the binding between capabilities tile matrix set identifiers and input
* {@link org.geotoolkit.coverage.Pyramid} ids. It's used only if we've got multiple pyramids with the same ID but
* different matrix structure. Otherwise, we directly use pyramid ids as tile matrix set name.
*/
private final HashMap<String, HashSet<String>> tmsIdBinding = new HashMap<>();
private final ReentrantReadWriteLock tmsBindingLock = new ReentrantReadWriteLock();
/**
* Instanciates the working class for a SOAP client, that do request on a SOAP PEP service.
*/
public DefaultWMTSWorker(final String id) {
super(id, ServiceDef.Specification.WMTS);
if (isStarted) {
LOGGER.log(Level.INFO, "WMTS worker {0} running", id);
}
}
@Override
protected MarshallerPool getMarshallerPool() {
return WMTSMarshallerPool.getInstance();
}
/**
* {@inheritDoc}
*/
@Override
public Capabilities getCapabilities(GetCapabilities requestCapabilities) throws CstlServiceException {
LOGGER.log(logLevel, "getCapabilities request processing\n");
final long start = System.currentTimeMillis();
final String userLogin = getUserLogin();
//we verify the base request attribute
if (requestCapabilities.getService() != null) {
if (!requestCapabilities.getService().equalsIgnoreCase("WMTS")) {
throw new CstlServiceException("service must be \"WMTS\"!",
INVALID_PARAMETER_VALUE, "service");
}
} else {
throw new CstlServiceException("Service must be specified!",
MISSING_PARAMETER_VALUE, "service");
}
final AcceptVersionsType versions = requestCapabilities.getAcceptVersions();
if (versions != null) {
if (!versions.getVersion().contains("1.0.0")){
throw new CstlServiceException("version available : 1.0.0",
VERSION_NEGOTIATION_FAILED, "acceptVersion");
}
}
final AcceptFormatsType formats = requestCapabilities.getAcceptFormats();
if (formats != null && formats.getOutputFormat().size() > 0 ) {
boolean found = false;
for (String form: formats.getOutputFormat()) {
if (ACCEPTED_OUTPUT_FORMATS.contains(form)) {
found = true;
}
}
if (!found) {
throw new CstlServiceException("accepted format : text/xml, application/xml",
INVALID_PARAMETER_VALUE, "acceptFormats");
}
}
SectionsType sections = requestCapabilities.getSections();
if (sections == null) {
sections = new SectionsType(SectionsType.getExistingSections("1.1.1"));
}
//set the current updateSequence parameter
final boolean returnUS = returnUpdateSequenceDocument(requestCapabilities.getUpdateSequence());
if (returnUS) {
return new Capabilities("1.0.0", getCurrentUpdateSequence());
}
// If the getCapabilities response is in cache, we just return it.
AbstractCapabilitiesCore cachedCapabilities = getCapabilitiesFromCache("1.0.0", null);
if (cachedCapabilities != null) {
return (Capabilities) cachedCapabilities.applySections(sections);
}
/* We synchronize Computing, because every thread will compute the same thing, its useless to waste CPU. One will
* do the job, the others will wait for it. More over, it's EXTREMELY important for the integrity of the binding
* between pyramids and Tile matrix set ids that we synchronize the get Capa using our binding lock.
*/
tmsBindingLock.writeLock().lock();
try {
cachedCapabilities = getCapabilitiesFromCache("1.0.0", null);
if (cachedCapabilities != null) {
return (Capabilities) cachedCapabilities.applySections(sections);
}
/*
* BUILD NEW CAPABILITIES DOCUMENT
*/
tmsIdBinding.clear();
// we load the skeleton capabilities
final Details skeleton = getStaticCapabilitiesObject("wmts", null);
final Capabilities skeletonCapabilities = (Capabilities) WMTSConstant.createCapabilities("1.0.0", skeleton);
//we prepare the response document
final ServiceIdentification si = skeletonCapabilities.getServiceIdentification();
final ServiceProvider sp = skeletonCapabilities.getServiceProvider();
final OperationsMetadata om = (OperationsMetadata) WMTSConstant.OPERATIONS_METADATA.clone();
// TODO
final List<Themes> themes = new ArrayList<>();
//we update the URL
om.updateURL(getServiceUrl());
// Build the list of layers
final List<LayerType> outputLayers = new ArrayList<>();
// and the list of matrix set
final HashMap<String, TileMatrixSet> tileSets = new HashMap<>();
final List<Layer> declaredLayers = getConfigurationLayers(userLogin);
for (final Layer configLayer : declaredLayers) {
final Data details = getLayerReference(configLayer);
if (details == null) {
LOGGER.log(Level.WARNING, "No data can be found for name : "+configLayer.getName());
continue;
}
final String name;
if (configLayer.getAlias() != null && !configLayer.getAlias().isEmpty()) {
name = configLayer.getAlias().trim().replaceAll(" ", "_");
} else {
name = configLayer.getName().getLocalPart();
}
final Object origin = details.getOrigin();
if (!(origin instanceof PyramidalCoverageReference)) {
//WMTS only handle PyramidalModel
LOGGER.log(Level.WARNING, "Layer {0} has not a PyramidalModel origin. It will not be included in capabilities", name);
continue;
}
try {
final PyramidalCoverageReference pmodel = (PyramidalCoverageReference) origin;
final PyramidSet set = pmodel.getPyramidSet();
final Envelope pyramidSetEnv = set.getEnvelope();
if (pyramidSetEnv == null) {
throw new CstlServiceException("No valid extent for layer " + name);
}
final CoordinateReferenceSystem pyramidSetEnvCRS = pyramidSetEnv.getCoordinateReferenceSystem();
final int xAxis = Math.max(0, CoverageUtilities.getMinOrdinate(pyramidSetEnvCRS));
final int yAxis = xAxis + 1;
/* We get pyramid set CRS components to identify additional dimensions. We remove horizontal component
* from the list to ease further operations, and prepare WMTS dimension descriptors. Dimension allowed
* values will be filled when we'll browse mosaics to build tile matrix capabilities.
*/
final HashMap<Integer, Dimension> dims = new HashMap<>();
final Map<Integer, CoordinateReferenceSystem> splittedCRS =
ReferencingUtilities.indexedDecompose(pyramidSetEnvCRS);
final Iterator<Map.Entry<Integer, CoordinateReferenceSystem>> iterator = splittedCRS.entrySet().iterator();
while (iterator.hasNext()) {
final Map.Entry<Integer, CoordinateReferenceSystem> entry = iterator.next();
final CoordinateReferenceSystem tmpCRS = entry.getValue();
// If it's not a single dimension, It's not an additional dimension.
if (tmpCRS.getCoordinateSystem().getDimension() > 1) {
iterator.remove();
} else {
// TODO : we have no check for multiple temporal dimensions (is it possible to have more than one ?)
final Dimension dimension;
if (tmpCRS instanceof TemporalCRS) {
// current value is a special wmts case.
dimension = new Dimension(TIME_NAME, TIME_UNIT, "current");
} else {
final String dimName;
final CoordinateSystemAxis axis = tmpCRS.getCoordinateSystem().getAxis(0);
// vertical dimension name is fixed by 1.0.0 standard.
if (tmpCRS instanceof VerticalCRS) {
dimName = ELEVATION_NAME;
} else {
dimName = axis.getName().getCode();
}
dimension = new Dimension(dimName, axis.getUnit().toString(), "current");
}
dims.put(entry.getKey(), dimension);
}
}
final Collection<Pyramid> pyramids = set.getPyramids();
final List<BoundingBoxType> bboxList = new ArrayList<>();
for (Pyramid pyramid : pyramids) {
final GeneralEnvelope pyramidEnv = CoverageUtilities.getPyramidEnvelope(pyramid);
final int envXAxis = Math.max(0, CoverageUtilities.getMinOrdinate(pyramid.getCoordinateReferenceSystem()));
final int envYAxis = xAxis + 1;
final BoundingBoxType bbox = new WGS84BoundingBoxType(
getCRSCode(pyramid.getCoordinateReferenceSystem()),
pyramidEnv.getMinimum(envXAxis),
pyramidEnv.getMinimum(envYAxis),
pyramidEnv.getMaximum(envXAxis),
pyramidEnv.getMaximum(envYAxis));
bboxList.add(bbox);
}
final LayerType outputLayer = new LayerType(
name,
name,
name,
bboxList,
WMTSConstant.DEFAULT_STYLES,
new ArrayList<>(dims.values()));
try {
final Envelope crs84Env = CRS.transform(pyramidSetEnv, CommonCRS.defaultGeographic());
outputLayer.getWGS84BoundingBox().add(new WGS84BoundingBoxType("CRS:84",
crs84Env.getMinimum(xAxis),
crs84Env.getMinimum(yAxis),
crs84Env.getMaximum(xAxis),
crs84Env.getMaximum(yAxis)));
} catch (Exception e) {
// Optional parameter, we don't let exception make capabilities fail.
LOGGER.log(Level.FINE, "Input envelope cannot be reprojected in CRS:84.");
}
final List<String> pformats = set.getFormats();
outputLayer.setFormat(pformats);
final List<URLTemplateType> resources = new ArrayList<>();
for (String pformat : pformats) {
String url = getServiceUrl();
url = url.substring(0, url.length() - 1) + "/" + name + "/{tileMatrixSet}/{tileMatrix}/{tileRow}/{tileCol}.{format}";
final URLTemplateType tileURL = new URLTemplateType(pformat, "tile", url);
resources.add(tileURL);
}
outputLayer.setResourceURL(resources);
for (Pyramid pr : pyramids) {
final TileMatrixSet tms = new TileMatrixSet();
tms.setIdentifier(new CodeType(pr.getId()));
tms.setSupportedCRS(getCRSCode(pr.getCoordinateReferenceSystem()));
final List<TileMatrix> tm = new ArrayList<>();
final double[] scales = pr.getScales();
for (int i = 0; i < scales.length; i++) {
final Iterator<GridMosaic> mosaicIt = pr.getMosaics(i).iterator();
if (!mosaicIt.hasNext()) {
continue;
}
final GridMosaic mosaic = mosaicIt.next();
DirectPosition upperLeft = mosaic.getUpperLeftCorner();
double scale = mosaic.getScale();
//convert scale in the strange WMTS scale denominator
scale = WMTSUtilities.toScaleDenominator(pr.getCoordinateReferenceSystem(), scale);
final TileMatrix matrix = new TileMatrix();
matrix.setIdentifier(new CodeType(mosaic.getId()));
matrix.setScaleDenominator(scale);
matrix.setMatrixDimension(mosaic.getGridSize());
matrix.setTileDimension(mosaic.getTileSize());
matrix.getTopLeftCorner().add(upperLeft.getOrdinate(xAxis));
matrix.getTopLeftCorner().add(upperLeft.getOrdinate(yAxis));
tm.add(matrix);
// Fill dimensions. We iterate over all mosaics of the current scale to find all slices.
int timeIndex = -1;
MathTransform toJavaTime = null;
final SimpleDateFormat dateFormatter = (SimpleDateFormat) ISO_8601_FORMATTER.clone();
for (Map.Entry<Integer, CoordinateReferenceSystem> entry : splittedCRS.entrySet()) {
String strValue;
// For temporal values, we convert it into timestamp, then to an ISO 8601 date.
final List<String> currentDimValues = dims.get(entry.getKey()).getValue();
if (entry.getValue() instanceof TemporalCRS) {
timeIndex = entry.getKey();
double value = upperLeft.getOrdinate(entry.getKey());
if (!CRS.equalsApproximatively(JAVA_TIME, entry.getValue())) {
final double[] tmpArray = new double[]{value};
toJavaTime = CRS.findMathTransform(entry.getValue(), JAVA_TIME);
toJavaTime.transform(tmpArray, 0, tmpArray, 0, 1);
value = tmpArray[0];
}
strValue = dateFormatter.format(new Date((long) value));
} else {
strValue = String.valueOf(upperLeft.getOrdinate(entry.getKey()));
}
if (strValue != null && !currentDimValues.contains(strValue)) {
currentDimValues.add(strValue);
}
}
while (mosaicIt.hasNext()) {
upperLeft = mosaicIt.next().getUpperLeftCorner();
for (Map.Entry<Integer, CoordinateReferenceSystem> entry : splittedCRS.entrySet()) {
String strValue = null;
// For temporal values, we convert it into timestamp, then to an ISO 8601 date.
final List<String> currentDimValues = dims.get(entry.getKey()).getValue();
if (timeIndex == entry.getKey()) {
double value = upperLeft.getOrdinate(entry.getKey());
if (toJavaTime != null) {
final double[] tmpArray = new double[]{value};
toJavaTime.transform(tmpArray, 0, tmpArray, 0, 1);
value = tmpArray[0];
}
strValue = dateFormatter.format(new Date((long) value));
} else {
strValue = String.valueOf(upperLeft.getOrdinate(entry.getKey()));
}
if (strValue != null && !currentDimValues.contains(strValue)) {
currentDimValues.add(strValue);
}
}
}
}
tms.setTileMatrix(tm);
/*
* Once our tile matrix set is defined, we must check if we've got one which is equal to the newly
* computed set.
* - If no matrix set is equal to the new one, and no matrix set with the same name exists, we add our
* matrix set to the service capabilities.
* - If we already have a tile matrix set equal to the current one, we just make a link to the old
* one.
* - If we've got two sets with the same name, but they're different, we rename the new matrix set
* to avoid mistakes.
*
* In all cases, we store a binding between the TMS identifier and the pyramid one to be able to
* retrieve pyramid at getTile request.
*/
TileMatrixSet previousDefined = tileSets.get(pr.getId());
boolean equalSets = false;
if (previousDefined == null || !(equalSets = areEqual(tms, previousDefined))) {
for (final TileMatrixSet tmpSet : tileSets.values()) {
if (areEqual(tms, tmpSet)) {
equalSets = true;
previousDefined = tmpSet;
break;
}
}
}
if (previousDefined == null) {
tileSets.put(pr.getId(), tms);
} else if (equalSets) {
tms.setIdentifier(previousDefined.getIdentifier());
} else {
// Two different matrix sets with same identifier. We'll change the name of the new one.
final String tmsUUID = UUID.randomUUID().toString();
tms.setIdentifier(new CodeType(tmsUUID));
tileSets.put(tmsUUID, tms);
}
addTileMatrixSetBinding(tms.getIdentifier().getValue(), pr.getId());
final TileMatrixSetLink tmsl = new TileMatrixSetLink(tms.getIdentifier().getValue());
outputLayer.addTileMatrixSetLink(tmsl);
}
outputLayers.add(outputLayer);
} catch (Exception ex) {
LOGGER.log(Level.WARNING, "Cannot build matrix list of the following layer : " + name, ex);
}
}
final ContentsType cont = new ContentsType(outputLayers, new ArrayList<>(tileSets.values()));
// put full capabilities in cache
final Capabilities c = new Capabilities(si, sp, om, "1.0.0", null, cont, themes);
putCapabilitiesInCache("1.0.0", null, c);
LOGGER.log(logLevel, "getCapabilities processed in {0}ms.\n", (System.currentTimeMillis() - start));
return (Capabilities) c.applySections(sections);
} finally {
tmsBindingLock.writeLock().unlock();
}
}
/**
* Add a binding between a tile matrix set defined in service GetCapabilities,
* and a pyramid set Id. The aim is to factorize the possible tile matrixes.
* @param tmsId The ID of the {@link TileMatrixSet} to expose via GetCapabilities.
* @param pyramidId The pyramid ID to add as valid pyramid for input TMS.
* @return True if we successfully added the binding, false otherwise (in could already exist).
*/
private boolean addTileMatrixSetBinding(final String tmsId, final String pyramidId) {
tmsBindingLock.writeLock().lock();
try {
HashSet<String> bindings = tmsIdBinding.get(tmsId);
if (bindings == null) {
bindings = new HashSet<>();
tmsIdBinding.put(tmsId, bindings);
}
return bindings.add(pyramidId);
} finally {
tmsBindingLock.writeLock().unlock();
}
}
/**
* Return CRS code name. As WMTS define only 2D CRS (additional dimensions are stored beside), we will extract
* horizontal CRS, and search for a standard EPSG identifier. If we cannot find it, we will just keep CRS initial
* code.
* @param candidate The system to analyse.
* @return An identifier for the horizontal part of input crs.
*/
private String getCRSCode(CoordinateReferenceSystem candidate) {
final SingleCRS horizontal = org.apache.sis.referencing.CRS.getHorizontalComponent(candidate);
// Workaround to normalize WGS84 that return "EPSG:WGS 84"
// for IdentifiedObjects.getIdentifierOrName() call
if (CRS.equalsIgnoreMetadata(CommonCRS.WGS84.normalizedGeographic(), horizontal)) {
return "CRS:84";
} else {
try {
final Integer identifier = org.geotoolkit.referencing.IdentifiedObjects.lookupEpsgCode(
horizontal, true);
if (identifier != null) {
return "EPSG:"+identifier;
}
} catch (FactoryException e) {
LOGGER.log(Level.INFO, e.getLocalizedMessage(), e);
}
}
return IdentifiedObjects.getIdentifierOrName(candidate);
}
/**
* {@inheritDoc}
*/
@Override
public Map.Entry<String, Object> getFeatureInfo(GetFeatureInfo request) throws CstlServiceException {
// -- get the List of layer references
final GetTile getTile = request.getGetTile();
final String userLogin = getUserLogin();
final GenericName layerName = Util.parseLayerName(getTile.getLayer());
final Data layerRef = getLayerReference(userLogin, layerName);
final Layer configLayer = getConfigurationLayer(layerName, userLogin);
// build an equivalent style List
final String styleName = getTile.getStyle();
final DataReference styleRef = configLayer.getStyle(styleName);
final MutableStyle style = getStyle(styleRef);
Coverage c = null;
// -- create the rendering parameter Map
Double elevation = null;
Date time = null;
final List<DimensionNameValue> dimensions = getTile.getDimensionNameValue();
for (DimensionNameValue dimension : dimensions) {
if (dimension.getName().equalsIgnoreCase("elevation")) {
try {
elevation = Double.parseDouble(dimension.getValue());
} catch (NumberFormatException ex) {
throw new CstlServiceException("Unable to parse the elevation value", INVALID_PARAMETER_VALUE, "elevation");
}
}
if (dimension.getName().equalsIgnoreCase("time")) {
try {
time = TimeParser.toDate(dimension.getValue());
} catch (ParseException ex) {
throw new CstlServiceException(ex, INVALID_PARAMETER_VALUE, "time");
}
}
}
final Map<String, Object> params = new HashMap<>();
params.put("ELEVATION", elevation);
params.put("TIME", time);
final SceneDef sdef = new SceneDef();
try {
final MapContext context = PortrayalUtil.createContext(layerRef, style, params);
sdef.setContext(context);
} catch (PortrayalException ex) {
throw new CstlServiceException(ex, NO_APPLICABLE_CODE);
}
// 2. VIEW
final JTSEnvelope2D refEnv = new JTSEnvelope2D(c.getEnvelope());
final double azimuth = 0;//request.getAzimuth();
final ViewDef vdef = new ViewDef(refEnv,azimuth);
// 3. CANVAS
final java.awt.Dimension canvasDimension = null;//request.getSize();
final Color background = null;
final CanvasDef cdef = new CanvasDef(canvasDimension,background);
// 4. SHAPE
// a
final int pixelTolerance = 3;
final int i = request.getI();
final int j = request.getJ();
if (i < 0 || i > canvasDimension.width) {
throw new CstlServiceException("The requested point has an invalid X coordinate.", INVALID_POINT);
}
if (j < 0 || j > canvasDimension.height) {
throw new CstlServiceException("The requested point has an invalid Y coordinate.", INVALID_POINT);
}
final Rectangle selectionArea = new Rectangle( request.getI()-pixelTolerance,
request.getJ()-pixelTolerance,
pixelTolerance*2,
pixelTolerance*2);
// 5. VISITOR
String infoFormat = request.getInfoFormat();
if (infoFormat == null) {
//Should not happen since the info format parameter is mandatory for the GetFeatureInfo request.
infoFormat = MimeType.TEXT_PLAIN;
}
FeatureInfoFormat featureInfo = null;
try {
featureInfo = FeatureInfoUtilities.getFeatureInfoFormat( getConfiguration(), configLayer, infoFormat);
} catch (ClassNotFoundException ex) {
throw new CstlServiceException(ex, NO_APPLICABLE_CODE);
} catch (ConfigurationException ex) {
throw new CstlServiceException(ex, NO_APPLICABLE_CODE);
}
if (featureInfo == null) {
throw new CstlServiceException("INFO_FORMAT="+infoFormat+" not supported for layers : "+layerName, NO_APPLICABLE_CODE);
}
try {
final Object result = featureInfo.getFeatureInfo(sdef, vdef, cdef, selectionArea, request);
return new AbstractMap.SimpleEntry<>(infoFormat, result);
} catch (PortrayalException ex) {
throw new CstlServiceException(ex, NO_APPLICABLE_CODE);
}
}
/**
* {@inheritDoc}
*/
@Override
public TileReference getTile(final GetTile request) throws CstlServiceException {
//1 LAYER NOT USED FOR NOW
final GenericName layerName = Util.parseLayerName(request.getLayer());
final String userLogin = getUserLogin();
// final Layer configLayer = getConfigurationLayer(layerName, userLogin);
// 2. STYLE NOT USED FOR NOW
// final String styleName = request.getStyle();
// final DataReference styleRef = configLayer.getStyle(styleName);
// final MutableStyle style = getStyle(styleRef);
// 3. Get and check parameters
final int columnIndex = request.getTileCol();
final int rowIndex = request.getTileRow();
final String level = request.getTileMatrix();
String matrixSetName = request.getTileMatrixSet();
final HashSet<String> validPyramidNames;
tmsBindingLock.readLock().lock();
try {
final HashSet<String> idBinding = tmsIdBinding.get(matrixSetName);
if (idBinding != null && !idBinding.isEmpty()) {
validPyramidNames = idBinding;
} else {
validPyramidNames = new HashSet<>(1);
validPyramidNames.add(matrixSetName);
}
} finally {
tmsBindingLock.readLock().unlock();
}
if (columnIndex < 0 || rowIndex < 0) {
throw new CstlServiceException("Operation request contains an invalid parameter value, " +
"TileCol and TileRow must be positive integers. Received position : " +
new Point(columnIndex, rowIndex), INVALID_PARAMETER_VALUE, "TileCol or TileRow");
}
try {
final Data details = getLayerReference(userLogin, layerName);
if (details == null) {
throw new CstlServiceException("Operation request contains an invalid parameter value, "
+ "No layer for name : " + layerName,
INVALID_PARAMETER_VALUE, "layerName");
}
final Object origin = details.getOrigin();
if (!(origin instanceof PyramidalCoverageReference)) {
//WMTS only handle PyramidalCoverageReference
throw new CstlServiceException("Operation request contains an invalid parameter value, "
+ "invalid layer : " + layerName + " , layer is not a pyramid model " + layerName,
INVALID_PARAMETER_VALUE, "layerName");
}
final PyramidSet set = ((PyramidalCoverageReference) origin).getPyramidSet();
Pyramid pyramid = null;
for (Pyramid pr : set.getPyramids()) {
if (validPyramidNames.contains(pr.getId())) {
pyramid = pr;
break;
}
}
if (pyramid == null) {
throw new CstlServiceException("Operation request contains an invalid parameter value,"
+ " undefined matrixSet: " + matrixSetName + " for layer: " + layerName,
INVALID_PARAMETER_VALUE, "tilematrixset");
}
GridMosaic mosaic = null;
for (GridMosaic gm : pyramid.getMosaics()) {
if (gm.getId().equals(level)) {
mosaic = gm;
break;
}
}
// 4. If we found a base mosaic and user specified additional dimensions, we try to switch on the right slice.
final List<DimensionNameValue> dimensions = request.getDimensionNameValue();
if (mosaic != null && dimensions != null && !dimensions.isEmpty()) {
final GeneralEnvelope envelope = envelopeFromDimensions(mosaic.getEnvelope(), dimensions);
// We use a strict finder, because default one (as methods in coverage utilities) return arbitrary data
// if it don't find any fitting mosaic...
StrictlyCoverageFinder finder = new StrictlyCoverageFinder();
mosaic = finder.findMosaic(pyramid, mosaic.getScale(), RESOLUTION_EPSILON, envelope, -1);
}
if (mosaic == null) {
throw new CstlServiceException("Operation request contains an invalid parameter value," +
" undefined matrix: " + level + " for matrixSet: " + matrixSetName,
INVALID_PARAMETER_VALUE, "tilematrix");
}
if (columnIndex >= mosaic.getGridSize().width) {
throw new CstlServiceException("TileCol out of range, expected value < "+mosaic.getGridSize().width+" but got " + columnIndex,
TILE_OUT_OF_RANGE, "tilecol");
}
if (rowIndex >= mosaic.getGridSize().height) {
throw new CstlServiceException("TileRow out of range, expected value < " + mosaic.getGridSize().height + " but got "+rowIndex,
TILE_OUT_OF_RANGE, "tilerow");
}
if (mosaic.isMissing(columnIndex, rowIndex)) {
return emptyTile(mosaic, columnIndex, rowIndex);
} else {
return mosaic.getTile(columnIndex, rowIndex, null);
}
} catch(CstlServiceException ex) {
throw ex;
} catch(Exception ex) {
throw new CstlServiceException("Unexpected error for operation GetTile : "+ layerName, ex , NO_APPLICABLE_CODE);
}
}
/**
* Create empty TileReference with black image as input.
* @param mosaic
* @param columnIndex
* @param rowIndex
* @return TileReference
*/
private TileReference emptyTile(final GridMosaic mosaic, final int columnIndex, final int rowIndex) {
return new TileReference() {
@Override
public ImageReader getImageReader() throws IOException {
return null;
}
@Override
public ImageReaderSpi getImageReaderSpi() {
return null;
}
@Override
public Object getInput() {
//TODO cache empty image
Color color = new Color(0x00FFFFFF, true);
return Cstl.getPortrayalService().writeBlankImage(color,mosaic.getTileSize());
}
@Override
public int getImageIndex() {
return 0;
}
@Override
public Point getPosition() {
return new Point(columnIndex, rowIndex);
}
};
}
/**
* Change range values of input envelope for all dimensions specified in given dimension list.
* @param envelope The envelope containing base values for dimensions to change. Not modified, a copy is performed.
* @param dimensions Dimensions to override.
* @return An envelope containing same values as input, except for ranges specified in dimensions parameter.
* @throws ParseException
* @throws FactoryException
* @throws TransformException
*/
private static GeneralEnvelope envelopeFromDimensions(final Envelope envelope, List<DimensionNameValue> dimensions) throws ParseException, FactoryException, TransformException {
final GeneralEnvelope result = new GeneralEnvelope(envelope);
final CoordinateReferenceSystem crs = envelope.getCoordinateReferenceSystem();
final Map<Integer, CoordinateReferenceSystem> systems = ReferencingUtilities.indexedDecompose(crs);
CoordinateReferenceSystem currentCRS;
for (final Map.Entry<Integer, CoordinateReferenceSystem> entry : systems.entrySet()) {
currentCRS = entry.getValue();
// Not an additional dimension, it must be horizontal CRS.
if (currentCRS.getCoordinateSystem().getDimension() > 1) {
continue;
}
if (currentCRS instanceof TemporalCRS) {
for (final DimensionNameValue dim : dimensions) {
if (dim.getName().equalsIgnoreCase(TIME_NAME)) {
if (dim.getValue().equalsIgnoreCase(CURRENT_VALUE)) break;
final long timestamp = TimeParser.toDate(dim.getValue()).getTime();
// We don't know what is the CRS of our envelope, but WMTS exposes times as ISO 8601, so a
// conversion may be needed.
if (CRS.equalsApproximatively(currentCRS, JAVA_TIME)) {
// put a minimal epsilon.
result.setRange(entry.getKey(), timestamp - 1, timestamp + 1);
} else {
final double[] time = new double[1];
CRS.findMathTransform(JAVA_TIME, currentCRS, true).transform(time, 0, time, 0, 1);
result.setRange(entry.getKey(), time[0], time[0]);
}
break;
}
}
} else {
final String axisName;
if (currentCRS instanceof VerticalCRS) {
axisName = ELEVATION_NAME; // Fixed in WMTS standard.
} else {
axisName = currentCRS.getCoordinateSystem().getAxis(0).getName().getCode();
}
for (final DimensionNameValue dim : dimensions) {
if (dim.getName().equalsIgnoreCase(axisName)) {
if (dim.getValue().equalsIgnoreCase(CURRENT_VALUE)) break;
final double value = Double.parseDouble(dim.getValue());
// put a minimal epsilon.
result.setRange(entry.getKey(), value, value);
break;
}
}
}
}
return result;
}
/**
* Test the equality of 2 {@link org.geotoolkit.wmts.xml.v100.TileMatrixSet}, ignoring their name.
*
* We check their bounding box, CRS and list of tile matrixes.
* @param tms1
* @param tms2
* @return
*/
private static boolean areEqual(final TileMatrixSet tms1, TileMatrixSet tms2) {
if (!tms1.getSupportedCRS().equals(tms2.getSupportedCRS())) return false;
final BoundingBoxType bbox1 = (tms1.getBoundingBox() == null)? null : tms1.getBoundingBox().getValue();
final BoundingBoxType bbox2 = (tms2.getBoundingBox() == null)? null : tms2.getBoundingBox().getValue();
if (bbox1 != null? !bbox1.equals(bbox2) : bbox2 != null) return false;
final List<TileMatrix> sourceMatrixes = tms1.getTileMatrix();
final List<TileMatrix> targetMatrixes = tms2.getTileMatrix();
return (targetMatrixes == null ? sourceMatrixes == null : targetMatrixes.equals(sourceMatrixes));
}
}