package gis;

/**
 * @author Jeroen van Dijk
 * @date Mar 12, 2007
 * @package test
 * @filename GISFeatures.java
 *  
 */

import com.vividsolutions.jts.geom.Coordinate;
import com.vividsolutions.jts.geom.Envelope;
import com.vividsolutions.jts.geom.Geometry;
import com.vividsolutions.jts.geom.GeometryFactory;
import com.vividsolutions.jts.geom.LineString;
import com.vividsolutions.jts.geom.MultiPoint;
import com.vividsolutions.jts.geom.Point;
import com.vividsolutions.jts.operation.distance.DistanceOp;
import java.io.File;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.NoSuchElementException;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.geotools.data.FeatureSource;
import org.geotools.data.FeatureWriter;
import org.geotools.data.Transaction;
import org.geotools.data.shapefile.ShapefileDataStore;
import org.geotools.factory.FactoryConfigurationError;
import org.geotools.feature.AttributeType;
import org.geotools.feature.AttributeTypeFactory;
import org.geotools.feature.Feature;
import org.geotools.feature.FeatureCollection;
import org.geotools.feature.FeatureType;
import org.geotools.feature.FeatureTypeBuilder;
import org.geotools.feature.FeatureTypeFactory;
import org.geotools.feature.IllegalAttributeException;
import org.geotools.feature.SchemaException;

/**
 * The Class GISFeatures.
 * 
 * @author jeroen
 */
public class GISFeatures {

	/** The log. */
	private static Log log = LogFactory.getLog(GISFeatures.class.getName());

	/**
	 * The Enum ConvertMode.
	 */
	public enum ConvertMode {

		TO_POINT, TO_MULTIPOINT
	};

	/**
	 * The main method.
	 * 
	 * @param args
	 *            the args
	 * 
	 * @throws IOException
	 *             the IO exception
	 * @throws IllegalAttributeException
	 *             the illegal attribute exception
	 * @throws SchemaException
	 *             the schema exception
	 * @throws FactoryConfigurationError
	 *             the factory configuration error
	 * @throws ArrayIndexOutOfBoundsException
	 *             the array index out of bounds exception
	 */
	public static void main(String[] args) throws IOException, IllegalAttributeException,
			ArrayIndexOutOfBoundsException, FactoryConfigurationError, SchemaException {

		GISFeatures gisFeatures = new GISFeatures(
				new File(
						"F:/Users/Jeroen/My Documents/Research Documents/Data/Kaarten Nederland/MWB/SHP/PcFind single point/PcFind single point.shp")
						.toURI().toURL());

		FeatureType schema = gisFeatures.getSchema();

		// FeatureType newSchema =
		// gisFeatures.changeSchema(gisFeatures.getSchema(),
		// ConvertMode.TO_MULTIPOINT);
		//
		// System.out.println("equals : " + schema.equals(newSchema));
		//		

		System.out.println(schema);

		Envelope envelope = new Envelope(new Coordinate(4.966f, 51.607f), new Coordinate(5.139f,
				51.507f));
		GeometryFactory geomFactory = new GeometryFactory();
		Geometry geometryArea = geomFactory.toGeometry(envelope);
		gisFeatures
				.write(
						geometryArea,
						0.0,
						new File(
								"F:/Users/Jeroen/My Documents/Research Documents/Data/Kaarten Nederland/MWB/SHP/Zipcodes_from_Tilburg.shp"));
	}

	/** The datastore of the shapefile. */
	private ShapefileDataStore ds;

	/** The feature source. */
	private FeatureSource fs;

	/** The feature collection. */
	private FeatureCollection fc;

	/** The shapefile URL. */
	private URL shapeURL;

	/** The all features. */
	private ArrayList<Feature> allFeatures;

	/** The schema. */
	private FeatureType schema;

	/**
	 * The Constructor.
	 * 
	 * @param shapeURL
	 *            the shapefile URL
	 */
	public GISFeatures(URL shapeURL) {
		this.shapeURL = shapeURL;
	}

	/**
	 * Processes the loaded shapefile.
	 */
	public void process() {
		// Load shapefile
		try {
			ds = new ShapefileDataStore(shapeURL);

			// below not compatible with the newest version
			fs = ds.getFeatureSource();
			fc = fs.getFeatures();
			schema = fc.getSchema();

		} catch (MalformedURLException e) {
			log.error("Wrong URL format of shapefile URL", e);
		} catch (IOException e) {
			log.error("I/O error while processing shapefile", e);
		}
	}

	/**
	 * Writes the features within a given distance of a geometry to a shapefile.
	 * 
	 * @param distance
	 *            the distance
	 * @param outputSHP
	 *            the output SHP
	 * @param comparison
	 *            the comparison
	 * 
	 * @throws IllegalAttributeException
	 *             the illegal attribute exception
	 * @throws IOException
	 *             the IO exception
	 */
	public void write(Geometry comparison, double distance, File outputSHP) throws IOException,
			IllegalAttributeException {
		Collection<Feature> features = getFeatures(comparison, distance);
		write(features, outputSHP);
	}

	/**
	 * Write the feature of the loaded shapefile.
	 * 
	 * @param outputSHP
	 *            the output SHP
	 * 
	 * @throws IOException
	 *             the IO exception
	 * @throws IllegalAttributeException
	 *             the illegal attribute exception
	 */
	public void write(File outputSHP) throws IOException, IllegalAttributeException {
		write(getFeatures(), outputSHP);
	}

	/**
	 * Write converted features.
	 * 
	 * @param outputSHP
	 *            the output SHP
	 * @param mode
	 *            the mode
	 * 
	 * @throws IOException
	 *             the IO exception
	 * @throws IllegalAttributeException
	 *             the illegal attribute exception
	 * @throws SchemaException
	 *             the schema exception
	 * @throws FactoryConfigurationError
	 *             the factory configuration error
	 * @throws ArrayIndexOutOfBoundsException
	 *             the array index out of bounds exception
	 */
	public void writeConvertedFeatures(ConvertMode mode, File outputSHP) throws IOException,
			IllegalAttributeException, ArrayIndexOutOfBoundsException, FactoryConfigurationError,
			SchemaException {
		write(getConvertedFeatures(mode), this.changeSchema(getSchema(), mode), null, outputSHP);
	}

	/**
	 * Write converted features.
	 * 
	 * @param geometryArea
	 *            the geometry area
	 * @param distance
	 *            the distance
	 * @param file
	 *            the file
	 * @param mode
	 *            the mode
	 * 
	 * @throws IllegalAttributeException
	 *             the illegal attribute exception
	 * @throws IOException
	 *             the IO exception
	 * @throws SchemaException
	 *             the schema exception
	 * @throws FactoryConfigurationError
	 *             the factory configuration error
	 */
	private void writeConvertedFeatures(Geometry geometryArea, double distance, ConvertMode mode,
			File file) throws IOException, IllegalAttributeException, FactoryConfigurationError,
			SchemaException {
		FeatureType newSchema = this.changeSchema(getSchema(), mode);
		write(getConvertedFeatures(newSchema, getFeatures(geometryArea, distance), mode),
				newSchema, null, file);
	}

	/**
	 * Write.
	 * 
	 * @param schema
	 *            the schema
	 * @param addedAttributes
	 *            the added attributes
	 * @param outputSHP
	 *            the output SHP
	 * 
	 * @throws IllegalAttributeException
	 *             the illegal attribute exception
	 * @throws IOException
	 *             the IO exception
	 * @throws ArrayIndexOutOfBoundsException
	 *             the array index out of bounds exception
	 */
	public void write(FeatureType schema, List<List<Object>> addedAttributes, File outputSHP)
			throws IOException, ArrayIndexOutOfBoundsException, IllegalAttributeException {
		write(getFeatures(), schema, addedAttributes, outputSHP);
	}

	/**
	 * Writes a feature collection to a shapefile.
	 * 
	 * @param features
	 *            the features
	 * @param outputSHP
	 *            the output SHP
	 * 
	 * @throws IllegalAttributeException
	 *             the illegal attribute exception
	 * @throws IOException
	 *             the IO exception
	 */
	public void write(Collection<Feature> features, File outputSHP) throws IOException,
			IllegalAttributeException {
		write(features, this.schema, null, outputSHP);
	}

	/**
	 * Write.
	 * 
	 * @param schema
	 *            the schema
	 * @param addedAttributes
	 *            the added attributes
	 * @param features
	 *            the features
	 * @param outputSHP
	 *            the output SHP
	 * 
	 * @throws IllegalAttributeException
	 *             the illegal attribute exception
	 * @throws IOException
	 *             the IO exception
	 * @throws ArrayIndexOutOfBoundsException
	 *             the array index out of bounds exception
	 */
	public void write(Collection<Feature> features, FeatureType schema,
			List<List<Object>> addedAttributes, File outputSHP) throws IOException,
			ArrayIndexOutOfBoundsException, IllegalAttributeException {
		// Create the output shapefile
		ShapefileDataStore outStore = new ShapefileDataStore(outputSHP.toURI().toURL());

		outStore.createSchema(schema);
		if (log.isDebugEnabled()) {
			log.debug("Number of features : " + features.size());
			log.debug("schema " + schema);
		}

		FeatureWriter outFeatureWriter = outStore.getFeatureWriter(outStore.getTypeNames()[0],
				Transaction.AUTO_COMMIT);

		Object[] att = null;

		boolean addFeatures = (addedAttributes != null && addedAttributes.size() == features.size());
		int numberOfFeatures = schema.getAttributeCount();
		Object[] attributes = null;

		if (log.isDebugEnabled())
			log.debug("Number of features " + numberOfFeatures);

		// initialize attributes so that it will pass the compiler
		Iterator<List<Object>> attributeIter = null;
		if (addFeatures)
			attributeIter = addedAttributes.iterator();

		for (Feature feature : features) {
			// Get the next, empty feature from the writer
			Feature writeFeature = outFeatureWriter.next();

			if (log.isDebugEnabled()) {
				log.debug("Feature : " + feature.toString());
				Geometry geometry = feature.getDefaultGeometry();
				log.debug("Geometry " + geometry.toString());
				log.debug("Coordinates " + geometry.getCoordinates());
			}

			att = feature.getAttributes(att);

			if (addFeatures) {
				attributes = new Object[writeFeature.getNumberOfAttributes()];
				System.arraycopy(att, 0, attributes, 0, att.length);

				int i = feature.getNumberOfAttributes();
				for (Object obj : attributeIter.next())
					attributes[i++] = obj;

				att = attributes;
			}

			// Set the attributes of the new feature by copying the old,
			// adjusted feature,
			// which includes the geometry (which is an attribute)
			for (int n = 0; n < att.length; n++) {
				if (att[n] == null) {
					System.out.println("an attribute was empty, on index " + n);
				} else
					writeFeature.setAttribute(n, att[n]);
			}

			outFeatureWriter.write();
		}
		// line below is essential, incomplete files are written without it!
		outFeatureWriter.close();

		if (log.isDebugEnabled())
			log.debug("Number of features written : " + features.size());

	}

	/**
	 * Gets the all features of a shapefile.
	 * 
	 * @param shapeURL
	 *            the shapefile URL
	 * 
	 * @return the features
	 * 
	 * @throws IllegalAttributeException
	 *             the illegal attribute exception
	 * @throws IOException
	 *             the IO exception
	 */
	public static Collection<Feature> getAllFeatures(URL shapeURL)
			throws IllegalAttributeException, IOException {

		// Process shapefile
		ShapefileDataStore ds = new ShapefileDataStore(shapeURL);
		FeatureSource fs = ds.getFeatureSource();
		FeatureCollection fc = fs.getFeatures();

		return getFeatures(fc);
	}

	/**
	 * Gets the features of the loaded shapefile.
	 * 
	 * @return the features
	 */
	public ArrayList<Feature> getFeatures() {
		if (allFeatures == null)
			allFeatures = getFeatures(this.fc);

		return allFeatures;
	}

	/**
	 * Gets the converted features from all the features.
	 * 
	 * @param mode
	 *            the mode
	 * 
	 * @return the converted features
	 * 
	 * @throws SchemaException
	 *             the schema exception
	 * @throws FactoryConfigurationError
	 *             the factory configuration error
	 */
	public ArrayList<Feature> getConvertedFeatures(ConvertMode mode)
			throws FactoryConfigurationError, SchemaException {

		return getConvertedFeatures(this.changeSchema(getSchema(), mode), getFeatures(), mode);
	}

	/**
	 * Gets the converted features.
	 * 
	 * @param schema
	 *            the schema
	 * @param features
	 *            the features
	 * @param mode
	 *            the mode
	 * 
	 * @return the converted features
	 */
	public ArrayList<Feature> getConvertedFeatures(FeatureType schema, ArrayList<Feature> features,
			ConvertMode mode) {
		ArrayList<Feature> newFeatures = new ArrayList<Feature>(features.size());

		if (mode == ConvertMode.TO_MULTIPOINT) {
			try {
				Object[] att = null;
				for (Feature feature : features) {
					Geometry geom = feature.getDefaultGeometry();

					feature.setDefaultGeometry(geom);
					att = feature.getAttributes(att);

					// newFeatures.add(schema.create(att));

					newFeatures.add(feature);
				}
			} catch (IllegalAttributeException e) {
				log.error("Could not convert " + schema.getAttributeType(schema.find("the_geom"))
						+ " to Point", e);
			}
		}

		if (mode == ConvertMode.TO_POINT) {
			try {
				Object[] att = null;
				for (Feature feature : features) {
					Geometry geom = feature.getDefaultGeometry().getInteriorPoint().getGeometryN(0);

					feature.setDefaultGeometry(geom);
					att = feature.getAttributes(att);

					// newFeatures.add(schema.create(att));

					newFeatures.add(feature);
				}
			} catch (IllegalAttributeException e) {
				log.error("Could not convert " + schema.getAttributeType(schema.find("the_geom"))
						+ " to Point", e);
			}
		}

		return newFeatures;
	}

	/**
	 * Gets the feature with the given index.
	 * 
	 * @param idx
	 *            the index
	 * 
	 * @return the feature
	 */
	public Feature getFeature(int idx) {
		return getFeatures().get(idx);
	}

	/**
	 * Gets the features of a given feature collection.
	 * 
	 * @param fc
	 *            the fc
	 * 
	 * @return the features
	 */
	private static ArrayList<Feature> getFeatures(FeatureCollection fc) {
		Iterator iterator = fc.iterator();

		ArrayList<Feature> features = new ArrayList<Feature>();

		while (iterator.hasNext()) {
			features.add((Feature) iterator.next());
		}

		return features;
	}

	// REMOVE UNUSED CODE BELOW
	// private static ArrayList<Feature> getFeatures(FeatureReader fr) {
	// ArrayList<Feature> features = new ArrayList<Feature>();
	//
	// try {
	// while (fr.hasNext()) {
	// features.add(fr.next());
	// }
	//
	// } catch (NoSuchElementException e) {
	// log.error("Error while reading features", e);
	// } catch (IOException e) {
	// log.error("Error while reading features", e);
	// } catch (IllegalAttributeException e) {
	// log.error("Error while reading features", e);
	// }
	//
	// return features;
	// }

	/**
	 * Gets the features of the loaded shapefile within the given distance of
	 * the given tile Geometry .
	 * 
	 * @param distance
	 *            the distance
	 * @param tileGeometry
	 *            the tile geometry
	 * 
	 * @return the features
	 * 
	 * @throws IllegalAttributeException
	 *             the illegal attribute exception
	 */
	public ArrayList<Feature> getFeatures(Geometry tileGeometry, double distance)
			throws IllegalAttributeException {

		ArrayList<Feature> features = new ArrayList<Feature>();
		// Process shapefile
		Iterator iterator = fc.iterator();

		while (iterator.hasNext()) {
			// Extract feature and geometry
			Feature feature = (Feature) iterator.next();
			Geometry featureGeometry = feature.getDefaultGeometry();

			// add feature if it is in the requested area
			if (featureGeometry.isWithinDistance(tileGeometry, distance)) {
				features.add(feature);
			}
		}

		return features;
	}

	/**
	 * Gets the features of the loaded shapefile within the given distance of
	 * the given tile Geometry .
	 * 
	 * @param distance
	 *            the distance
	 * @param tileGeometry
	 *            the tile geometry
	 * 
	 * @return the features
	 * 
	 * @throws IllegalAttributeException
	 *             the illegal attribute exception
	 */
	public ArrayList<Integer> getFeaturesIdx(Geometry tileGeometry, double distance) {

		ArrayList<Integer> featureIdxs = new ArrayList<Integer>();
		// Process shapefile
		Iterator iterator = fc.iterator();
		int idx = 0;
		while (iterator.hasNext()) {
			// Extract feature and geometry
			Feature feature = (Feature) iterator.next();
			Geometry featureGeometry = feature.getDefaultGeometry();

			// add feature if it is in the requested area
			if (featureGeometry.isWithinDistance(tileGeometry, distance)) {
				// REMOVE distance request below
				if (log.isDebugEnabled()) {
					double jtsDistance = DistanceOp.distance(tileGeometry, featureGeometry);
					log.debug("Accepted distance according JTS" + jtsDistance
							+ ", while given distance was " + distance + " in decimal degrees");

				}

				featureIdxs.add(idx);
			}
			idx++;
		}

		return featureIdxs;
	}

	/**
	 * Gets the schema.
	 * 
	 * @return the schema
	 * 
	 * @uml.property name="schema"
	 */
	public FeatureType getSchema() {
		return schema;
	}

	/**
	 * Change schema.
	 * 
	 * @param schema
	 *            the schema
	 * @param mode
	 *            the mode
	 * 
	 * @return the feature type
	 * 
	 * @throws SchemaException
	 *             the schema exception
	 * @throws FactoryConfigurationError
	 *             the factory configuration error
	 */
	public FeatureType changeSchema(FeatureType schema, ConvertMode mode)
			throws FactoryConfigurationError, SchemaException {
		AttributeType geom = null;
		if (mode == ConvertMode.TO_POINT) {
			geom = AttributeTypeFactory.newAttributeType("the_geom", Point.class);
		} else if (mode == ConvertMode.TO_MULTIPOINT) {
			geom = AttributeTypeFactory.newAttributeType("the_geom", MultiPoint.class);
		} else {
			return schema;
		}

		AttributeType[] attributeTypes = schema.getAttributeTypes();
		attributeTypes[schema.find("the_geom")] = geom;
		FeatureType newSchema = FeatureTypeBuilder.newFeatureType(attributeTypes, schema
				.getTypeName(), schema.getNamespace(), schema.isAbstract(), schema.getAncestors(),
				schema.getDefaultGeometry());

		return newSchema;
	}

}
