package org.wikibrain.spatial;
import org.apache.commons.io.FileUtils;
import org.geotools.data.DataStore;
import org.geotools.data.DataStoreFinder;
import org.geotools.data.shapefile.ShapefileDataStoreFactory;
import org.geotools.data.simple.SimpleFeatureCollection;
import org.geotools.data.simple.SimpleFeatureIterator;
import org.geotools.data.simple.SimpleFeatureSource;
import org.h2.util.StringUtils;
import org.opengis.feature.simple.SimpleFeatureType;
import org.supercsv.io.CsvMapReader;
import org.supercsv.prefs.CsvPreference;
import org.wikibrain.utils.WpIOUtils;
import java.io.File;
import java.io.IOException;
import java.net.MalformedURLException;
import java.util.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* A utility wrapper around GeoTool's shape file class.
* Also contains many static helper methods.
*
* Several of the methods contain variants that take a layer and ones that do not.
* The ones that do not take a layer default to the "first" layer in the shapefile.
*
* @author Shilad Sen
*/
public class WikiBrainShapeFile {
private static final Logger LOG = LoggerFactory.getLogger(WikiBrainShapeFile.class);
public static final String [] EXTENSIONS = new String[] { ".shp", ".shx", ".dbf" };
public static final String WB_MAP_EXTENSINO = ".wbmapping.csv";
// Ends with ".shp" extension
private final File file;
private final String encoding;
private DataStore dataStore;
/**
* Creates a new shapefile wrapper associated with the given file.
* @param file Must end with ".shp"
*/
public WikiBrainShapeFile(File file) {
this(file, "UTF-8");
}
/**
* Creates a new shapefile wrapper associated with the given file.
* @param file Must end with ".shp"
*/
public WikiBrainShapeFile(File file, String encoding) {
ensureHasShpExtension(file);
this.file = file;
this.encoding = encoding;
}
public synchronized void initDataStoreIfNecessary() throws IOException {
if (this.dataStore == null) {
this.dataStore = fileToDataStore(file, encoding);
}
}
static class KeyAndScore implements Comparable<KeyAndScore> {
String key;
Double score;
KeyAndScore(String key, Double score) {
this.key = key;
this.score = score;
}
@Override
public int compareTo(KeyAndScore o) {
int r = -1 * score.compareTo(o.score);
if (r == 0) {
r = key.compareTo(o.key);
}
return r;
}
@Override
public String toString() {
return "{" + "key='" + key + '\'' + ", score=" + score + '}';
}
}
/**
* Reads in a mapping from shapefile key to title for all entries with status != U
* @return
*/
public Map<String, String> readMapping() throws IOException {
HashMap<String, String> mapping = new HashMap<String, String>();
if (!hasMappingFile()) {
throw new IOException("No mapping file found: " + getMappingFile());
}
CsvMapReader reader = new CsvMapReader(
WpIOUtils.openBufferedReader(getMappingFile()),
CsvPreference.STANDARD_PREFERENCE
);
// Read in the data and scores
Map<String, List<KeyAndScore>> scores = new HashMap<String, List<KeyAndScore>>();
String [] header = reader.getHeader(true);
while (true) {
Map<String, String> row = reader.read(header);
if (row == null) {
break;
}
if (!row.get("WB_STATUS").equalsIgnoreCase("U")) {
String key = row.get("WB_KEY");
String title = row.get("WB_TITLE");
double score = Double.valueOf(row.get("WB_SCORE"));
if (StringUtils.isNullOrEmpty(title)) {
continue;
}
if (!scores.containsKey(title)) {
scores.put(title, new ArrayList<KeyAndScore>());
}
scores.get(title).add(new KeyAndScore(key, score));
}
}
for (String title : scores.keySet()) {
List<KeyAndScore> titleScores = scores.get(title);
Collections.sort(titleScores);
mapping.put(titleScores.get(0).key, title);
if (titleScores.size() > 1) {
LOG.warn("duplicate keys for title " + title + ": " + titleScores);
}
}
return mapping;
}
/**
* Returns all feature names (i.e. column ids) for the specified layer.
* @param layer
* @return
* @throws IOException
*/
public List<String> getFeatureNames(String layer) throws IOException {
initDataStoreIfNecessary();
SimpleFeatureCollection features = getFeatureCollection(layer);
SimpleFeatureType type = features.getSchema();
List<String> fields = new ArrayList<String>();
for (int i = 0; i < type.getAttributeCount(); i++) {
fields.add(type.getDescriptor(i).getLocalName());
}
return fields;
}
/**
* Returns all feature names (i.e. column ids) for the default layer.
* @return
* @throws IOException
*/
public List<String> getFeatureNames() throws IOException {
return getFeatureNames(getDefaultLayer());
}
/**
* Returns an iterator over rows for the default layer.
* @return
* @throws IOException
*/
public SimpleFeatureIterator getFeatureIter() throws IOException {
return getFeatureCollection().features();
}
/**
* Returns an iterator over rows for the specified layer.
* @return
* @throws IOException
*/
public SimpleFeatureIterator getFeatureIter(String layer) throws IOException {
return getFeatureCollection(layer).features();
}
public SimpleFeatureCollection getFeatureCollection(String layer) throws IOException {
initDataStoreIfNecessary();
SimpleFeatureSource featureSource = dataStore.getFeatureSource(layer);
return featureSource.getFeatures();
}
public SimpleFeatureCollection getFeatureCollection() throws IOException {
return getFeatureCollection(getDefaultLayer());
}
/**
* Returns the name of the default layer
* @return
* @throws IOException
*/
public String getDefaultLayer() throws IOException {
initDataStoreIfNecessary();
return dataStore.getTypeNames()[0];
}
public WikiBrainShapeFile move(File dest) throws IOException {
ensureHasShpExtension(dest);
dest.getParentFile().mkdirs();
for (String ext : EXTENSIONS) {
File extDest = getAlternateExtension(dest, ext);
FileUtils.deleteQuietly(extDest);
getAlternateExtension(file, ext).renameTo(extDest);
}
return new WikiBrainShapeFile(dest, encoding);
}
public File getMappingFile() {
return getAlternateExtension(file, WB_MAP_EXTENSINO);
}
public boolean hasMappingFile() {
return getMappingFile().isFile();
}
public File getFile() {
return file;
}
public List<File> getComponentFiles() {
List<File> files = new ArrayList<File>();
for (String ext : EXTENSIONS) {
files.add(getAlternateExtension(file, ext));
}
if (hasMappingFile()) {
files.add(getMappingFile());
}
return files;
}
public boolean hasComponentFiles() {
for (File f : getComponentFiles()) {
if (!f.isFile()) return false;
}
return true;
}
public DataStore getDataStore() {
return dataStore;
}
public static DataStore fileToDataStore(File file) throws IOException {
return fileToDataStore(file, "utf-8");
}
public static DataStore fileToDataStore(File file, String encoding) throws IOException {
Map<String, Object> map = new HashMap<String, Object>();
try {
map.put("url", file.toURI().toURL());
} catch (MalformedURLException e) {
throw new IOException(e);
}
map.put(ShapefileDataStoreFactory.DBFCHARSET.getName(), encoding);
return DataStoreFinder.getDataStore(map);
}
public static boolean exists(File file) {
ensureHasShpExtension(file);
return new WikiBrainShapeFile(file).hasComponentFiles();
}
public static File getAlternateExtension(File file, String ext) {
ensureHasShpExtension(file);
String prefix = file.toString().substring(0, file.toString().length() - ".shp".length());
return new File(prefix + ext);
}
public static boolean hasShpExtension(File file) {
return file.toString().toLowerCase().endsWith(".shp");
}
private static void ensureHasShpExtension(File file) {
if (!hasShpExtension(file)) {
throw new IllegalArgumentException("File " + file + " does not have .shp extension");
}
}
}