/*
* GeoTools - The Open Source Java GIS Toolkit
* http://geotools.org
*
* (C) 2015, Open Source Geospatial Foundation (OSGeo)
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation;
* version 2.1 of the License.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*/
package org.geotools.renderer.lite;
import java.util.ArrayList;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import org.geotools.data.Query;
import org.geotools.factory.CommonFactoryFinder;
import org.geotools.map.DirectLayer;
import org.geotools.map.FeatureLayer;
import org.geotools.map.Layer;
import org.geotools.map.MapContent;
import org.geotools.renderer.style.SLDStyleFactory;
import org.geotools.styling.FeatureTypeStyle;
import org.geotools.styling.Style;
import org.geotools.styling.StyleFactory;
import org.opengis.filter.expression.PropertyName;
import org.opengis.filter.sort.SortBy;
/**
* Builds {@link ZGroupLayer} instances from a MapContent using
* {@link FeatureTypeStyle#SORT_BY_GROUP} options
*
* @author Andrea Aime - GeoSolutions
*
*/
class ZGroupLayerFactory {
private static StyleFactory STYLE_FACTORY = CommonFactoryFinder.getStyleFactory();
/**
* Filters a MapContent and returns a new one where adjacent {@link FeatureTypeStyle} using the
* same {@link FeatureTypeStyle#SORT_BY_GROUP} key are turned into {@link ZGroupLayer}
*
* @param mapContent
* @return
*/
public static MapContent filter(MapContent mapContent) {
// Quick check, do we have any z-group to care for? For the common
// case of not having z-groups, we want to avoid building a new object and
// go though the complex splitting and checking logic
// Also check that the usage of groups is not made in conjunction with incompatible
// functionality
if (!hasZGroup(mapContent, false)) {
return mapContent;
}
// build a new map content where the z-groups are munched toghether into ZGroupLayer
MapContent result = new MapContent();
result.getViewport().setCoordinateReferenceSystem(
mapContent.getViewport().getCoordinateReferenceSystem());
result.getViewport().setBounds(mapContent.getViewport().getBounds());
ZGroupLayer currentGroup = null;
for (Layer layer : mapContent.layers()) {
if (layer instanceof DirectLayer) {
result.layers().add(layer);
currentGroup = null;
} else {
// in case this layer is clean, eventually dump the current group, dump the
// layer, and move on
if (!hasZGroup(layer, true)) {
if (currentGroup != null) {
result.layers().add(currentGroup);
currentGroup = null;
}
result.layers().add(layer);
} else {
// reorganize the layer in z-groups, and eventually attach to the current one
List<Layer> zGroupLayers = arrangeOnZGroups(layer, currentGroup);
// if the last in the list is a z-group, it's the new current, we don't
// want to add it into the MapContent yet, might join with the next layer
if (zGroupLayers.get(zGroupLayers.size() - 1) instanceof ZGroupLayer) {
currentGroup = (ZGroupLayer) zGroupLayers.remove(zGroupLayers.size() - 1);
} else {
currentGroup = null;
}
// dump the rest
for (Layer l : zGroupLayers) {
result.layers().add(l);
}
}
}
}
// dump the last group, if any
if (currentGroup != null) {
result.layers().add(currentGroup);
}
return result;
}
private static List<Layer> arrangeOnZGroups(Layer layer, ZGroupLayer previousGroup) {
List<Layer> splitLayers = new ArrayList<>();
if (previousGroup != null) {
splitLayers.add(previousGroup);
}
String currentGroupId = previousGroup != null ? previousGroup.getGroupId() : null;
List<FeatureTypeStyle> featureTypeStyles = new ArrayList<>();
for (FeatureTypeStyle fts : layer.getStyle().featureTypeStyles()) {
String groupName = fts.getOptions().get(FeatureTypeStyle.SORT_BY_GROUP);
if (!(groupName == currentGroupId
|| (groupName != null && groupName.equals(currentGroupId)))
&& !featureTypeStyles.isEmpty()) {
// the group name changed, dump the current feature type styles
addToSplitLayers(layer, previousGroup, splitLayers, currentGroupId,
featureTypeStyles);
}
featureTypeStyles.add(fts);
currentGroupId = groupName;
}
// add the residual fts, if needed
if (featureTypeStyles != null) {
addToSplitLayers(layer, previousGroup, splitLayers, currentGroupId, featureTypeStyles);
}
return splitLayers;
}
private static void addToSplitLayers(Layer layer, ZGroupLayer previousGroup,
List<Layer> splitLayers, String groupId, List<FeatureTypeStyle> featureTypeStyles) {
Style style = STYLE_FACTORY.createStyle();
style.featureTypeStyles().addAll(featureTypeStyles);
featureTypeStyles.clear();
FeatureLayer singleGroupLayer = buildNewFeatureLayer(layer, style);
if (groupId == null) {
splitLayers.add(singleGroupLayer);
} else if (previousGroup != null && groupId.equals(previousGroup.getGroupId())) {
previousGroup.addLayer(singleGroupLayer);
} else if (groupId != null) {
ZGroupLayer newZGroup = new ZGroupLayer(groupId, singleGroupLayer);
splitLayers.add(newZGroup);
}
}
private static FeatureLayer buildNewFeatureLayer(Layer layer, Style style) {
FeatureLayer singleGroupLayer = new FeatureLayer(layer.getFeatureSource(), style);
SortBy[] sortBy = SLDStyleFactory.getSortBy(style.featureTypeStyles().get(0).getOptions());
Query nativeQuery = layer.getQuery();
Query query = ensureSortProperties(nativeQuery, sortBy);
singleGroupLayer.setQuery(query);
singleGroupLayer.setTitle(layer.getTitle());
return singleGroupLayer;
}
/**
* Makes sure the properties needed for in-memory sorting are available by adding them into the
* query
*
* @param nativeQuery
* @param sortBy
* @return
*/
private static Query ensureSortProperties(Query nativeQuery, SortBy[] sortBy) {
LinkedHashSet<PropertyName> sortProperties = new LinkedHashSet<>();
for (SortBy sb : sortBy) {
PropertyName pn = sb.getPropertyName();
if (pn != null) {
sortProperties.add(pn);
}
}
List<PropertyName> nativeProperties = nativeQuery.getProperties();
Query q = new Query(nativeQuery);
if (nativeProperties == Query.ALL_PROPERTIES) {
q.setProperties(new ArrayList<>(sortProperties));
} else {
List<PropertyName> allProperties = new ArrayList<>(nativeProperties);
for (PropertyName propertyName : sortProperties) {
if (!allProperties.contains(propertyName)) {
allProperties.add(propertyName);
}
}
q.setProperties(allProperties);
}
return q;
}
private static boolean hasZGroup(MapContent mapContent, boolean checkValid) {
for (Layer layer : mapContent.layers()) {
if (hasZGroup(layer, true)) {
return true;
}
}
return false;
}
private static boolean hasZGroup(Layer layer, boolean checkValid) {
boolean hasGroup = false;
if (layer.getStyle() != null) {
for (FeatureTypeStyle fts : layer.getStyle().featureTypeStyles()) {
Map<String, String> options = fts.getOptions();
String groupName = options.get(FeatureTypeStyle.SORT_BY_GROUP);
if (groupName != null && !groupName.trim().isEmpty()) {
hasGroup = true;
if (checkValid) {
if (fts.getTransformation() != null) {
throw new IllegalArgumentException(
"Invalid " + FeatureTypeStyle.SORT_BY_GROUP + " usage in layer "
+ layer.getTitle()
+ ": cannot be mixed with rendering transformations");
} else if (options.get(FeatureTypeStyle.SORT_BY) == null) {
throw new IllegalArgumentException("Invalid "
+ FeatureTypeStyle.SORT_BY_GROUP + " usage in layer "
+ layer.getTitle()
+ ": the corresponding sortBy vendor option is missing");
}
}
}
}
}
if (hasGroup && !(layer instanceof FeatureLayer)) {
throw new IllegalArgumentException(
"Invalid " + FeatureTypeStyle.SORT_BY_GROUP + " usage in layer "
+ layer.getTitle() + ": can only be applied to vector layers");
}
return hasGroup;
}
}