package org.geotoolkit.data.kml;
import com.vividsolutions.jts.geom.Coordinate;
import com.vividsolutions.jts.geom.Geometry;
import com.vividsolutions.jts.geom.GeometryCollection;
import com.vividsolutions.jts.geom.GeometryFactory;
import com.vividsolutions.jts.geom.LineString;
import com.vividsolutions.jts.geom.LinearRing;
import com.vividsolutions.jts.geom.MultiLineString;
import com.vividsolutions.jts.geom.Point;
import com.vividsolutions.jts.geom.Polygon;
import org.geotoolkit.data.kml.model.AbstractGeometry;
import org.geotoolkit.data.kml.model.Boundary;
import org.geotoolkit.data.kml.model.Data;
import org.geotoolkit.data.kml.model.DefaultExtendedData;
import org.geotoolkit.data.kml.model.DefaultMultiGeometry;
import org.geotoolkit.data.kml.model.ExtendedData;
import org.geotoolkit.data.kml.model.IdAttributes;
import org.geotoolkit.data.kml.model.Kml;
import org.geotoolkit.data.kml.model.MultiGeometry;
import org.geotoolkit.data.kml.model.SchemaData;
import org.geotoolkit.data.kml.model.SimpleData;
import org.geotoolkit.data.kml.xml.KmlReader;
import org.geotoolkit.geometry.jts.JTS;
import org.apache.sis.referencing.CommonCRS;
import org.apache.sis.internal.referencing.j2d.AffineTransform2D;
import org.geotoolkit.nio.IOUtilities;
import org.geotoolkit.nio.PosixDirectoryFilter;
import org.geotoolkit.nio.ZipUtilities;
import org.opengis.referencing.operation.TransformException;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.file.*;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.AbstractMap;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.apache.sis.feature.builder.AttributeRole;
import org.apache.sis.feature.builder.FeatureTypeBuilder;
import org.apache.sis.internal.feature.AttributeConvention;
import org.apache.sis.util.logging.Logging;
import org.opengis.feature.Feature;
import org.opengis.feature.FeatureType;
import org.opengis.feature.Property;
import org.opengis.feature.PropertyType;
import org.geotoolkit.data.kml.xml.KmlConstants;
/**
* Generate a {@link org.opengis.feature.Feature} {@link java.util.List} from kml/kmz folder or file
* @author bgarcia
* @since 11/04/13
*/
public class KmlFeatureUtilities {
private static final Logger LOGGER = Logging.getLogger("org.geotoolkit.data.kml");
/**
* create {@link SimpleFeature} from kml or kmz file on a folder
* @param directory folder which have kml files
* @return a {@link SimpleFeature} {@link List}
*/
public static List<Feature> getAllKMLGeometriesEntries(final Path directory) throws IOException {
final List<Feature> results = new ArrayList<>();
if (Files.isDirectory(directory)) {
//first loop to unzip kmz files
try(DirectoryStream<Path> filteredStream = Files.newDirectoryStream(directory, new PosixDirectoryFilter("*.kmz", true))) {
for (Path path : filteredStream) {
if (LOGGER.isLoggable(Level.FINE)) {
LOGGER.log(Level.FINE, "getAllKMLGeometriesEntries unzipping kmz file : {0}", path.getFileName().toString());
}
ZipUtilities.unzip(path, null);
}
} catch (Exception ex) {
LOGGER.log(Level.WARNING, "Error on unzip kmz file", ex);
}
final KmlReader reader = new KmlReader();
try {
//first loop to unzip kmz files recursively
Files.walkFileTree(directory, new SimpleFileVisitor<Path>() {
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
final String ext = IOUtilities.extension(file);
if ("kml".equalsIgnoreCase(ext)) {
if (LOGGER.isLoggable(Level.FINE)) {
LOGGER.log(Level.FINE, "getAllKMLGeometriesEntries proceed to extract features for kml : {0}",
file.getFileName().toString());
}
try {
// create kml reader for current file
reader.setInput(file);
reader.setUseNamespace(false);
final Kml kmlObject = reader.read();
// find features and add it on lisr
final List<Feature> simplefeatList = resolveFeaturesFromKml(kmlObject);
results.addAll(simplefeatList);
} catch (Exception ex) {
LOGGER.log(Level.WARNING, ex.getMessage(), ex);
}
}
return FileVisitResult.CONTINUE;
}
});
} finally {
try {
reader.dispose();
} catch (Exception ex) {
LOGGER.log(Level.WARNING, ex.getMessage(), ex);
}
}
}
return results;
}
/**
* get {@link SimpleFeature} {@link List} from a {@link Kml} object
* @param kmlObject : object which can have feature to extract
* @return {@link SimpleFeature} {@link List} include in kml file
*/
public static List<Feature> resolveFeaturesFromKml(final Kml kmlObject) {
final List<Feature> results = new ArrayList<>();
if (kmlObject != null) {
final Feature document = kmlObject.getAbstractFeature();
final Iterator<?> propertiesFeat = ((Iterable<?>) document.getPropertyValue(KmlConstants.TAG_FEATURES)).iterator();
//increment for each features
int idgeom = 0;
//loop on document properties
while (propertiesFeat.hasNext()) {
final Object object = propertiesFeat.next();
if (object instanceof Feature) {
final Feature candidat = (Feature) object;
//find geometry on tree
final List<Map.Entry<Object, Map<String, String>>> geometries = new ArrayList<>();
fillGeometryListFromFeature(candidat, geometries);
//if geometry was found
if (!geometries.isEmpty()) {
//loop to create simpleFeature from geometry
for (final Map.Entry<Object, Map<String, String>> geometry : geometries) {
Feature simpleFeature = extractFeature(idgeom, geometry);
//test if geometry already exist
if(simpleFeature!=null){
if (!results.contains(simpleFeature)) {
results.add(simpleFeature);
idgeom++;
}
}
}
}
}
}
}
return results;
}
/**
* create a {@link SimpleFeature} from an {@link Map.Entry}
* @param idgeom current id iterator
* @param geometry : {@link Map.Entry} which contains a geometry
* @return a {@link SimpleFeature}
*/
private static Feature extractFeature(int idgeom, Map.Entry<Object, Map<String, String>> geometry) {
final Object geom = geometry.getKey();
Geometry finalGeom = null;
//if it's a simple geometry
if (geom instanceof Geometry) {
finalGeom = (Geometry) geometry.getKey();
try {
//if it's lineString it can cut meridian. So test it
if (finalGeom instanceof LineString) {
LineString lineString = (LineString) finalGeom;
MultiLineString multiLine = cutAtMeridian(lineString);
if(multiLine!=null){
finalGeom = multiLine;
}
}
} catch (Exception ex) {
LOGGER.log(Level.WARNING, ex.getMessage(), ex);
}
}
//it's a geometry collection
else if(geom instanceof DefaultMultiGeometry){
final DefaultMultiGeometry kmlabstractGeometry = (DefaultMultiGeometry)geom;
final List<Geometry> multiGeometry = new ArrayList<>(0);
//loop on geometry to add id on a GeometryList
for (AbstractGeometry abstractGeometry : kmlabstractGeometry.getGeometries()) {
if(abstractGeometry instanceof Geometry){
final Geometry currentGeom = (Geometry)abstractGeometry;
multiGeometry.add(currentGeom);
}
}
final GeometryFactory gf = new GeometryFactory();
Geometry[] geometryArray = new Geometry[multiGeometry.size()];
for (int i = 0; i < multiGeometry.size(); i++) {
geometryArray[i] = multiGeometry.get(i);
}
finalGeom = new GeometryCollection(geometryArray, gf);
}
if(finalGeom!=null){
return BuildSimpleFeature(idgeom, geometry.getValue(), finalGeom);
}
return null;
}
/**
* Build simple feature
* @param idgeom geometry id
* @param values no geographic data
* @param finalGeom geometry need to be insert in feature
* @return a {@link SimpleFeature}
*/
private static Feature BuildSimpleFeature(int idgeom, Map<String, String> values, Geometry finalGeom) {
//Building simplefeature
final FeatureTypeBuilder ftb = new FeatureTypeBuilder();
final String name = "Geometry";
ftb.setName(name);
ftb.addAttribute(Geometry.class).setName("geometry").setCRS(CommonCRS.WGS84.normalizedGeographic()).addRole(AttributeRole.DEFAULT_GEOMETRY);
//loop on values to find data names
for (String valName : values.keySet()) {
ftb.addAttribute(String.class).setName(valName);
}
final FeatureType sft = ftb.build();
final Feature simpleFeature = sft.newInstance();
simpleFeature.setPropertyValue(AttributeConvention.IDENTIFIER_PROPERTY.toString(), "feature" + idgeom);
//add geometry
simpleFeature.setPropertyValue("geometry", finalGeom);
//add other data
for (String valName : values.keySet()) {
simpleFeature.setPropertyValue(valName, values.get(valName));
}
return simpleFeature;
}
/**
* recursive method on feature to find geometries
* @param feature {@link org.opengis.feature.Feature} traveled to find geometry
* @param geometries {@link List} where we add {@link Geometry}
*/
public static void fillGeometryListFromFeature(final Feature feature, final List<Map.Entry<Object, Map<String, String>>> geometries) {
final Object geometry = feature.getPropertyValue("geometry");
if (geometry != null) {
// create map which have other data value
final Map<String, String> values = new HashMap<>(0);
// get feature name
final String name = (String) feature.getPropertyValue("name");
if(name!=null){
values.put("name", name);
}
//get extendedData
final DefaultExtendedData extendData = (DefaultExtendedData) feature.getPropertyValue("ExtendedData");
if(extendData!=null){
// loop on extendedSchemaData to find data
final List<SchemaData> schemaDatas = extendData.getSchemaData();
for (SchemaData schemaData : schemaDatas) {
//get simples data to add it on values map
final List<SimpleData> simpleDataList = schemaData.getSimpleDatas();
for (SimpleData simpleData : simpleDataList) {
values.put(simpleData.getName(), simpleData.getContent());
}
}
}
//add geometry on list
geometries.add(new AbstractMap.SimpleEntry<Object, Map<String, String>>(geometry, values));
// it's a folder, go recursivly on childs
} else {
final Iterator<?> iterator = ((Iterable<?>) feature.getPropertyValue(KmlConstants.TAG_FEATURES)).iterator();
while (iterator.hasNext()) {
final Object object = iterator.next();
if (object instanceof Feature) {
final Feature candidat = (Feature) object;
//recursive call
fillGeometryListFromFeature(candidat, geometries);
}
}
}
}
/**
* Cut a lineString if the anti-meridien intersect the line and return a multiLineString
* column type of geometry from lineString to MultiLineString
*
* @param geom {@link LineString} tested
* @return a {@link MultiLineString} if {@link LineString} cut meridian else <code>null</code>
* @throws org.opengis.referencing.operation.TransformException
*/
public static MultiLineString cutAtMeridian(LineString geom) throws TransformException {
final GeometryFactory gf = geom.getFactory();
final Geometry clip = gf.createPolygon(
gf.createLinearRing(
new Coordinate[]{
new Coordinate(-180, 90),
new Coordinate(180, 90),
new Coordinate(180, -90),
new Coordinate(-180, -90),
new Coordinate(-180, 90)}),
new LinearRing[0]);
final Geometry rightMeridian = gf.createPolygon(
gf.createLinearRing(
new Coordinate[]{
new Coordinate(180, 90),
new Coordinate(360, 90),
new Coordinate(360, -90),
new Coordinate(180, -90),
new Coordinate(180, 90)}),
new LinearRing[0]);
final Geometry leftMeridian = gf.createPolygon(
gf.createLinearRing(
new Coordinate[]{
new Coordinate(-180, 90),
new Coordinate(-360, 90),
new Coordinate(-360, -90),
new Coordinate(-180, -90),
new Coordinate(-180, 90)}),
new LinearRing[0]);
Geometry cutright = rightMeridian.intersection(geom);
Geometry cutleft = leftMeridian.intersection(geom);
Geometry clipped = clip.intersection(geom);
final List<LineString> strs = new ArrayList<>();
if (cutright instanceof LineString) {
final AffineTransform2D trs = new AffineTransform2D(1, 0, 0, 1, -180, 0);
cutright = JTS.transform(cutright, trs);
strs.add((LineString) cutright);
}
if (cutleft instanceof LineString) {
final AffineTransform2D trs = new AffineTransform2D(1, 0, 0, 1, +180, 0);
cutleft = JTS.transform(cutleft, trs);
strs.add((LineString) cutleft);
}
if (clipped instanceof LineString) {
strs.add((LineString) clipped);
}
//if strs.size<0, geometry don't need to change his value
if(strs.size()>0){
MultiLineString ml = gf.createMultiLineString(strs.toArray(new LineString[strs.size()]));
//second pass to cut points which already cross the meridien properly
strs.clear();
for (int i = 0; i < ml.getNumGeometries(); i++) {
final LineString ls = (LineString) ml.getGeometryN(i);
final Coordinate[] coords = ls.getCoordinates();
int from = 0;
int end = 1;
while (end < coords.length) {
if (Math.abs(coords[end - 1].x - coords[end].x) > 160) {
final Coordinate[] cut = Arrays.copyOfRange(coords, from, end);
if (cut.length > 1) {
final LineString cls = gf.createLineString(cut);
strs.add(cls);
from = end;
}
}
end++;
}
if ((end - from) > 1) {
final Coordinate[] cut = Arrays.copyOfRange(coords, from, end);
final LineString cls = gf.createLineString(cut);
strs.add(cls);
}
}
ml = gf.createMultiLineString(strs.toArray(new LineString[strs.size()]));
return ml;
}
return null;
}
/**
* create a {@link Geometry from a Feature.}
*
* @param noKmlFeature feature from an other type
* @param defaultIdStyle style defined on document.
* @return a valid kml {@link Feature}
*/
public static Feature buildKMLFeature(Feature noKmlFeature, IdAttributes defaultIdStyle){
//Transform geometry
final KmlFactory kmlFactory = DefaultKmlFactory.getInstance();
final Feature placemark = kmlFactory.createPlacemark();
final String geoColumn = AttributeConvention.GEOMETRY_PROPERTY.tip().toString();
final AbstractGeometry ag = buildKMLGeometry((Geometry) noKmlFeature.getPropertyValue(geoColumn));
placemark.setPropertyValue(KmlConstants.TAG_GEOMETRY, ag);
try {
placemark.setPropertyValue(KmlConstants.TAG_STYLE_URL, new URI("#" + defaultIdStyle.getId()));
} catch (URISyntaxException e) {
LOGGER.log(Level.WARNING, "unnable to define style URI", e);
}
//TODO : transform datas
final List<Data> simpleDatas = new ArrayList<>(0);
for (final PropertyType type : noKmlFeature.getType().getProperties(true)) {
final Property property = noKmlFeature.getProperty(type.getName().toString());
String localPartName = property.getName().tip().toString();
final Object value = property.getValue();
if (localPartName.equalsIgnoreCase(KmlConstants.TAG_NAME)) {
placemark.setPropertyValue(KmlConstants.TAG_NAME, value);
} else if (!(localPartName.equalsIgnoreCase(geoColumn) || localPartName.equalsIgnoreCase("fid"))) {
if (value != null) {
Data simpleData = kmlFactory.createData();
simpleData.setName(localPartName);
simpleData.setValue(value.toString());
simpleDatas.add(simpleData);
}
}
}
if (!simpleDatas.isEmpty()) {
ExtendedData extendedData = kmlFactory.createExtendedData();
extendedData.setDatas(simpleDatas);
placemark.setPropertyValue(KmlConstants.TAG_EXTENDED_DATA, extendedData);
}
return placemark;
}
private static AbstractGeometry buildKMLGeometry(Geometry geometry) {
Class<?> geometryClass = geometry.getClass();
final KmlFactory kmlFactory = DefaultKmlFactory.getInstance();
if (geometryClass.equals(LineString.class)) {
LineString ls = (LineString) geometry;
org.geotoolkit.data.kml.model.LineString lineString = kmlFactory.createLineString(ls.getCoordinateSequence());
return lineString;
} else if (geometryClass.equals(Point.class)) {
Point point = (Point) geometry;
org.geotoolkit.data.kml.model.Point kmlPoint = kmlFactory.createPoint(point.getCoordinateSequence());
return kmlPoint;
} else if (geometryClass.equals(Polygon.class)) {
final Polygon poly = (Polygon) geometry;
// interiorRing
final List<Boundary> innerBoundaries = new ArrayList<>(0);
int innerRing = poly.getNumInteriorRing();
if (innerRing>0) {
for (int i = 0; i < innerRing; i++) {
final LineString inner = poly.getInteriorRingN(i);
final org.geotoolkit.data.kml.model.LinearRing innerLineRing = kmlFactory.createLinearRing(inner.getCoordinateSequence());
final Boundary innerBoundary = kmlFactory.createBoundary(innerLineRing, null, null);
innerBoundaries.add(innerBoundary);
}
}
//exterior ring
final org.geotoolkit.data.kml.model.LinearRing lr = kmlFactory.createLinearRing(poly.getExteriorRing().getCoordinateSequence());
final Boundary boundary = kmlFactory.createBoundary(lr, null, null);
final org.geotoolkit.data.kml.model.Polygon kmlPolygon = kmlFactory.createPolygon(boundary, innerBoundaries);
return kmlPolygon;
} else if (GeometryCollection.class.isAssignableFrom(geometryClass)) {
final GeometryCollection geoCollec = (GeometryCollection)geometry;
final MultiGeometry mg = kmlFactory.createMultiGeometry();
final List<AbstractGeometry> geometries = new ArrayList<>(0);
for (int i = 0; i < geoCollec.getNumGeometries(); i++) {
Geometry currentGeometry = geoCollec.getGeometryN(i);
AbstractGeometry ag = buildKMLGeometry(currentGeometry);
geometries.add(ag);
}
mg.setGeometries(geometries);
return mg;
}
return null;
}
}