// License: GPL. For details, see LICENSE file.
package org.openstreetmap.josm.plugins.opendata.core.io.tabular;
import static org.openstreetmap.josm.tools.I18n.tr;
import java.awt.GraphicsEnvironment;
import java.io.IOException;
import java.io.InputStream;
import java.text.NumberFormat;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.openstreetmap.josm.Main;
import org.openstreetmap.josm.data.coor.EastNorth;
import org.openstreetmap.josm.data.osm.DataSet;
import org.openstreetmap.josm.data.osm.Node;
import org.openstreetmap.josm.data.projection.Projection;
import org.openstreetmap.josm.gui.progress.ProgressMonitor;
import org.openstreetmap.josm.io.AbstractReader;
import org.openstreetmap.josm.io.IllegalDataException;
import org.openstreetmap.josm.plugins.opendata.core.OdConstants;
import org.openstreetmap.josm.plugins.opendata.core.gui.ChooserLauncher;
import org.openstreetmap.josm.plugins.opendata.core.io.ProjectionPatterns;
public abstract class SpreadSheetReader extends AbstractReader {
private static final NumberFormat formatFrance = NumberFormat.getInstance(Locale.FRANCE);
private static final NumberFormat formatUK = NumberFormat.getInstance(Locale.UK);
private static final String COOR = "(\\-?\\d+(?:[\\.,]\\d+)?)";
// Lat/lon pattern with optional altitude and precision
private static final Pattern LATLON_PATTERN = Pattern.compile(
"^"+COOR+"[,;\\s]\\s*"+COOR+"(?:[,;\\s]\\s*"+COOR+"(?:[,;\\s]\\s*"+COOR+")?)?$");
protected final SpreadSheetHandler handler;
public SpreadSheetReader(SpreadSheetHandler handler) {
this.handler = handler;
}
protected static double parseDouble(String value) throws ParseException {
if (value.contains(",")) {
return formatFrance.parse(value.replace(" ", "")).doubleValue();
} else {
return formatUK.parse(value.replace(" ", "")).doubleValue();
}
}
protected abstract void initResources(InputStream in, ProgressMonitor progressMonitor) throws IOException;
protected abstract String[] readLine(ProgressMonitor progressMonitor) throws IOException;
protected final int getSheetNumber() {
return handler != null && handler.getSheetNumber() > -1 ? handler.getSheetNumber() : 0;
}
protected final int getLineNumber() {
return handler != null ? handler.getLineNumber() : -1;
}
public static class CoordinateColumns {
public Projection proj = null;
public int xCol = -1;
public int yCol = -1;
public final boolean isOk() {
return xCol > -1 && yCol > -1;
}
@Override
public String toString() {
return "CoordinateColumns [proj=" + proj + ", xCol=" + xCol + ", yCol=" + yCol + ']';
}
}
private CoordinateColumns addCoorColIfNeeded(List<CoordinateColumns> columns, CoordinateColumns col) {
if (col == null || col.isOk()) {
columns.add(col = new CoordinateColumns());
}
return col;
}
@Override
protected DataSet doParseDataSet(InputStream source,
ProgressMonitor progressMonitor) throws IllegalDataException {
return null;
}
public DataSet doParse(String[] header, ProgressMonitor progressMonitor) throws IOException {
Main.info("Header: "+Arrays.toString(header));
Map<ProjectionPatterns, List<CoordinateColumns>> projColumns = new HashMap<>();
for (int i = 0; i < header.length; i++) {
for (ProjectionPatterns pp : OdConstants.PROJECTIONS) {
List<CoordinateColumns> columns = projColumns.get(pp);
if (columns == null) {
projColumns.put(pp, columns = new ArrayList<>());
}
CoordinateColumns col = columns.isEmpty() ? null : columns.get(columns.size()-1);
if (pp.getXYPattern().matcher(header[i]).matches()) {
CoordinateColumns coorCol = addCoorColIfNeeded(columns, col);
coorCol.xCol = i;
coorCol.yCol = i;
break;
} else if (pp.getXPattern().matcher(header[i]).matches()) {
addCoorColIfNeeded(columns, col).xCol = i;
break;
} else if (pp.getYPattern().matcher(header[i]).matches()) {
addCoorColIfNeeded(columns, col).yCol = i;
break;
}
}
}
final List<CoordinateColumns> columns = new ArrayList<>();
for (ProjectionPatterns pp : projColumns.keySet()) {
for (CoordinateColumns col : projColumns.get(pp)) {
if (col.isOk()) {
columns.add(col);
if (col.proj == null) {
col.proj = pp.getProjection(header[col.xCol], header[col.yCol]);
}
}
}
}
final boolean handlerOK = handler != null && handler.handlesProjection();
boolean projFound = false;
for (CoordinateColumns c : columns) {
if (c.proj != null) {
projFound = true;
break;
}
}
if (projFound) {
// projection identified, do nothing
} else if (!columns.isEmpty()) {
if (!handlerOK) {
if (GraphicsEnvironment.isHeadless()) {
throw new IllegalArgumentException("No valid coordinates have been found and cannot prompt user in headless mode.");
}
// TODO: filter proposed projections with min/max values ?
Projection p = ChooserLauncher.askForProjection(progressMonitor);
if (p == null) {
return null; // User clicked Cancel
}
for (CoordinateColumns c : columns) {
c.proj = p;
}
}
} else {
throw new IllegalArgumentException(tr("No valid coordinates have been found."));
}
String message = "";
for (CoordinateColumns c : columns) {
if (!message.isEmpty()) {
message += "; ";
}
message += c.proj + "("+header[c.xCol]+", "+header[c.yCol]+")";
}
Main.info("Loading data using projections "+message);
final DataSet ds = new DataSet();
int lineNumber = 1;
String[] fields;
while ((fields = readLine(progressMonitor)) != null) {
lineNumber++;
if (handler != null) {
handler.setXCol(-1);
handler.setYCol(-1);
}
final Map<CoordinateColumns, EastNorth> ens = new HashMap<>();
final Map<CoordinateColumns, Node> nodes = new HashMap<>();
for (CoordinateColumns c : columns) {
nodes.put(c, new Node());
ens.put(c, new EastNorth(Double.NaN, Double.NaN));
}
if (fields.length > header.length) {
Main.warn(tr("Invalid file. Bad length on line {0}. Expected {1} columns, got {2}.", lineNumber, header.length, fields.length));
Main.warn(Arrays.toString(fields));
}
for (int i = 0; i < Math.min(fields.length, header.length); i++) {
try {
boolean coordinate = false;
for (CoordinateColumns c : columns) {
EastNorth en = ens.get(c);
if (i == c.xCol && i == c.yCol) {
Matcher m = LATLON_PATTERN.matcher(fields[i]);
if (m.matches()) {
coordinate = true;
ens.put(c, new EastNorth(parseDouble(m.group(2)), parseDouble(m.group(1))));
if (handler != null) {
handler.setXCol(i);
handler.setYCol(i);
}
}
} else if (i == c.xCol) {
coordinate = true;
ens.put(c, new EastNorth(parseDouble(fields[i]), en.north()));
if (handler != null) {
handler.setXCol(i);
}
} else if (i == c.yCol) {
coordinate = true;
ens.put(c, new EastNorth(en.east(), parseDouble(fields[i])));
if (handler != null) {
handler.setYCol(i);
}
}
}
if (!coordinate) {
if (!fields[i].isEmpty()) {
for (Node n : nodes.values()) {
n.put(header[i], fields[i]);
}
}
}
} catch (ParseException e) {
Main.warn("Parsing error on line "+lineNumber+": "+e.getMessage());
}
}
Node firstNode = null;
for (CoordinateColumns c : columns) {
Node n = nodes.get(c);
EastNorth en = ens.get(c);
if (en.isValid()) {
n.setCoor(c.proj != null && !handlerOK ? c.proj.eastNorth2latlon(en) : handler != null ? handler.getCoor(en, fields) : null);
} else {
Main.warn("Skipping line "+lineNumber+" because no valid coordinates have been found at columns "+c);
}
if (n.getCoor() != null) {
if (firstNode == null) {
firstNode = n;
}
if (n == firstNode || n.getCoor().greatCircleDistance(firstNode.getCoor())
> Main.pref.getDouble(OdConstants.PREF_TOLERANCE, OdConstants.DEFAULT_TOLERANCE)) {
ds.addPrimitive(n);
} else {
nodes.remove(c);
}
}
}
if (handler != null && !Main.pref.getBoolean(OdConstants.PREF_RAWDATA)) {
handler.nodesAdded(ds, nodes, header, lineNumber);
}
}
return ds;
}
public final DataSet parse(InputStream in, ProgressMonitor progressMonitor) throws IOException {
initResources(in, progressMonitor);
String[] header = null;
int length = 0;
int n = 0;
while (header == null || length == 0) {
n++;
header = readLine(progressMonitor);
length = 0;
if (header == null && n > getLineNumber()) {
return null;
} else if (header != null && (getLineNumber() == -1 || getLineNumber() == n)) {
for (String field : header) {
length += field.length();
}
}
}
return doParse(header, progressMonitor);
}
}