package org.activityinfo.geoadmin; import com.google.common.base.Charsets; import com.google.common.collect.Lists; import com.google.common.io.Files; import com.vividsolutions.jts.geom.Envelope; import com.vividsolutions.jts.geom.Geometry; import org.activityinfo.geoadmin.model.Country; import org.geotools.data.FeatureSource; import org.geotools.data.shapefile.ShapefileDataStore; import org.geotools.data.simple.SimpleFeatureCollection; import org.geotools.data.simple.SimpleFeatureIterator; import org.geotools.data.simple.SimpleFeatureSource; import org.geotools.geometry.jts.JTS; import org.geotools.referencing.CRS; import org.geotools.referencing.crs.DefaultGeographicCRS; import org.opengis.feature.simple.SimpleFeature; import org.opengis.feature.type.GeometryDescriptor; import org.opengis.feature.type.GeometryType; import org.opengis.feature.type.PropertyDescriptor; import org.opengis.referencing.crs.CoordinateReferenceSystem; import org.opengis.referencing.operation.MathTransform; import java.io.File; import java.io.IOException; import java.math.BigInteger; import java.nio.channels.FileChannel.MapMode; import java.security.MessageDigest; import java.util.List; import java.util.logging.Logger; /** * A set of features from a file to be imported into ActivityInfo's * administrative reference database. * */ public class ImportSource { private static final Logger LOGGER = Logger.getLogger(ImportSource.class.getName()); private static final int IN_MEMORY_LIMIT_BYTES = 1024 * 1024 * 5; private List<PropertyDescriptor> attributes; private SimpleFeatureSource featureSource; private MathTransform transform; private File file; private boolean mbrOnly; private List<ImportFeature> features = Lists.newArrayList(); private String hash; public ImportSource(File shapefile) throws Exception { this.file = shapefile; this.mbrOnly = this.file.length() > IN_MEMORY_LIMIT_BYTES; ShapefileDataStore ds = new ShapefileDataStore(shapefile.toURI().toURL()); featureSource = ds.getFeatureSource(); transform = createTransform(); loadFeatures(); calculateHash(); } public boolean isMbrOnly() { return mbrOnly; } /** * Loads the feature's attributes and the envelope for the geometry into * memory. */ private void loadFeatures() throws IOException { attributes = getNonGeometryAttributes(); SimpleFeatureCollection features = featureSource.getFeatures(); SimpleFeatureIterator it = features.features(); while (it.hasNext()) { SimpleFeature feature = (SimpleFeature) it.next(); if(hasGeometry(feature)) { ImportFeature importFeature = new ImportFeature( attributes, toAttributeArray(feature), calcWgs84Geometry(feature)); this.features.add(importFeature); } else { System.err.println("No geometry: " + attributes); } } } public boolean hasGeometry(SimpleFeature feature) { Geometry geometry = (Geometry) feature.getDefaultGeometryProperty().getValue(); return geometry != null; } /** * Calculates the geographic envelope of the feature in the WGS 84 * Geographic Reference system. */ private Geometry calcWgs84Geometry(SimpleFeature feature) { try { Geometry geometry = (Geometry) feature.getDefaultGeometryProperty().getValue(); Geometry geometryInWgs84 = JTS.transform(geometry, transform); if(mbrOnly) { geometryInWgs84 = geometryInWgs84.getEnvelope(); } return geometryInWgs84; } catch (Exception e) { throw new RuntimeException(e); } } /** * Reads the attribute vaules in an array. * * @param feature * @return */ private Object[] toAttributeArray(SimpleFeature feature) { Object[] attribs = new Object[attributes.size()]; for (int i = 0; i != attribs.length; ++i) { attribs[i] = feature.getAttribute(attributes.get(i).getName()); } return attribs; } /** * Finds all the non-geographic attributes in the source. */ private List<PropertyDescriptor> getNonGeometryAttributes() { attributes = Lists.newArrayList(); for (PropertyDescriptor descriptor : featureSource.getSchema().getDescriptors()) { if (!(descriptor.getType() instanceof GeometryType)) { attributes.add(descriptor); } } return attributes; } public List<PropertyDescriptor> getAttributes() { return attributes; } public int getFeatureCount() { return features.size(); } private MathTransform createTransform() throws Exception { GeometryDescriptor geometryType = featureSource.getSchema().getGeometryDescriptor(); CoordinateReferenceSystem sourceCrs = geometryType.getCoordinateReferenceSystem(); if(sourceCrs == null) { // if it's not WGS84, we'll soon find out as we check the geometry against the // country bounds sourceCrs = DefaultGeographicCRS.WGS84; } CoordinateReferenceSystem geoCRS = DefaultGeographicCRS.WGS84; boolean lenient = true; // allow for some error due to different datums return CRS.findMathTransform(sourceCrs, geoCRS, lenient); } public int getAttributeCount() { return attributes.size(); } public String[] getAttributeNames() { String[] names = new String[attributes.size()]; for (int i = 0; i != names.length; ++i) { names[i] = attributes.get(i).getName().getLocalPart(); } return names; } public FeatureSource getFeatureSource() { return featureSource; } /** * Checks to see whether all geometry at least intersects the country's * geographic bounds. This is a good check to ensure that we have correctly * understood the source's CRS. * * @param country * @return */ public boolean validateGeometry(Country country) { Envelope countryEnvelope = countryBounds(country); for (ImportFeature feature : features) { if (!countryEnvelope.intersects(feature.getEnvelope())) { System.out.println(feature.toString() + " has envelope " + feature.getEnvelope()); return false; } } return true; } private Envelope countryBounds(Country country) { if (country.getBounds() == null) { return new Envelope(-180, 180, -90, 90); } else { return GeoUtils.toEnvelope(country.getBounds()); } } public File getFile() { return file; } public String getMetadata() throws IOException { File metadataFile = getFile(".shp.xml"); if (metadataFile.exists()) { return Files.toString(metadataFile, Charsets.UTF_8); } else { return null; } } public List<ImportFeature> getFeatures() { return features; } public String getMd5Hash() { return hash; } private void calculateHash() { try { MessageDigest digest = MessageDigest.getInstance("MD5"); updateHash(digest, ".shp"); updateHash(digest, ".shx"); updateHash(digest, ".shp.xml"); updateHash(digest, ".dbf"); updateHash(digest, ".sbn"); updateHash(digest, ".prj"); this.hash = new BigInteger(1, digest.digest()).toString(16); } catch (Exception e) { throw new RuntimeException("Exception generating hash"); } } private void updateHash(MessageDigest digest, String extension) throws IOException { File file = getFile(extension); if (file.exists()) { digest.update(Files.map(file, MapMode.READ_ONLY)); } } private File getFile(String extension) throws AssertionError { String absPath = file.getAbsolutePath(); if (!absPath.endsWith(".shp")) { throw new AssertionError(); } File file = new File(absPath.substring(0, absPath.length() - 4) + extension); return file; } }