/*******************************************************************************
* Copyright (c) 2014 Open Door Logistics (www.opendoorlogistics.com)
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the GNU Lesser Public License 3.0
* which accompanies this distribution, and is available at
* http://www.gnu.org/licenses/lgpl.html
*
******************************************************************************/
package com.opendoorlogistics.components.shapefileexporter;
import java.io.File;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.swing.Icon;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JTextField;
import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener;
import javax.swing.filechooser.FileNameExtensionFilter;
import javax.xml.bind.annotation.XmlAttribute;
import javax.xml.bind.annotation.XmlRootElement;
import org.geotools.data.DefaultTransaction;
import org.geotools.data.Transaction;
import org.geotools.data.collection.ListFeatureCollection;
import org.geotools.data.shapefile.ShapefileDataStore;
import org.geotools.data.shapefile.ShapefileDataStoreFactory;
import org.geotools.data.simple.SimpleFeatureCollection;
import org.geotools.data.simple.SimpleFeatureSource;
import org.geotools.data.simple.SimpleFeatureStore;
import org.geotools.feature.simple.SimpleFeatureBuilder;
import org.geotools.feature.simple.SimpleFeatureTypeBuilder;
import org.geotools.geometry.jts.Geometries;
import org.geotools.referencing.crs.DefaultGeographicCRS;
import org.opengis.feature.simple.SimpleFeature;
import org.opengis.feature.simple.SimpleFeatureType;
import com.opendoorlogistics.api.ODLApi;
import com.opendoorlogistics.api.components.ComponentConfigurationEditorAPI;
import com.opendoorlogistics.api.components.ComponentExecutionApi;
import com.opendoorlogistics.api.components.ODLComponent;
import com.opendoorlogistics.api.geometry.ODLGeom;
import com.opendoorlogistics.api.scripts.ScriptTemplatesBuilder;
import com.opendoorlogistics.api.tables.ODLColumnType;
import com.opendoorlogistics.api.tables.ODLDatastore;
import com.opendoorlogistics.api.tables.ODLDatastoreAlterable;
import com.opendoorlogistics.api.tables.ODLTable;
import com.opendoorlogistics.api.tables.ODLTableAlterable;
import com.opendoorlogistics.api.tables.ODLTableDefinition;
import com.opendoorlogistics.api.tables.ODLTableDefinitionAlterable;
import com.opendoorlogistics.api.tables.ODLTableReadOnly;
import com.opendoorlogistics.api.tables.TableFlags;
import com.opendoorlogistics.api.ui.UIFactory.IntChangedListener;
import com.opendoorlogistics.core.geometry.ODLGeomImpl;
import com.opendoorlogistics.core.geometry.rog.builder.ROGBuilder;
import com.opendoorlogistics.core.tables.ColumnValueProcessor;
import com.opendoorlogistics.core.tables.utils.TableUtils;
import com.opendoorlogistics.core.utils.strings.Strings;
import com.opendoorlogistics.core.utils.ui.FileBrowserPanel;
import com.opendoorlogistics.api.ui.UIFactory.FilenameChangeListener;
import com.opendoorlogistics.core.utils.ui.IntegerEntryPanel;
import com.opendoorlogistics.core.utils.ui.VerticalLayoutPanel;
import com.opendoorlogistics.core.utils.ui.VerticalLayoutPanel.CheckChangedListener;
import com.opendoorlogistics.utils.ui.Icons;
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.Point;
import com.vividsolutions.jts.geom.Polygon;
public class ShapefileExporterComponent implements ODLComponent {
@Override
public String getId() {
return "com.opendoorlogistics.components.shapefileexporter";
}
@Override
public String getName() {
return "Shapefile exporter";
}
@Override
public ODLDatastore<? extends ODLTableDefinition> getIODsDefinition(ODLApi api, Serializable configuration) {
ExportConfig c = (ExportConfig)configuration;
if(c.isConvertShpToROGMode()){
return null;
}
ODLDatastoreAlterable<? extends ODLTableDefinitionAlterable> ds = api.tables().createAlterableDs();
ODLTableDefinitionAlterable table = ds.createTable("Shapefile", -1);
table.setFlags(table.getFlags() | TableFlags.FLAG_COLUMN_WILDCARD);
table.addColumn(-1, "the_geom", ODLColumnType.GEOM, 0);
return ds;
}
@Override
public ODLDatastore<? extends ODLTableDefinition> getOutputDsDefinition(ODLApi api, int mode, Serializable configuration) {
// TODO Auto-generated method stub
return null;
}
private int geomColumn(ODLTableDefinition tableDefinition) {
int ret = TableUtils.findColumnIndx(tableDefinition, "the_geom");
if (ret == -1) {
throw new RuntimeException("Cannot find the_geom field.");
} else if (tableDefinition.getColumnType(ret) != ODLColumnType.GEOM) {
throw new RuntimeException("the_geom field is not of geometry type.");
}
return ret;
}
@Override
public void execute(ComponentExecutionApi api, int mode, Object configuration, ODLDatastore<? extends ODLTable> ioDs, ODLDatastoreAlterable<? extends ODLTableAlterable> outputDs) {
ExportConfig c = (ExportConfig) configuration;
File file = new File(c.getFilename());
if (c.isConvertShpToROGMode()) {
buildODLGRFile(api, c, file);
return;
}
api.postStatusMessage("Exporting shapefile " + c.getFilename());
// get the geometry type
ODLTable table = ioDs.getTableAt(0);
Geometries geomtype = identifyType(table);
if (geomtype == null) {
throw new RuntimeException("Cannot identify a valid geometry type for the shapefile export. All geometries must the same type and one or more must be present.");
}
// built the feature type
SimpleFeatureType type = createFeatureType(table, geomtype);
try {
int geomCol = geomColumn(table);
// build list of features
String[] fieldnames = getNonGeomFieldNames(table);
List<SimpleFeature> features = new ArrayList<SimpleFeature>();
for (int i = 0; i < table.getRowCount(); i++) {
// check geometry is not null as this is not generally supported in shapefiles
ODLGeom geom = (ODLGeom) table.getValueAt(i, geomCol);
if (geom == null) {
continue;
}
Geometry g = ((ODLGeomImpl) geom).getJTSGeometry();
if (g == null) {
continue;
}
// write geom first
SimpleFeatureBuilder featureBuilder = new SimpleFeatureBuilder(type);
featureBuilder.add(assignToType(g, geomtype));
for (int j = 0; j < table.getColumnCount(); j++) {
if (j != geomCol && fieldnames[j] != null) {
Object value = table.getValueAt(i, j);
switch (table.getColumnType(j)) {
case LONG:
if (value != null) {
featureBuilder.add(((Long) value).intValue());
} else {
featureBuilder.add(new Integer(0));
}
break;
case DOUBLE:
if (value != null) {
featureBuilder.add(((Double) value).intValue());
} else {
featureBuilder.add(new Double(0));
}
break;
default:
String s = (String) ColumnValueProcessor.convertToMe(ODLColumnType.STRING, value);
if (s == null) {
s = "";
}
// max length in shapefile is 255
if (s.length() > 255) {
s = s.substring(0, 255);
}
featureBuilder.add(s);
break;
}
}
}
SimpleFeature feature = featureBuilder.buildFeature(null);
features.add(feature);
}
// create shapefile
ShapefileDataStoreFactory dataStoreFactory = new ShapefileDataStoreFactory();
Map<String, Serializable> params = new HashMap<String, Serializable>();
params.put("url", file.toURI().toURL());
params.put("create spatial index", Boolean.FALSE);
ShapefileDataStore newDataStore = (ShapefileDataStore) dataStoreFactory.createNewDataStore(params);
// create type in shapefile
newDataStore.createSchema(type);
Transaction transaction = new DefaultTransaction("create");
// String typeName = newDataStore.getTypeNames()[0];
String typeName = newDataStore.getTypeNames()[0];
SimpleFeatureSource featureSource = newDataStore.getFeatureSource(typeName);
if (featureSource instanceof SimpleFeatureStore) {
SimpleFeatureStore featureStore = (SimpleFeatureStore) featureSource;
SimpleFeatureCollection collection = new ListFeatureCollection(type, features);
featureStore.setTransaction(transaction);
try {
featureStore.addFeatures(collection);
transaction.commit();
} catch (Exception problem) {
problem.printStackTrace();
transaction.rollback();
} finally {
transaction.close();
}
if (c.isBuildROGFile()) {
buildODLGRFile(api, c, file);
}
} else {
throw new RuntimeException(type.getTypeName() + " does not support read/write access");
}
} catch (Exception e) {
throw new RuntimeException(e);
}
}
/**
* @param api
* @param c
* @param file
*/
private void buildODLGRFile(ComponentExecutionApi api, ExportConfig c, File file) {
api.postStatusMessage("Starting build of .odlrg file...");
int nbThreads = c.getNbROGBuilderThreads();
if (nbThreads < 1 || nbThreads > 10) {
throw new RuntimeException("Number of .odlrg builder threads must be between 1 and 10");
}
new ROGBuilder(file, false, 1, nbThreads, api).build();
}
@XmlRootElement
private static class ExportConfig implements Serializable {
private String filename = "";
private boolean buildROGFile;
private boolean convertShpToROGMode;
private int nbROGBuilderThreads = 1;
public String getFilename() {
return filename;
}
@XmlAttribute
public void setFilename(String filename) {
this.filename = filename;
}
public boolean isBuildROGFile() {
return buildROGFile;
}
public void setBuildROGFile(boolean buildROGFile) {
this.buildROGFile = buildROGFile;
}
public int getNbROGBuilderThreads() {
return nbROGBuilderThreads;
}
public void setNbROGBuilderThreads(int nbROGBuilderThreads) {
this.nbROGBuilderThreads = nbROGBuilderThreads;
}
public boolean isConvertShpToROGMode() {
return convertShpToROGMode;
}
public void setConvertShpToROGMode(boolean convertShpToROGMode) {
this.convertShpToROGMode = convertShpToROGMode;
}
}
@Override
public Class<? extends Serializable> getConfigClass() {
return ExportConfig.class;
}
@Override
public JPanel createConfigEditorPanel(ComponentConfigurationEditorAPI api, int mode, Serializable config, boolean isFixedIO) {
final ExportConfig c = (ExportConfig) config;
final JTextField field = new JTextField();
field.setText(c.getFilename());
field.getDocument().addDocumentListener(new DocumentListener() {
@Override
public void removeUpdate(DocumentEvent e) {
save();
}
@Override
public void insertUpdate(DocumentEvent e) {
save();
}
@Override
public void changedUpdate(DocumentEvent e) {
save();
}
private void save() {
c.setFilename(field.getText());
}
});
final VerticalLayoutPanel ret = new VerticalLayoutPanel();
FileBrowserPanel filePanel = new FileBrowserPanel(c.getFilename(), new FilenameChangeListener() {
@Override
public void filenameChanged(String newFilename) {
c.setFilename(newFilename);
}
}, false, "OK", new FileNameExtensionFilter("Shapefile (.shp)", "shp"));
ret.addLine(new JLabel(c.isConvertShpToROGMode() ? "Input shapefile:" : "Output shapefile:"));
ret.addLine(filePanel);
ret.addWhitespace();
final IntegerEntryPanel nbRogThreads = new IntegerEntryPanel("Number .odlrg builder threads", c.getNbROGBuilderThreads(), null, new IntChangedListener() {
@Override
public void intChange(int newInt) {
c.setNbROGBuilderThreads(newInt);
}
});
nbRogThreads.setEnabled(c.isBuildROGFile() || c.isConvertShpToROGMode());
if (!c.isConvertShpToROGMode()) {
ret.addCheckBox("Build .odlrg file (needs at least 6 GB memory - use java Xmx flag)", c.isBuildROGFile(), new CheckChangedListener() {
@Override
public void checkChanged(boolean isChecked) {
c.setBuildROGFile(isChecked);
nbRogThreads.setEnabled(isChecked);
}
});
}
else{
ret.addLine(new JLabel("Warning - building an .odlrg file requires lots of memory. Run ODL Studio with minimum 6 GB using java Xmx flag."));
}
ret.addHalfWhitespace();
ret.addLine(nbRogThreads);
return ret;
}
@Override
public long getFlags(ODLApi api, int mode) {
return ODLComponent.FLAG_ALLOW_USER_INTERACTION_WHEN_RUNNING;
}
@Override
public Icon getIcon(ODLApi api, int mode) {
return Icons.loadFromStandardPath("shapefile-exporter.png");
}
@Override
public boolean isModeSupported(ODLApi api, int mode) {
return mode == ODLComponent.MODE_DEFAULT;
}
@Override
public void registerScriptTemplates(ScriptTemplatesBuilder templatesApi) {
ExportConfig c = new ExportConfig();
templatesApi.registerTemplate(getName(), getName(), getName(), getIODsDefinition(templatesApi.getApi(), c), c);
c = new ExportConfig();
c.setConvertShpToROGMode(true);
String s = "Create .odlrg file from pre-existing .shp";
templatesApi.registerTemplate(s, s,s, getIODsDefinition(templatesApi.getApi(), c), c);
}
private void getAtomicGeometries(Geometry g, List<Geometry> outList) {
if (GeometryCollection.class.isInstance(g)) {
for (int i = 0; i < g.getNumGeometries(); i++) {
getAtomicGeometries(g.getGeometryN(i), outList);
}
} else {
outList.add(g);
}
}
/**
* Assign the geometry to the input type assuming the geometry has already passed the isAssignableToType test.
*
* @param g
* @param targetType
* @return
*/
private Geometry assignToType(Geometry g, Geometries targetType) {
Geometries currentType = Geometries.get(g);
if (currentType == targetType) {
return g;
}
ArrayList<Geometry> list = new ArrayList<>();
getAtomicGeometries(g, list);
GeometryFactory factory = new GeometryFactory();
switch (targetType) {
case MULTIPOINT:
return factory.createMultiPoint(list.toArray(new Point[list.size()]));
case MULTILINESTRING:
return factory.createMultiLineString(list.toArray(new LineString[list.size()]));
case MULTIPOLYGON:
return factory.createMultiPolygon(list.toArray(new Polygon[list.size()]));
default:
return g;
}
}
/**
* Check if the input geometry can be assigned to the type
*
* @param g
* @param type
* @return
*/
private boolean isAssignableToType(Geometry g, Geometries type) {
Geometries thisType = Geometries.get(g);
// check for exactly the same type
if (thisType == type) {
return true;
}
// check for having simple type and complex type being acceptable
if (type == Geometries.MULTIPOLYGON && thisType == Geometries.POLYGON) {
return true;
}
if (type == Geometries.MULTILINESTRING && thisType == Geometries.LINESTRING) {
return true;
}
if (type == Geometries.MULTIPOINT && thisType == Geometries.POINT) {
return true;
}
if (thisType == Geometries.GEOMETRYCOLLECTION) {
for (int i = 0; i < g.getNumGeometries(); i++) {
if (!isAssignableToType(g.getGeometryN(i), type)) {
return false;
}
}
return true;
}
return false;
}
/**
* Identify a geometry type for the table
*
* @param table
* @return
*/
private Geometries identifyType(ODLTableReadOnly table) {
int n = table.getRowCount();
int geomCol = geomColumn(table);
for (Geometries type : Geometries.values()) {
int count = 0;
for (int i = 0; i < n; i++) {
ODLGeom geom = (ODLGeom) table.getValueAt(i, geomCol);
if (geom != null) {
Geometry g = ((ODLGeomImpl) geom).getJTSGeometry();
if (!isAssignableToType(g, type)) {
count = -1;
break;
}
count++;
}
}
if (count > 0) {
return type;
}
}
return null;
}
/**
* Get the non-geom field names or null if field not allowed to be exported (repeat field name when truncated)
*
* @param table
* @return
*/
private String[] getNonGeomFieldNames(ODLTableDefinition table) {
int n = table.getColumnCount();
String[] ret = new String[n];
int gc = geomColumn(table);
for (int i = 0; i < n; i++) {
if (i != gc) {
String s = table.getColumnName(i);
if (s.length() >= 10) {
s = s.substring(0, 10);
}
// check unique...
boolean unique = true;
for (int j = 0; j < i; j++) {
if (Strings.equals(ret[j], s)) {
unique = false;
}
}
if (unique) {
ret[i] = s;
}
}
}
return ret;
}
private SimpleFeatureType createFeatureType(ODLTableReadOnly table, Geometries geomtype) {
SimpleFeatureTypeBuilder builder = new SimpleFeatureTypeBuilder();
builder.setName(table.getName());
builder.setCRS(DefaultGeographicCRS.WGS84);
// add geom first
builder.add("the_geom", geomtype.getBinding());
// add other types
int geomCol = geomColumn(table);
int nc = table.getColumnCount();
String[] fieldnames = getNonGeomFieldNames(table);
for (int i = 0; i < nc; i++) {
if (i != geomCol && fieldnames[i] != null) {
Class<?> cls;
switch (table.getColumnType(i)) {
case LONG:
cls = Integer.class;
break;
case DOUBLE:
cls = Double.class;
break;
default:
cls = String.class;
break;
}
builder.add(fieldnames[i], cls);
}
}
SimpleFeatureType type = builder.buildFeatureType();
return type;
}
}