/*
* 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.map.featureinfo;
import org.apache.sis.geometry.GeneralDirectPosition;
import org.apache.sis.geometry.GeneralEnvelope;
import org.apache.sis.referencing.CRS;
import org.apache.sis.referencing.crs.DefaultCompoundCRS;
import org.apache.sis.util.ArgumentChecks;
import org.apache.sis.util.ArraysExt;
import org.apache.sis.util.logging.Logging;
import org.constellation.configuration.ConfigurationException;
import org.constellation.configuration.GetFeatureInfoCfg;
import org.constellation.configuration.Layer;
import org.constellation.configuration.LayerContext;
import org.geotoolkit.coverage.GridSampleDimension;
import org.geotoolkit.coverage.grid.GridCoverage2D;
import org.geotoolkit.coverage.io.CoverageStoreException;
import org.geotoolkit.coverage.io.GridCoverageReadParam;
import org.geotoolkit.coverage.io.GridCoverageReader;
import org.geotoolkit.display2d.canvas.AbstractGraphicVisitor;
import org.geotoolkit.display2d.canvas.RenderingContext2D;
import org.geotoolkit.display2d.primitive.ProjectedCoverage;
import org.geotoolkit.display2d.primitive.SearchAreaJ2D;
import org.geotoolkit.lang.Static;
import org.geotoolkit.map.CoverageMapLayer;
import org.opengis.coverage.CannotEvaluateException;
import org.opengis.geometry.Envelope;
import org.opengis.referencing.crs.CoordinateReferenceSystem;
import org.opengis.referencing.crs.TemporalCRS;
import org.opengis.referencing.operation.TransformException;
import javax.imageio.spi.ServiceRegistry;
import javax.measure.converter.ConversionException;
import javax.measure.unit.NonSI;
import java.awt.geom.Rectangle2D;
import java.util.AbstractMap;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.logging.Level;
import org.geotoolkit.storage.coverage.CoverageReference;
/**
* Set of utilities methods for FeatureInfoFormat and GetFeatureInfoCfg manipulation.
*
* @author Quentin Boileau (Geomatys)
*/
public final class FeatureInfoUtilities extends Static {
/**
* Get all declared in resources/META-INF/service/org.constellation.map.featureinfo.FeatureInfoFormat file
* FeatureInfoFormat.
* @return an array of FeatureInfoFormat instances.
*/
public static FeatureInfoFormat[] getAllFeatureInfoFormat() {
final Set<FeatureInfoFormat> infoFormats = new HashSet<>();
final Iterator<FeatureInfoFormat> ite = ServiceRegistry.lookupProviders(FeatureInfoFormat.class);
while (ite.hasNext()) {
infoFormats.add(ite.next());
}
return infoFormats.toArray(new FeatureInfoFormat[infoFormats.size()]);
}
/**
* Search a specific instance of {@link FeatureInfoFormat} in layer (if not null) then in service configuration using
* mimeType.
*
* @param serviceConf service configuration (can't be null)
* @param layerConf layer configuration. Can be null. If not null, search in layer configuration first.
* @param mimeType searched mimeType (can't be null)
* @return found FeatureInfoFormat of <code>null</code> if not found.
* @throws ClassNotFoundException if a {@link org.constellation.configuration.GetFeatureInfoCfg} binding class is not in classpath
* @throws ConfigurationException if binding class is not an {@link FeatureInfoFormat} instance
* or declared {@link org.constellation.configuration.GetFeatureInfoCfg} MimeType is not supported by the {@link FeatureInfoFormat} implementation.
*/
public static FeatureInfoFormat getFeatureInfoFormat (final LayerContext serviceConf, final Layer layerConf, final String mimeType)
throws ClassNotFoundException, ConfigurationException {
ArgumentChecks.ensureNonNull("serviceConf", serviceConf);
ArgumentChecks.ensureNonNull("mimeType", mimeType);
FeatureInfoFormat featureInfo = null;
if (layerConf != null) {
final List<GetFeatureInfoCfg> infos = layerConf.getGetFeatureInfoCfgs();
if (infos != null && infos.size() > 0) {
for (GetFeatureInfoCfg infoCfg : infos) {
if (infoCfg.getMimeType().equals(mimeType)) {
featureInfo = FeatureInfoUtilities.getFeatureInfoFormatFromConf(infoCfg);
} else if (infoCfg.getMimeType() == null || infoCfg.getMimeType().isEmpty()) {
//Find supported mimetypes in FeatureInfoFormat
final FeatureInfoFormat tmpFormat = FeatureInfoUtilities.getFeatureInfoFormatFromConf(infoCfg);
final List<String> supportedMime = tmpFormat.getSupportedMimeTypes();
if (!(supportedMime.isEmpty()) && supportedMime.contains(mimeType)) {
featureInfo = tmpFormat;
}
}
}
}
}
//try generics
if (featureInfo == null) {
final Set<GetFeatureInfoCfg> generics = FeatureInfoUtilities.getGenericFeatureInfos(serviceConf);
for (GetFeatureInfoCfg infoCfg : generics) {
if (infoCfg.getMimeType().equals(mimeType)) {
featureInfo = FeatureInfoUtilities.getFeatureInfoFormatFromConf(infoCfg);
}
}
}
return featureInfo;
}
/**
* Find {@link FeatureInfoFormat} from a given {@link org.constellation.configuration.GetFeatureInfoCfg}.
* Also check if {@link org.constellation.configuration.GetFeatureInfoCfg} mimeType is supported by {@link FeatureInfoFormat} found.
*
* @param infoConf {@link org.constellation.configuration.GetFeatureInfoCfg} input
* @return a {@link FeatureInfoFormat} or null if not found
* @throws ClassNotFoundException if a {@link org.constellation.configuration.GetFeatureInfoCfg} binding class is not in classpath
* @throws ConfigurationException if binding class is not an {@link FeatureInfoFormat} instance
* or declared {@link org.constellation.configuration.GetFeatureInfoCfg} MimeType is not supported by the {@link FeatureInfoFormat} implementation.
*/
public static FeatureInfoFormat getFeatureInfoFormatFromConf (final GetFeatureInfoCfg infoConf) throws ClassNotFoundException, ConfigurationException {
final String mime = infoConf.getMimeType();
final String binding = infoConf.getBinding();
final FeatureInfoFormat featureInfo = getFeatureInfoFormatFromBinding(binding);
if (featureInfo != null) {
featureInfo.setConfiguration(infoConf);//give his configuration
if (mime == null || mime.isEmpty()) {
return featureInfo; // empty config mime type -> no need to check
} else {
if (featureInfo.getSupportedMimeTypes().contains(mime)) {
return featureInfo;
} else {
throw new ConfigurationException("MimeType "+mime+" not supported by FeatureInfo "+binding+
". Supported output MimeTypes are "+ featureInfo.getSupportedMimeTypes());
}
}
}
return null;
}
/**
* Find {@link FeatureInfoFormat} from a given canonical class name.
*
* @param binding canonical class name String
* @return {@link FeatureInfoFormat} or null if binding class is not an instance of {@link FeatureInfoFormat}.
* @throws ConfigurationException if binding class is not an {@link FeatureInfoFormat} instance
*/
private static FeatureInfoFormat getFeatureInfoFormatFromBinding (final String binding) throws ClassNotFoundException {
ArgumentChecks.ensureNonNull("binding", binding);
final Class clazz = Class.forName(binding);
final FeatureInfoFormat[] FIs = getAllFeatureInfoFormat();
for (FeatureInfoFormat fi : FIs) {
if (clazz.isInstance(fi)) {
return fi;
}
}
return null;
}
/**
* Check {@link org.constellation.configuration.GetFeatureInfoCfg} configuration in {@link LayerContext}.
*
* @param config service configuration
* @throws ConfigurationException if binding class is not an {@link FeatureInfoFormat} instance
* or declared MimeType is not supported by the {@link FeatureInfoFormat} implementation.
* @throws ClassNotFoundException if binding class is not in classpath
*/
public static void checkConfiguration(final LayerContext config) throws ConfigurationException, ClassNotFoundException {
if (config != null) {
final Set<GetFeatureInfoCfg> generics = getGenericFeatureInfos(config);
FeatureInfoFormat featureinfo;
for (final GetFeatureInfoCfg infoConf : generics) {
featureinfo = getFeatureInfoFormatFromConf(infoConf);
if (featureinfo == null) {
throw new ConfigurationException("Unknown generic FeatureInfo configuration binding "+infoConf.getBinding());
}
}
/*for (Source source : config.getLayers()) {
if (source != null) {
for (Layer layer : source.getInclude()) {
if (layer != null && layer.getGetFeatureInfoCfgs() != null) {
for (GetFeatureInfoCfg infoConf : layer.getGetFeatureInfoCfgs()) {
featureinfo = getFeatureInfoFormatFromConf(infoConf);
if (featureinfo == null) {
throw new ConfigurationException("Unknown FeatureInfo configuration binding "+infoConf.getBinding()+
" for layer "+layer.getName().getLocalPart());
}
}
}
}
}
}*/
}
}
/**
* Get all configured mimeTypes from a service {@link LayerContext}.
* @param config service configuration
* @return a Set of all MimeType from generic list and from layers config without duplicates.
*/
public static Set<String> allSupportedMimeTypes (final LayerContext config) throws ConfigurationException, ClassNotFoundException {
final Set<String> mimes = new HashSet<>();
if (config != null) {
final Set<GetFeatureInfoCfg> generics = getGenericFeatureInfos(config);
for (GetFeatureInfoCfg infoConf : generics) {
if (infoConf.getMimeType() != null && infoConf.getBinding() != null) {
mimes.add(infoConf.getMimeType());
} else {
throw new ConfigurationException("Binding or MimeType not define for GetFeatureInfoCfg "+infoConf);
}
}
/*for (Source source : config.getLayers()) {
if (source != null) {
for (Layer layer : source.getInclude()) {
if (layer != null && layer.getGetFeatureInfoCfgs() != null) {
for (GetFeatureInfoCfg infoConf : layer.getGetFeatureInfoCfgs()) {
if (infoConf.getMimeType() == null || infoConf.getMimeType().isEmpty()) {
//Empty mimeType -> Find supported mimetypes in format
final FeatureInfoFormat tmpFormat = FeatureInfoUtilities.getFeatureInfoFormatFromConf(infoConf);
tmpFormat.setConfiguration(infoConf); //give his configuration
final List<String> supportedMime = tmpFormat.getSupportedMimeTypes();
mimes.addAll(supportedMime);
} else {
mimes.add(infoConf.getMimeType());
}
}
}
}
}
}*/
}
return mimes;
}
/**
* Extract generic {@link org.constellation.configuration.GetFeatureInfoCfg} configurations from {@link LayerContext} base.
*
* @param config service configuration
* @return a Set of GetFeatureInfoCfg
*/
public static Set<GetFeatureInfoCfg> getGenericFeatureInfos (final LayerContext config) {
final Set<GetFeatureInfoCfg> fis = new HashSet<>();
if (config != null) {
final List<GetFeatureInfoCfg> globalFI = config.getGetFeatureInfoCfgs();
if (globalFI != null && !(globalFI.isEmpty())) {
for (GetFeatureInfoCfg infoConf : globalFI) {
fis.add(infoConf);
}
}
}
return fis;
}
/**
* Create the default {@link GetFeatureInfoCfg} list to configure a LayerContext.
* This list is build from generic {@link FeatureInfoFormat} and there supported mimetype.
* HTMLFeatureInfoFormat, CSVFeatureInfoFormat, GMLFeatureInfoFormat
*
* @return a list of {@link GetFeatureInfoCfg}
*/
public static List<GetFeatureInfoCfg> createGenericConfiguration () {
//Default featureInfo configuration
final List<GetFeatureInfoCfg> featureInfos = new ArrayList<>();
//HTML
FeatureInfoFormat infoFormat = new HTMLFeatureInfoFormat();
for (String mime : infoFormat.getSupportedMimeTypes()) {
featureInfos.add(new GetFeatureInfoCfg(mime, infoFormat.getClass().getCanonicalName()));
}
//CSV
infoFormat = new CSVFeatureInfoFormat();
for (String mime : infoFormat.getSupportedMimeTypes()) {
featureInfos.add(new GetFeatureInfoCfg(mime, infoFormat.getClass().getCanonicalName()));
}
//GML
infoFormat = new GMLFeatureInfoFormat();
for (String mime : infoFormat.getSupportedMimeTypes()) {
featureInfos.add(new GetFeatureInfoCfg(mime, infoFormat.getClass().getCanonicalName()));
}
//XML
infoFormat = new XMLFeatureInfoFormat();
for (String mime : infoFormat.getSupportedMimeTypes()) {
featureInfos.add(new GetFeatureInfoCfg(mime, infoFormat.getClass().getCanonicalName()));
}
return featureInfos;
}
/**
* Returns the data values of the given coverage, or {@code null} if the
* values can not be obtained.
*
* @return list : each entry contain a gridsampledimension and value associated.
*/
public static List<Map.Entry<GridSampleDimension,Object>> getCoverageValues(final ProjectedCoverage gra,
final RenderingContext2D context,
final SearchAreaJ2D queryArea){
final CoverageMapLayer layer = gra.getLayer();
Envelope objBounds = context.getCanvasObjectiveBounds();
CoordinateReferenceSystem objCRS = objBounds.getCoordinateReferenceSystem();
TemporalCRS temporalCRS = CRS.getTemporalComponent(objCRS);
if (temporalCRS == null) {
/*
* If there is no temporal range, arbitrarily select the latest date.
* This is necessary otherwise the call to reader.read(...) will scan
* every records in the GridCoverages table for the layer.
*/
Envelope timeRange = layer.getBounds();
if (timeRange != null) {
temporalCRS = CRS.getTemporalComponent(timeRange.getCoordinateReferenceSystem());
if (temporalCRS != null) {
try {
timeRange = org.geotoolkit.referencing.CRS.transform(timeRange, temporalCRS);
} catch (TransformException e) {
// Should never happen since temporalCRS is a component of layer CRS.
Logging.unexpectedException(null, AbstractGraphicVisitor.class, "getCoverageValues", e);
return null;
}
final double lastTime = timeRange.getMaximum(0);
double day;
try {
// Arbitrarily use a time range of 1 day, to be converted in units of the temporal CRS.
day = NonSI.DAY.getConverterToAny(temporalCRS.getCoordinateSystem().getAxis(0).getUnit()).convert(1);
} catch (ConversionException e) {
// Should never happen since TemporalCRS use time units. But if it happen
// anyway, use a time range of 1 of whatever units the temporal CRS use.
Logging.unexpectedException(null, AbstractGraphicVisitor.class, "getCoverageValues", e);
day = 1;
}
objCRS = new DefaultCompoundCRS(Collections.singletonMap(DefaultCompoundCRS.NAME_KEY,
objCRS.getName().getCode() + " + time"), objCRS, temporalCRS);
final GeneralEnvelope merged = new GeneralEnvelope(objCRS);
GeneralEnvelope subEnv = merged.subEnvelope(0, objBounds.getDimension());
subEnv.setEnvelope(objBounds);
merged.setRange(objBounds.getDimension(), lastTime - day, lastTime);
objBounds = merged;
}
}
}
double[] resolution = context.getResolution();
resolution = ArraysExt.resize(resolution, objCRS.getCoordinateSystem().getDimension());
final GridCoverageReadParam param = new GridCoverageReadParam();
param.setEnvelope(objBounds);
param.setResolution(resolution);
final CoverageReference ref = layer.getCoverageReference();
GridCoverageReader reader = null;
final GridCoverage2D coverage;
try {
reader = ref.acquireReader();
coverage = (GridCoverage2D) reader.read(ref.getImageIndex(),param);
} catch (CoverageStoreException ex) {
context.getMonitor().exceptionOccured(ex, Level.INFO);
return null;
} finally {
if (reader!= null) {
ref.recycle(reader);
}
}
if (coverage == null) {
//no coverage for this BBOX
return null;
}
final GeneralDirectPosition dp = new GeneralDirectPosition(objCRS);
final Rectangle2D bounds2D = queryArea.getObjectiveShape().getBounds2D();
dp.setOrdinate(0, bounds2D.getCenterX());
dp.setOrdinate(1, bounds2D.getCenterY());
float[] values = null;
try{
values = coverage.evaluate(dp, values);
}catch(CannotEvaluateException ex){
context.getMonitor().exceptionOccured(ex, Level.INFO);
values = new float[coverage.getSampleDimensions().length];
Arrays.fill(values, Float.NaN);
}
final List<Map.Entry<GridSampleDimension,Object>> results = new ArrayList<>();
for (int i=0; i<values.length; i++){
final GridSampleDimension sample = coverage.getSampleDimension(i);
results.add(new AbstractMap.SimpleImmutableEntry<GridSampleDimension, Object>(sample, values[i]));
}
return results;
}
}