/* Copyright (c) 2013-2014 Boundless and others.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Distribution License v1.0
* which accompanies this distribution, and is available at
* https://www.eclipse.org/org/documents/edl-v10.html
*
* Contributors:
* Victor Olaya (Boundless) - initial implementation
*/
package org.locationtech.geogig.osm.internal;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Set;
import javax.annotation.Nullable;
import org.geotools.feature.simple.SimpleFeatureBuilder;
import org.geotools.feature.simple.SimpleFeatureTypeBuilder;
import org.geotools.referencing.CRS;
import org.locationtech.geogig.storage.FieldType;
import org.locationtech.geogig.storage.text.TextValueSerializer;
import org.opengis.feature.Feature;
import org.opengis.feature.GeometryAttribute;
import org.opengis.feature.simple.SimpleFeature;
import org.opengis.feature.simple.SimpleFeatureType;
import org.opengis.feature.type.AttributeDescriptor;
import org.opengis.referencing.FactoryException;
import org.opengis.referencing.NoSuchAuthorityCodeException;
import org.opengis.referencing.crs.CoordinateReferenceSystem;
import org.openstreetmap.osmosis.core.domain.v0_6.Tag;
import com.google.common.base.Optional;
import com.google.common.base.Preconditions;
import com.google.common.collect.Lists;
import com.google.gson.annotations.Expose;
import com.vividsolutions.jts.geom.Coordinate;
import com.vividsolutions.jts.geom.Geometry;
import com.vividsolutions.jts.geom.GeometryFactory;
import com.vividsolutions.jts.geom.Point;
import com.vividsolutions.jts.geom.Polygon;
/**
* A rule used to convert an OSM entity into a feature with a custom feature type. Attributes values
* for the attributes in the feature type are taken from the tags and geometry of the feature to
* convert
*
*/
public class MappingRule {
public enum GeomRestriction {
ONLY_OPEN_LINES("open"), ONLY_CLOSED_LINES("closed"), ALL_LINESTRINGS("all");
private String s;
GeomRestriction(String s) {
this.s = s;
}
public String getModifierString() {
return s;
}
public static GeomRestriction valueFromText(String s) {
for (GeomRestriction gr : values()) {
if (gr.getModifierString().equals(s)) {
return gr;
}
}
throw new NoSuchElementException("Unknown modifier: " + s);
}
}
public enum DefaultField {
visible(Boolean.class), timestamp(Long.class), tags(String.class), changeset(Long.class), version(
Integer.class), user(String.class);
private Class<?> clazz;
private static ArrayList<Object> names = Lists.newArrayList();
static {
for (DefaultField v : DefaultField.values()) {
names.add(v.name());
}
}
DefaultField(Class<?> clazz) {
this.clazz = clazz;
}
public Class<?> getFieldClass() {
return clazz;
}
public static boolean isDefaultField(String s) {
return names.contains(s);
}
}
/**
* The name of the rule
*/
@Expose
private String name;
/**
* A map of key, list_of_accepted_values, to be used to filter features. If a feature has any of
* the keys in this map with any of the accepted values, it will be transformed by this rule
*/
@Expose
private Map<String, List<String>> filter;
/**
* A map of key, list_of_values_to_exclude, to be used to filter features. If a feature has any
* of the keys in this map with any of the values in the list, it will not be transformed by
* this rule, even if it meets the conditions set by the 'filter' object
*/
@Expose
@Nullable
private Map<String, List<String>> exclude;
/**
* The fields to use for the custom feature type of the transformed feature
*/
@Expose
private Map<String, AttributeDefinition> fields;
/**
* The default fields to include in the destination feature type without transforming them
*/
@Expose
@Nullable
private List<DefaultField> defaultFields;
private SimpleFeatureType featureType;
private SimpleFeatureBuilder featureBuilder;
private Class<?> geometryType;
private GeomRestriction geomRestriction;
private ArrayList<String> _mandatoryTags = null;
private static GeometryFactory gf = new GeometryFactory();
public MappingRule(final String name, final Map<String, List<String>> filter,
@Nullable final Map<String, List<String>> filterExclude,
final Map<String, AttributeDefinition> fields,
@Nullable final List<DefaultField> defaultFields) {
Preconditions.checkNotNull(name);
Preconditions.checkNotNull(filter);
Preconditions.checkNotNull(fields);
this.name = name;
this.filter = filter;
this.exclude = filterExclude;
this.fields = fields;
this.defaultFields = defaultFields;
ArrayList<String> names = Lists.newArrayList();
for (AttributeDefinition ad : fields.values()) {
Preconditions.checkState(!names.contains(ad.getName()),
"Duplicated alias in mapping rule: " + ad.getName());
names.add(ad.getName());
}
}
/**
* Returns the feature type defined by this rule. This is the feature type that features
* transformed by this rule will have
*
* @return
*/
public SimpleFeatureType getFeatureType() {
if (featureType == null) {
SimpleFeatureTypeBuilder fb = new SimpleFeatureTypeBuilder();
fb.setName(name);
fb.add("id", Long.class);
if (defaultFields != null) {
for (DefaultField df : defaultFields) {
fb.add(df.name().toLowerCase(), df.getFieldClass());
}
}
Set<String> keys = this.fields.keySet();
for (String key : keys) {
AttributeDefinition field = fields.get(key);
Class<?> clazz = field.getType().getBinding();
if (Geometry.class.isAssignableFrom(clazz)) {
Preconditions.checkArgument(geometryType == null,
"The mapping has more than one geometry attribute");
CoordinateReferenceSystem epsg4326;
try {
epsg4326 = CRS.decode("EPSG:4326", true);
fb.add(field.getName(), clazz, epsg4326);
} catch (NoSuchAuthorityCodeException e) {
} catch (FactoryException e) {
}
geometryType = clazz;
} else {
fb.add(field.getName(), clazz);
}
}
Preconditions.checkNotNull(geometryType,
"The mapping rule does not define a geometry field");
if (!geometryType.equals(Point.class)) {
fb.add("nodes", String.class);
}
featureType = fb.buildFeatureType();
featureBuilder = new SimpleFeatureBuilder(featureType);
}
return featureType;
}
private GeomRestriction getGeomRestriction() {
if (geomRestriction == null) {
if (filter.containsKey("geom")) {
geomRestriction = GeomRestriction.valueFromText(filter.get("geom").get(0));
} else {
geomRestriction = GeomRestriction.ALL_LINESTRINGS;
}
}
return geomRestriction;
}
/**
* Returns the feature resulting from transforming a given feature using this rule
*
* @param feature
* @return
*/
public Optional<Feature> apply(Feature feature) {
String tagsString = (String) ((SimpleFeature) feature).getAttribute("tags");
Collection<Tag> tags = OSMUtils.buildTagsCollectionFromString(tagsString);
return apply(feature, tags);
}
/**
* Returns the feature resulting from transforming a given feature using this rule. This method
* takes a collection of tags, so there is no need to compute them from the 'tags' attribute.
* This is meant as a faster alternative to the apply(Feature) method, in case the mapping
* object calling this has already computed the tags, to avoid recomputing them
*
* @param feature
* @param tags
* @return
*/
public Optional<Feature> apply(Feature feature, Collection<Tag> tags) {
if (!canBeApplied(feature, tags)) {
return Optional.absent();
}
for (AttributeDescriptor attribute : getFeatureType().getAttributeDescriptors()) {
String attrName = attribute.getName().toString();
Class<?> clazz = attribute.getType().getBinding();
if (Geometry.class.isAssignableFrom(clazz)) {
Geometry geom = prepareGeometry((Geometry) feature.getDefaultGeometryProperty()
.getValue());
if (geom == null) {
return Optional.absent();
}
featureBuilder.set(attrName, geom);
} else {
Object value = null;
for (Tag tag : tags) {
if (fields.containsKey(tag.getKey())) {
if (fields.get(tag.getKey()).getName().equals(attrName)) {
FieldType type = FieldType.forBinding(clazz);
value = getAttributeValue(tag.getValue(), type);
break;
}
}
}
featureBuilder.set(attribute.getName(), value);
}
}
String id = feature.getIdentifier().getID();
featureBuilder.set("id", id);
if (defaultFields != null) {
for (DefaultField df : defaultFields) {
featureBuilder.set(df.name(), feature.getProperty(df.name()).getValue());
}
}
if (!featureType.getGeometryDescriptor().getType().getBinding().equals(Point.class)) {
featureBuilder.set("nodes", feature.getProperty("nodes").getValue());
}
return Optional.of((Feature) featureBuilder.buildFeature(id));
}
private Geometry prepareGeometry(Geometry geom) {
if (geometryType.equals(Polygon.class)) {
Coordinate[] coords = geom.getCoordinates();
if (!coords[0].equals(coords[coords.length - 1])) {
Coordinate[] newCoords = new Coordinate[coords.length + 1];
System.arraycopy(coords, 0, newCoords, 0, coords.length);
newCoords[coords.length] = coords[0];
coords = newCoords;
}
if (coords.length < 4) {
return null;
}
return gf.createPolygon(coords);
}
return geom;
}
private Object getAttributeValue(String value, FieldType type) {
return TextValueSerializer.fromString(type, value);
}
public boolean canBeApplied(Feature feature, Collection<Tag> tags) {
return hasCorrectTags(feature, tags) && hasCompatibleGeometryType(feature);
}
private boolean hasCompatibleGeometryType(Feature feature) {
getFeatureType();
GeomRestriction restriction = getGeomRestriction();
GeometryAttribute property = feature.getDefaultGeometryProperty();
Geometry geom = (Geometry) property.getValue();
if (geom.getClass().equals(Point.class)) {
return geometryType == Point.class;
} else {
if (geometryType.equals(Point.class)) {
return false;
}
Coordinate[] coords = geom.getCoordinates();
if (geometryType.equals(Polygon.class) && coords.length < 3) {
return false;
}
boolean isClosed = coords[0].equals(coords[coords.length - 1]);
if (isClosed && restriction.equals(GeomRestriction.ONLY_OPEN_LINES)) {
return false;
}
if (!isClosed && restriction.equals(GeomRestriction.ONLY_CLOSED_LINES)) {
return false;
}
return true;
}
}
private boolean hasCorrectTags(Feature feature, Collection<Tag> tags) {
if (filter.isEmpty() || (filter.size() == 1 && filter.containsKey("geom"))
&& (exclude == null || exclude.isEmpty())) {
return true;
}
boolean ret = false;
ArrayList<String> tagNames = Lists.newArrayList();
for (Tag tag : tags) {
tagNames.add(tag.getKey());
if (exclude != null && exclude.keySet().contains(tag.getKey())) {
List<String> values = exclude.get(tag.getKey());
if (values != null) {
if (values.isEmpty() || values.contains(tag.getValue())) {
return false;
}
}
}
if (filter.keySet().contains(tag.getKey())) {
List<String> values = filter.get(tag.getKey());
if (values.isEmpty() || values.contains(tag.getValue())) {
ret = true;
}
}
}
if (ret) {
for (String mandatory : getMandatoryTags()) {
if (!tagNames.contains(mandatory)) {
return false;
}
}
}
return ret;
}
public String getName() {
return name;
}
/**
* Returns true if this rule generates feature with a line or polygon geometry, or it doesn't
* have a geometry attribute, so it can take ways as inputs
*
* @return
*/
public boolean canUseWays() {
getFeatureType();
return !geometryType.equals(Point.class);
}
/**
* Returns true if this rule generates feature with a point geometry, or it doesn't have a
* geometry attribute, so it can take nodes as inputs
*
* @return
*/
public boolean canUseNodes() {
getFeatureType();
return geometryType.equals(Point.class);
}
/**
* Resolves the original tag name based on the name of a field created by this mapping rule (an
* alias for a tag name) *
*
* @param field the name of the field
* @return the name of the tag from which the passed field was created in the specified mapped
* tree. If the alias cannot be resolved, that passed alias itself is returned
*/
public String getTagNameFromAlias(String alias) {
Set<String> keys = this.fields.keySet();
for (String key : keys) {
AttributeDefinition field = fields.get(key);
if (field.getName().equals(alias)) {
return key;
}
}
return alias;
}
public boolean equals(Object o) {
if (o instanceof MappingRule) {
MappingRule m = (MappingRule) o;
return name.equals(m.name) && m.fields.equals(fields) && m.filter.equals(filter)
&& m.exclude.equals(exclude) && m.defaultFields.equals(defaultFields);
} else {
return false;
}
}
private ArrayList<String> getMandatoryTags() {
if (_mandatoryTags == null) {
_mandatoryTags = Lists.newArrayList();
if (exclude != null) {
for (String key : this.exclude.keySet()) {
if (exclude.get(key) == null) {
_mandatoryTags.add(key);
}
}
}
}
return _mandatoryTags;
}
}