/* * 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 com.vividsolutions.jts.geom.Geometry; import org.apache.sis.geometry.GeneralDirectPosition; import org.apache.sis.measure.MeasurementRange; import org.apache.sis.storage.DataStoreException; import org.apache.sis.util.logging.Logging; import org.apache.sis.xml.MarshallerPool; import org.constellation.provider.Data; import org.constellation.provider.DataProviders; import org.constellation.ws.MimeType; import org.geotoolkit.coverage.GridSampleDimension; import org.geotoolkit.display.PortrayalException; import org.geotoolkit.display2d.canvas.RenderingContext2D; import org.geotoolkit.display2d.primitive.ProjectedCoverage; import org.geotoolkit.display2d.primitive.ProjectedFeature; import org.geotoolkit.display2d.primitive.SearchAreaJ2D; import org.geotoolkit.display2d.service.CanvasDef; import org.geotoolkit.display2d.service.SceneDef; import org.geotoolkit.display2d.service.ViewDef; import org.geotoolkit.feature.ComplexAttribute; import org.geotoolkit.feature.Feature; import org.geotoolkit.feature.GeometryAttribute; import org.geotoolkit.feature.Property; import org.geotoolkit.feature.type.FeatureType; import org.geotoolkit.geometry.isoonjts.JTSUtils; import org.geotoolkit.geometry.jts.JTSEnvelope2D; import org.geotoolkit.internal.jaxb.ObjectFactory; import org.geotoolkit.map.FeatureMapLayer; import org.geotoolkit.ows.xml.GetFeatureInfo; import org.geotoolkit.referencing.IdentifiedObjects; import org.geotoolkit.util.DateRange; import org.opengis.geometry.Envelope; import org.opengis.referencing.crs.CoordinateReferenceSystem; import org.opengis.util.FactoryException; import javax.measure.unit.Unit; import javax.xml.bind.JAXBContext; import javax.xml.bind.JAXBException; import javax.xml.bind.Marshaller; import java.awt.*; import java.io.StringWriter; import java.text.DateFormat; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.SortedSet; import java.util.TimeZone; import java.util.logging.Level; import java.util.logging.Logger; import org.geotoolkit.util.NamesExt; import org.geotoolkit.storage.coverage.CoverageReference; import org.opengis.util.GenericName; /** * A generic FeatureInfoFormat that produce GML output for Features and Coverages. * Supported mimeTypes are : * <ul> * <li>application/vnd.ogc.gml : will return msGMLOutput</li> * <li>application/gml+xml : will return OGC GML3</li> * </ul> * * @author Quentin Boileau (Geomatys) */ public class GMLFeatureInfoFormat extends AbstractTextFeatureInfoFormat { private static final Logger LOGGER = Logging.getLogger("org.constellation.map.featureinfo"); private final DataProviders dp = DataProviders.getInstance(); /** * GML version flag : 0 for mapserver output * 1 for GML 3 output */ private int mode = 1; private GetFeatureInfo gfi; /** * A Map of namespace / prefix */ private final Map<String, String> prefixMap = new HashMap<>(); private static final MarshallerPool pool; static { MarshallerPool candidate = null; try { final Map<String, Object> properties = new HashMap<>(); properties.put(Marshaller.JAXB_FRAGMENT, true); properties.put(Marshaller.JAXB_FORMATTED_OUTPUT, false); candidate = new MarshallerPool(JAXBContext.newInstance(ObjectFactory.class), properties); } catch (JAXBException ex) { LOGGER.log(Level.SEVERE, "JAXB Exception while initializing the marshaller pool", ex); } pool = candidate; } public GMLFeatureInfoFormat() { prefixMap.put("http://www.opengis.net/gml", "gml"); } /** * Return the defined prefix for the specified namespace. * if it does not already exist a prefix for this namespace, * a new one will be created on the form: "ns" + prefixMap.size() * * @param namespace a attribute or featureType namespace. * * @return a prefix used in XML. */ private String acquirePrefix(String namespace) { if (namespace != null && !namespace.isEmpty()) { String result = prefixMap.get(namespace); if (result == null) { result = "ns" + prefixMap.size(); prefixMap.put(namespace,result); } return result + ":"; } return ""; } /** * {@inheritDoc} */ @Override protected void nextProjectedCoverage(ProjectedCoverage coverage, RenderingContext2D context, SearchAreaJ2D queryArea) { final List<Map.Entry<GridSampleDimension,Object>> results = FeatureInfoUtilities.getCoverageValues(coverage, context, queryArea); if (results == null) { return; } final CoverageReference ref = coverage.getLayer().getCoverageReference(); final GenericName fullLayerName = ref.getName(); String layerName = fullLayerName.tip().toString(); List<String> strs = coverages.get(layerName); if (strs == null) { strs = new ArrayList<String>(); coverages.put(layerName, strs); } StringBuilder builder = new StringBuilder(); for (final Map.Entry<GridSampleDimension,Object> entry : results) { final Object value = entry.getValue(); if (value == null) { continue; } builder.append(value); } final String result = builder.toString(); builder = new StringBuilder(); final String endMark = ">\n"; layerName = layerName.replaceAll("\\W", ""); builder.append("\t<").append(layerName).append("_layer").append(endMark) .append("\t\t<").append(layerName).append("_feature").append(endMark); final List<Data> layerDetailsList = getLayersDetails(); Data layerPostgrid = null; for (Data layer : layerDetailsList) { if (layer.getType().equals(Data.TYPE.COVERAGE) && layer.getName().equals(fullLayerName)) { layerPostgrid = layer; } } final Envelope objEnv; final List<Date> time; final Double elevation; if (gfi != null && gfi instanceof org.geotoolkit.wms.xml.GetFeatureInfo) { org.geotoolkit.wms.xml.GetFeatureInfo wmsGFI = (org.geotoolkit.wms.xml.GetFeatureInfo) gfi; objEnv = wmsGFI.getEnvelope2D(); time = wmsGFI.getTime(); elevation = wmsGFI.getElevation(); } else { objEnv = null; time = null; elevation = null; } if (objEnv != null) { final CoordinateReferenceSystem crs = objEnv.getCoordinateReferenceSystem(); final GeneralDirectPosition pos = getPixelCoordinates(gfi); if (pos != null) { builder.append("\t\t\t<gml:boundedBy>").append("\n"); String crsName; try { crsName = IdentifiedObjects.lookupIdentifier(crs, true); } catch (FactoryException ex) { LOGGER.log(Level.INFO, ex.getLocalizedMessage(), ex); crsName = crs.getName().getCode(); } builder.append("\t\t\t\t<gml:Box srsName=\"").append(crsName).append("\">\n"); builder.append("\t\t\t\t\t<gml:coordinates>"); builder.append(pos.getOrdinate(0)).append(",").append(pos.getOrdinate(1)).append(" ") .append(pos.getOrdinate(0)).append(",").append(pos.getOrdinate(1)); builder.append("</gml:coordinates>").append("\n"); builder.append("\t\t\t\t</gml:Box>").append("\n"); builder.append("\t\t\t</gml:boundedBy>").append("\n"); builder.append("\t\t\t<x>").append(pos.getOrdinate(0)).append("</x>").append("\n") .append("\t\t\t<y>").append(pos.getOrdinate(1)).append("</y>").append("\n"); } } if (time != null && !time.isEmpty()) { // TODO : Adapt code to use periods. builder.append("\t\t\t<time>").append(time.get(time.size()-1)).append("</time>") .append("\n"); } else { /* * Get the date of the last slice in this layer. Don't invoke * layerPostgrid.getAvailableTimes().last() because getAvailableTimes() is very * costly. The layerPostgrid.getEnvelope() method is much cheaper, since it can * leverage the database index. */ DateRange dates = null; if (layerPostgrid != null) { try { dates = layerPostgrid.getDateRange(); } catch (DataStoreException ex) { LOGGER.log(Level.INFO, ex.getLocalizedMessage(), ex); } } if (dates != null && !(dates.isEmpty())) { if (dates.getMaxValue() != null) { final DateFormat df = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'"); df.setTimeZone(TimeZone.getTimeZone("UTC")); builder.append("\t\t\t<time>").append(df.format(dates.getMaxValue())) .append("</time>").append("\n"); } } } if (elevation != null) { builder.append("\t\t\t<elevation>").append(elevation) .append("</elevation>").append("\n"); } else { SortedSet<Number> elevs = null; if (layerPostgrid != null) { try { elevs = layerPostgrid.getAvailableElevations(); } catch (DataStoreException ex) { LOGGER.log(Level.INFO, ex.getLocalizedMessage(), ex); elevs = null; } } if (elevs != null && !(elevs.isEmpty())) { builder.append("\t\t\t<elevation>").append(elevs.first().toString()) .append("</elevation>").append("\n"); } } if (!results.isEmpty()) { builder.append("\t\t\t<variable>") .append(results.get(0).getKey().getDescription()) .append("</variable>").append("\n"); } MeasurementRange[] ranges = null; if (layerPostgrid != null) { ranges = layerPostgrid.getSampleValueRanges(); } if (ranges != null && ranges.length > 0) { final MeasurementRange range = ranges[0]; if (range != null) { final Unit unit = range.unit(); if (unit != null && !unit.toString().isEmpty()) { builder.append("\t\t\t<unit>").append(unit.toString()) .append("</unit>").append("\n"); } } } builder.append("\t\t\t<value>").append(result) .append("</value>").append("\n") .append("\t\t</").append(layerName).append("_feature").append(endMark) .append("\t</").append(layerName).append("_layer").append(endMark); strs.add(builder.toString()); } /** * {@inheritDoc} */ @Override protected void nextProjectedFeature(ProjectedFeature graphic, RenderingContext2D context, SearchAreaJ2D queryArea) { final StringBuilder builder = new StringBuilder(); final FeatureMapLayer layer = graphic.getLayer(); final Feature feature = graphic.getCandidate(); final FeatureType featureType = feature.getType(); String margin = "\t"; if (mode == 0) { // featureType mark if (featureType != null) { final String ftLocal = featureType.getName().tip().toString(); builder.append(margin).append("<").append(encodeXML(ftLocal)).append("_feature").append(">\n"); margin += "\t"; builder.append(margin).append("<ID>").append(encodeXML(feature.getIdentifier().getID())).append("</ID>\n"); XMLFeatureInfoFormat.complexAttributetoXML(builder, feature, margin); // end featureType mark margin = margin.substring(1); builder.append(margin).append("</").append(encodeXML(ftLocal)).append("_feature").append(">\n"); } else { LOGGER.warning("The feature type is null"); } } else { // feature member mark builder.append(margin).append("<gml:featureMember>\n"); margin += "\t"; // featureType mark if (featureType != null) { String ftLocal = featureType.getName().tip().toString(); String ftPrefix = acquirePrefix(NamesExt.getNamespace(featureType.getName())); builder.append(margin).append('<').append(ftPrefix).append(ftLocal).append(">\n"); margin += "\t"; toGML3(builder, feature, margin); // end featureType mark margin = margin.substring(1); builder.append(margin).append("</").append(ftPrefix).append(ftLocal).append(">\n"); } else { LOGGER.warning("The feature type is null"); } // end feature member mark margin = margin.substring(1); builder.append(margin).append("</gml:featureMember>\n"); } final String result = builder.toString(); if (builder.length() > 0) { final String layerName = layer.getName(); List<String> strs = features.get(layerName); if (strs == null) { strs = new ArrayList<>(); features.put(layerName, strs); } strs.add(result.substring(0, result.length())); } } private void toGML3(final StringBuilder builder, final ComplexAttribute complexAtt, String margin) { for (final Property prop : complexAtt.getProperties()) { if (prop == null) { continue; } final GenericName propName = prop.getName(); if (propName == null) { continue; } String pLocal = propName.tip().toString(); String pPrefix = acquirePrefix(NamesExt.getNamespace(propName)); if (Geometry.class.isAssignableFrom(prop.getType().getBinding())) { GeometryAttribute geomProp = (GeometryAttribute) prop; builder.append(margin).append('<').append(pPrefix).append(pLocal).append(">\n"); Marshaller m = null; try { m = pool.acquireMarshaller(); StringWriter sw = new StringWriter(); org.opengis.geometry.Geometry gmlGeometry = JTSUtils.toISO((Geometry) prop.getValue(), geomProp.getType().getCoordinateReferenceSystem()); ObjectFactory factory = new ObjectFactory(); m.setProperty(Marshaller.JAXB_FRAGMENT, true); m.marshal(factory.buildAnyGeometry(gmlGeometry), sw); builder.append(sw.toString()); } catch (JAXBException ex) { LOGGER.log(Level.WARNING, "JAXB exception while marshalling the geometry", ex); } finally { if (m != null) { pool.recycle(m); } } builder.append("\n"); builder.append(margin).append("</").append(pPrefix).append(pLocal).append(">\n"); } else { if (prop instanceof ComplexAttribute) { final ComplexAttribute complex = (ComplexAttribute) prop; builder.append(margin).append('<').append(pPrefix).append(pLocal).append(">\n"); margin += "\t"; toGML3(builder, complex, margin); margin = margin.substring(1); builder.append(margin).append("</").append(pPrefix).append(pLocal).append(">\n"); } else { //simple final Object value = prop.getValue(); if (value != null) { builder.append(margin).append('<').append(pPrefix).append(pLocal).append('>') .append(value) .append("</").append(pPrefix).append(pLocal).append(">\n"); } else { builder.append(margin).append('<').append(pPrefix).append(pLocal).append("\\>\n"); } } } } } /** * {@inheritDoc} */ @Override public Object getFeatureInfo(SceneDef sdef, ViewDef vdef, CanvasDef cdef, Rectangle searchArea, GetFeatureInfo getFI) throws PortrayalException { this.gfi = getFI; final StringBuilder builder = new StringBuilder(); if ( getFI.getInfoFormat().equals(MimeType.APP_GML)) { mode = 0;//msGMLOutput } else { mode = 1;//GML3 } //fill coverages and features maps getCandidates(sdef, vdef, cdef, searchArea, -1); if (mode == 0) { // Map Server GML output builder.append("<?xml version=\"1.0\" encoding=\"UTF-8\"?>").append("\n") .append("<msGMLOutput xmlns:gml=\"http://www.opengis.net/gml\" ") .append("xmlns:xlink=\"http://www.w3.org/1999/xlink\" ") .append("xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\">") .append("\n"); } else { builder.append("<?xml version=\"1.0\" encoding=\"UTF-8\"?>").append("\n") .append("<gml:featureCollection ") .append("xmlns:xlink=\"http://www.w3.org/1999/xlink\" ") .append("xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" "); for (Map.Entry<String, String> entry: prefixMap.entrySet()) { builder.append("xmlns:").append(entry.getValue()).append("=\"").append(entry.getKey()).append("\" "); } builder.append(">\n"); } final Map<String, List<String>> values = new HashMap<>(); values.putAll(features); values.putAll(coverages); // optimization move this filter to getCandidates Integer maxValue = getFeatureCount(getFI); if (maxValue == null) { maxValue = 1; } for (String layerName : values.keySet()) { if (mode == 0) { builder.append("<").append(encodeXML(layerName)).append("_layer").append(">\n"); } int cpt = 0; for (final String record : values.get(layerName)) { builder.append(record); cpt++; if (cpt >= maxValue) break; } if (mode == 0) { builder.append("</").append(encodeXML(layerName)).append("_layer").append(">\n"); } } if (mode == 0) { builder.append("</msGMLOutput>"); } else { builder.append("</gml:featureCollection>"); } features.clear(); coverages.clear(); return builder.toString(); } /** * {@inheritDoc} */ @Override public List<String> getSupportedMimeTypes() { final List<String> mimes = new ArrayList<>(); mimes.add(MimeType.APP_GML);//will return map server GML mimes.add(MimeType.APP_GML_XML);//will return GML 3 return mimes; } /** * Returns the coordinates of the requested pixel in the image, expressed in the * {@linkplain CoordinateReferenceSystem crs} defined in the request. */ private GeneralDirectPosition getPixelCoordinates(final GetFeatureInfo gfi) { if (gfi != null) { JTSEnvelope2D objEnv = new JTSEnvelope2D(); int width = 0; int height = 0; int pixelX = 0; int pixelY = 0; if(gfi instanceof org.geotoolkit.wms.xml.GetFeatureInfo) { org.geotoolkit.wms.xml.GetFeatureInfo wmsGFI = (org.geotoolkit.wms.xml.GetFeatureInfo) gfi; objEnv = new JTSEnvelope2D(wmsGFI.getEnvelope2D()); width = wmsGFI.getSize().width; height = wmsGFI.getSize().height; pixelX = wmsGFI.getX(); pixelY = wmsGFI.getY(); } else if (gfi instanceof org.geotoolkit.wmts.xml.v100.GetFeatureInfo) { org.geotoolkit.wmts.xml.v100.GetFeatureInfo wmtsGFI = (org.geotoolkit.wmts.xml.v100.GetFeatureInfo) gfi; objEnv = new JTSEnvelope2D(); //gfi.getEnvelope()); width = 0; // gfi.getSize().width; height = 0; // gfi.getSize().height; pixelX = wmtsGFI.getI(); pixelY = wmtsGFI.getJ(); } final double widthEnv = objEnv.getSpan(0); final double heightEnv = objEnv.getSpan(1); final double resX = widthEnv / width; final double resY = -1 * heightEnv / height; final double geoX = (pixelX + 0.5) * resX + objEnv.getMinimum(0); final double geoY = (pixelY + 0.5) * resY + objEnv.getMaximum(1); final GeneralDirectPosition position = new GeneralDirectPosition(geoX, geoY); position.setCoordinateReferenceSystem(objEnv.getCoordinateReferenceSystem()); return position; } return null; } }