/*
* GeoTools - The Open Source Java GIS Toolkit
* http://geotools.org
*
* (C) 2017, 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.mbstyle.parse;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import org.json.simple.JSONArray;
import org.opengis.filter.Filter;
import org.opengis.filter.FilterFactory2;
import org.opengis.filter.expression.Expression;
import org.opengis.filter.expression.Function;
import org.opengis.filter.identity.FeatureId;
import org.opengis.style.SemanticType;
/**
* MBFilter json wrapper, allowing conversion to a GeoTools Filter.
* <p>
* This wrapper class is used by {@link MBObjectParser} to generate rule filters when transforming
* MBStyle.
* </p>
* <p>
* This wrapper and {@link MBFunction} are a matched set handling dynamic data.
* </p>
*
* <h2>About MapBox Filter</h2>
* <p>
* A filter selects specific features from a layer. A filter is an array of one of the following
* forms:
* </p>
* <p>
* Existential Filters
* </p>
* <ul>
* <li><code>["has", key]<code> - feature[key] exists</li>
* <li><code>["!has", key]</code> - feature[key] does not exist</li>
* </ul>
* <p>
* Comparison Filters:
* </p>
* <ul>
* <li>["==", key, value] equality: feature[key] = value</li>
* <li>["!=", key, value] inequality: feature[key] ≠ value</li>
* <li>[">", key, value] greater than: feature[key] > value</li>
* <li>[">=", key, value] greater than or equal: feature[key] ≥ value</li>
* <li>["<", key, value] less than: feature[key] < value</li>
* <li>["<=", key, value] less than or equal: feature[key] ≤ value</li>
* </ul>
* <p>
* Set Memmbership Filters:</p>
* <ul>
* <li>["in", key, v0, ..., vn] set inclusion: feature[key] ∈ {v0, ..., vn}</li>
* <li>["!in", key, v0, ..., vn] set exclusion: feature[key] ∉ {v0, ..., vn}</li>
* </ul>
* <p>
* Combining Filters:</p>
* <ul>
* <li>["all", f0, ..., fn] logical AND: f0 ∧ ... ∧ fn</li>
* <li>["any", f0, ..., fn] logical OR: f0 ∨ ... ∨ fn</li>
* <li>["none", f0, ..., fn] logical NOR: ¬f0 ∧ ... ∧ ¬fn</li>
* </ul>
* <p>
* A key must be a string that identifies a feature property, or one of the following special keys:</p>
* <ul>
* <li>"$type": the feature type. This key may be used with the "==", "!=", "in", and "!in" operators. Possible values are "Point", "LineString", and "Polygon".</li>
* <li>"$id": the feature identifier. This key may be used with the "==", "!=", "has", "!has", "in", and "!in" operators.</li>
* </ul>
*
* @see Filter
* @see MBFunction
*/
public class MBFilter {
public static final String TYPE_POINT = "Point";
public static final String TYPE_LINE = "LineString";
public static final String TYPE_POLYGON = "Polygon";
/** Default semanticType (or null for "geometry"). */
final protected SemanticType semanticType;
/** Parser context. */
final protected MBObjectParser parse;
/** Wrapped json */
final protected JSONArray json;
public MBFilter(JSONArray json) {
this(json, new MBObjectParser(MBFilter.class));
}
public MBFilter(JSONArray json, MBObjectParser parse) {
this( json, parse, null );
}
public MBFilter(JSONArray json, MBObjectParser parse, SemanticType semanticType) {
this.parse = parse == null ? new MBObjectParser(MBFilter.class)
: new MBObjectParser(MBFilter.class, parse);
this.json = json;
this.semanticType = semanticType;
}
/**
* Translate "$type": the feature type we need This key may be used with the "==", "!=", "in", and "!in" operators.
* Possible values are "Point", "LineString", and "Polygon".</li>
*
* @return
*/
public Set<SemanticType> semanticTypeIdentifiers(){
if (json == null || json.isEmpty()) {
return semanticTypeIdentifiersDefaults();
}
Set<SemanticType> semanticTypes = semanticTypeIdentifiers(json);
return semanticTypes.isEmpty() ? semanticTypeIdentifiersDefaults() : semanticTypes;
}
/**
* Generate default set of semantic types based on {@link #semanticType} default.
*
* @return default to use, if nothing is provided explicitly by json "$type" field.
*/
private Set<SemanticType> semanticTypeIdentifiersDefaults(){
Set<SemanticType> defaults = new HashSet<>();
if( semanticType != null ){
defaults.add(semanticType); // default as provided
}
return defaults;
}
/**
* Utility method to convert json to set of {@link SemanticType}.
* <p>
* This method recursively calls itself to handle all and any operators.</p>
*
* @param array JSON array defining filter
* @return SemanticTypes from provided json, may be nested
*/
Set<SemanticType> semanticTypeIdentifiers(JSONArray array){
if (array == null || array.isEmpty()) {
throw new MBFormatException("MBFilter expected");
}
String operator = parse.get(array, 0);
if (("==".equals(operator) || "!=".equals(operator) ||
"in".equals(operator) || "!in".equals(operator)) &&
"$type".equals(parse.get(array, 1))) {
if ("in".equals(operator) || "==".equals(operator)) {
Set<SemanticType> semanticTypes = new HashSet<>();
List<?> types = array.subList(2, array.size());
for (Object type : types) {
if (type instanceof String) {
String jsonText = (String) type;
SemanticType semanticType = translateSemanticType(jsonText);
semanticTypes.add(semanticType);
} else {
throw new MBFormatException("[\"in\",\"$type\", ...] limited to Point, LineString, Polygon: "+type);
}
}
if ("==".equals(operator) && types.size() != 1) {
throw new MBFormatException("[\"==\",\"$type\", ...] limited one geometry type, to test more than one use \"in\" operator.");
}
return semanticTypes;
} else if ("!in".equals(operator) || "!=".equals(operator)) {
Set<SemanticType> semanticTypes = new HashSet<>( Arrays.asList(SemanticType.values()) );
List<?> types = array.subList(2, array.size());
for (Object type : types ) {
if (type instanceof String) {
String jsonText = (String) type;
SemanticType semanticType = translateSemanticType(jsonText);
semanticTypes.remove(semanticType);
} else {
throw new MBFormatException("[\"!in\",\"$type\", ...] limited to Point, LineString, Polygon: "+type);
}
}
if ("!=".equals(operator) && types.size() != 1) {
throw new MBFormatException("[\"!=\",\"$type\", ...] limited one geometry type, to test more than one use \"!in\" operator.");
}
return semanticTypes;
}
}
if ("all".equals(operator)) {
Set<SemanticType> semanticTypes = new HashSet<>();
for( int i = 1; i < json.size();i++){
JSONArray alternative = (JSONArray) json.get(i);
Set<SemanticType> types = semanticTypeIdentifiers(alternative);
if (types.isEmpty()) {
continue;
} else {
if (semanticTypes.isEmpty()) {
// exactly one alternative is okay
semanticTypes.addAll(types);
} else {
throw new MBFormatException("Only one \"all\" alternative may be a $type filter");
}
}
}
return semanticTypes;
} else if ("any".equals(operator)) {
Set<SemanticType> semanticTypes = new HashSet<>();
for (int i = 1; i < json.size();i++) {
Set<SemanticType> types = semanticTypeIdentifiers((JSONArray) json.get(i));
semanticTypes.addAll(types);
}
return semanticTypes;
} else if( "none".equals(operator)) {
Set<SemanticType> semanticTypes = new HashSet<>(Arrays.asList(SemanticType.values()));
for (int i = 1; i < json.size();i++) {
Set<SemanticType> types = semanticTypeIdentifiers((JSONArray) json.get(i));
semanticTypes.removeAll(types);
}
return semanticTypes;
}
return Collections.emptySet();
}
private Filter translateType(String jsonText) {
final FilterFactory2 ff = parse.getFilterFactory();
//TODO: How to wildcard geometry
Expression dimension = ff.function("dimension", ff.function("geometry"));
switch (jsonText) {
case TYPE_POINT:
return ff.equals(dimension, ff.literal(0));
case TYPE_LINE:
return ff.equals(dimension, ff.literal(1));
case TYPE_POLYGON:
return ff.and(
ff.equals(dimension, ff.literal(2)),
ff.not(ff.equals(ff.function("isCoverage"), ff.literal(true))));
default:
return null;
}
}
/**
* Translate from json "Point", "LineString", and "Polygon".
* @param jsonText
* @return translate from jsonText
*/
private SemanticType translateSemanticType(String jsonText) {
switch (jsonText) {
case TYPE_POINT:
return SemanticType.POINT;
case TYPE_LINE:
return SemanticType.LINE;
case TYPE_POLYGON:
return SemanticType.POLYGON;
default:
return null;
}
}
/**
* Generate GeoTools {@link Filter} from json definition.
* <p>
* This filter specifying conditions on source features. Only features that match the filter are
* displayed.
* </p>
*
* @return GeoTools {@link Filter} specifying conditions on source features.
*/
public Filter filter() {
final FilterFactory2 ff = parse.getFilterFactory();
if (json == null || json.isEmpty()) {
return Filter.INCLUDE; // by default include everything!
}
String operator = parse.get(json, 0);
//
// TYPE
//
if (("==".equals(operator) || "!=".equals(operator) ||
"in".equals(operator) || "!in".equals(operator)) &&
"$type".equals(parse.get(json, 1))) {
List<Filter> typeFilters = new ArrayList<>();
List<?> types = json.subList(2, json.size());
for (Object type : types ) {
Filter typeFilter = null;
if (type instanceof String) {
typeFilter = translateType((String) type);
}
if (typeFilter == null) {
throw new MBFormatException("\"$type\" limited to Point, LineString, Polygon: "+type);
}
typeFilters.add(typeFilter);
}
if ("==".equals(operator)) {
if (typeFilters.size() != 1) {
throw new MBFormatException("[\"==\",\"$type\", ...] limited one geometry type, to test more than one use \"in\" operator.");
}
return typeFilters.get(0);
}
if ("!=".equals(operator)) {
if (typeFilters.size() != 1) {
throw new MBFormatException("[\"!=\",\"$type\", ...] limited one geometry type, to test more than one use \"!in\" operator.");
}
return ff.not(typeFilters.get(0));
}
if ("in".equals(operator)) {
return ff.or(typeFilters);
}
if ("!in".equals(operator)) {
return ff.not(ff.or(typeFilters));
}
}
//
// ID
//
if (("==".equals(operator) || "!=".equals(operator) ||
"has".equals(operator) || "!has".equals(operator) ||
"in".equals(operator) || "!in".equals(operator)) &&
"$id".equals(parse.get(json, 1))) {
Set<FeatureId> fids = new HashSet<>();
for (Object value : json.subList(2, json.size())) {
if (value instanceof String) {
String fid = (String) value;
fids.add(ff.featureId(fid));
}
}
if ("has".equals(operator) || "in".equals(operator)) {
return ff.id(fids);
} else if ("!has".equals(operator) || "!in".equals(operator)) {
return ff.not(ff.id(fids));
} else {
throw new UnsupportedOperationException("$id \"" + operator + "\" not valid");
}
}
//
// Feature Property
//
// Existential Filters
if ("has".equals(operator)) {
String key = parse.get(json, 1);
return ff.isNull(ff.property(key)); // null is the same as no value present
} else if ("!has".equals(operator)) {
String key = parse.get(json, 1);
return ff.not(ff.isNull(ff.property(key)));
// Comparison Filters
} else if ("==".equals(operator)) {
String key = parse.get(json, 1);
Object value = parse.value(json,2);
return ff.equal(ff.property(key),ff.literal(value), false);
} else if ("!=".equals(operator)) {
String key = parse.get(json, 1);
Object value = parse.value(json,2);
return ff.notEqual(ff.property(key),ff.literal(value), false);
} else if (">".equals(operator)) {
String key = parse.get(json, 1);
Object value = parse.value(json,2);
return ff.greater(ff.property(key),ff.literal(value), false);
} else if (">=".equals(operator)) {
String key = parse.get(json, 1);
Object value = parse.value(json,2);
return ff.greaterOrEqual(ff.property(key),ff.literal(value), false);
} else if ("<".equals(operator)) {
String key = parse.get(json, 1);
Object value = parse.value(json,2);
return ff.less(ff.property(key),ff.literal(value), false);
} else if ("<=".equals(operator)) {
String key = parse.get(json, 1);
Object value = parse.value(json, 2);
return ff.lessOrEqual(ff.property(key), ff.literal(value), false);
// Set Membership Filters
} else if ("in".equals(operator)) {
String key = parse.get(json, 1);
Expression[] args = new Expression[json.size()-1];
args[0] = ff.property(key);
for (int i=1; i<args.length;i++) {
Object value = parse.value( json,i+1);
args[i] = ff.literal( value );
}
Function in = ff.function("in", args );
return ff.equal( in, ff.literal(true));
} else if ("!in".equals(operator)) {
String key = parse.get(json, 1);
Expression[] args = new Expression[json.size() - 1];
args[0] = ff.property(key);
for (int i = 1; i < args.length; i++) {
Object value = parse.value(json, i + 1);
args[i] = ff.literal(value);
}
Function in = ff.function("in", args);
return ff.equal(in, ff.literal(false));
// Combining Filters
} else if ("all".equals(operator)) {
List<Filter> all = new ArrayList<>();
for (int i = 1; i < json.size();i++) {
MBFilter mbFilter = new MBFilter((JSONArray) json.get(i));
Filter filter = mbFilter.filter();
if (filter != Filter.INCLUDE) {
all.add(filter);
}
}
return ff.and(all);
} else if ("any".equals(operator)) {
List<Filter> any = new ArrayList<>();
for (int i = 1; i < json.size(); i++) {
MBFilter mbFilter = new MBFilter((JSONArray) json.get(i));
Filter filter = mbFilter.filter();
if (filter != Filter.INCLUDE) {
any.add(filter);
}
}
return ff.or(any);
} else if ("none".equals(operator)) {
List<Filter> none = new ArrayList<>();
for (int i = 1; i < json.size(); i++) {
// using not here so we can short circuit the and filter below
MBFilter mbFilter = new MBFilter((JSONArray) json.get(i));
Filter filter = mbFilter.filter();
if (filter != Filter.INCLUDE) {
none.add(ff.not(filter));
}
}
return ff.and(none);
} else {
throw new MBFormatException("Unsupported filter "+json);
}
}
}