// License: GPL. For details, see LICENSE file. package org.openstreetmap.josm.plugins.opendata.core.io.geographic; import static org.openstreetmap.josm.plugins.opendata.core.io.geographic.MifDatum.CUSTOM; import static org.openstreetmap.josm.plugins.opendata.core.io.geographic.MifDatum.GEODETIC_REFERENCE_SYSTEM_1980_GRS_80; import static org.openstreetmap.josm.plugins.opendata.core.io.geographic.MifProjection.Hotine_Oblique_Mercator; import static org.openstreetmap.josm.plugins.opendata.core.io.geographic.MifProjection.Longitude_Latitude; import java.io.BufferedReader; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.Reader; import java.io.UnsupportedEncodingException; import java.nio.charset.Charset; import java.util.Arrays; 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.osm.OsmPrimitive; import org.openstreetmap.josm.data.osm.Relation; import org.openstreetmap.josm.data.osm.RelationMember; import org.openstreetmap.josm.data.osm.Way; import org.openstreetmap.josm.data.projection.CustomProjection; import org.openstreetmap.josm.data.projection.CustomProjection.Param; import org.openstreetmap.josm.data.projection.Projection; import org.openstreetmap.josm.data.projection.Projections; import org.openstreetmap.josm.gui.progress.NullProgressMonitor; import org.openstreetmap.josm.gui.progress.ProgressMonitor; import org.openstreetmap.josm.plugins.opendata.core.OdConstants; import org.openstreetmap.josm.plugins.opendata.core.datasets.AbstractDataSetHandler; import org.openstreetmap.josm.plugins.opendata.core.gui.ChooserLauncher; import org.openstreetmap.josm.plugins.opendata.core.io.InputStreamReaderUnbuffered; import org.openstreetmap.josm.plugins.opendata.core.util.OdUtils; /** * MapInfo Interchange File (MIF) reader, based on these specifications:<ul> * <li><a href="https://github.com/tricycle/electrodrive-market-analysis/blob/master/specifications/Mapinfo_Mif.pdf">Mapinfo_Mif.pdf</a></li> * <li><a href="http://resource.mapinfo.com/static/files/document/1074660800077/interchange_file.pdf">interchange_file.pdf</a></li> * </ul> * These files have been stored in reference directory to avoid future dead links. */ public final class MifReader extends AbstractMapInfoReader { private enum State { UNKNOWN, READING_COLUMNS, START_POLYGON, READING_POINTS, END_POLYGON, START_POLYLINE_SEGMENT, END_POLYLINE } private final AbstractDataSetHandler handler; private File file; private InputStream stream; protected Charset charset; protected BufferedReader midReader; private Character delimiter = '\t'; private State state = State.UNKNOWN; private Projection josmProj; private DataSet ds; private Relation region; private Way polygon; private Node node; private Way polyline; // CoordSys clause private String units; private Double originLon; private Double originLat; private Double stdP1; private Double stdP2; private Double scaleFactor; private Double falseEasting; private Double falseNorthing; private Double minx; private Double miny; private Double maxx; private Double maxy; // Region clause private int numpolygons = -1; private int numpts = -1; // PLine clause private int numsections = -1; private MifReader(AbstractDataSetHandler handler) { this.handler = handler; } public static DataSet parseDataSet(InputStream in, File file, AbstractDataSetHandler handler, ProgressMonitor instance) throws IOException { return new MifReader(handler).parse(in, file, instance, Charset.forName(OdConstants.ISO8859_15)); } private void parseDelimiter(String[] words) { delimiter = words[1].charAt(1); } private void parseUnique(String[] words) { // TODO Main.warn("TODO Unique: "+line); } private void parseIndex(String[] words) { // TODO Main.warn("TODO Index: "+line); } private static String param(Param p, Object value) { return " +"+p.key+"="+value; } private void parseCoordSysSyntax1(String[] words) { MifProjection proj = MifProjection.forCode(Integer.parseInt(words[3])); MifDatum datum = MifDatum.forCode(Integer.parseInt(words[4])); // Custom datum: TODO: use custom decalage values int offset = datum == CUSTOM ? 4 : 0; if (proj == Longitude_Latitude) { josmProj = Projections.getProjectionByCode("EPSG:4326"); // WGS 84 return; } // Initialize proj4-like parameters String params = param(Param.proj, proj.getProj4Id()); // Units units = words[5+offset]; params += param(Param.units, units); // Origin, longitude originLon = Double.parseDouble(words[6+offset]); params += param(Param.lon_0, originLon); // Origin, latitude switch(proj) { case Albers_Equal_Area_Conic: case Azimuthal_Equidistant_polar_aspect_only: case Equidistant_Conic_also_known_as_Simple_Conic: case Hotine_Oblique_Mercator: case Lambert_Azimuthal_Equal_Area_polar_aspect_only: case Lambert_Conformal_Conic: case Lambert_Conformal_Conic_modified_for_Belgium_1972: case New_Zealand_Map_Grid: case Stereographic: case Swiss_Oblique_Mercator: case Transverse_Mercator_also_known_as_Gauss_Kruger: case Transverse_Mercator_modified_for_Danish_System_34_Jylland_Fyn: case Transverse_Mercator_modified_for_Danish_System_45_Bornholm: case Transverse_Mercator_modified_for_Finnish_KKJ: case Transverse_Mercator_modified_for_Sjaelland: case Polyconic: originLat = Double.parseDouble(words[7+offset]); params += param(Param.lat_0, originLat); break; } // Standard Parallel 1 switch (proj) { case Cylindrical_Equal_Area: case Regional_Mercator: stdP1 = Double.parseDouble(words[7+offset]); params += param(Param.lat_1, stdP1); break; case Albers_Equal_Area_Conic: case Equidistant_Conic_also_known_as_Simple_Conic: case Lambert_Conformal_Conic: case Lambert_Conformal_Conic_modified_for_Belgium_1972: stdP1 = Double.parseDouble(words[8+offset]); params += param(Param.lat_1, stdP1); break; } // Standard Parallel 2 switch (proj) { case Albers_Equal_Area_Conic: case Equidistant_Conic_also_known_as_Simple_Conic: case Lambert_Conformal_Conic: case Lambert_Conformal_Conic_modified_for_Belgium_1972: stdP2 = Double.parseDouble(words[9+offset]); params += param(Param.lat_2, stdP2); break; } // Azimuth if (proj == Hotine_Oblique_Mercator) { Double.parseDouble(words[8+offset]); // TODO: what's proj4 parameter ? } // Scale Factor switch (proj) { case Hotine_Oblique_Mercator: scaleFactor = Double.parseDouble(words[9+offset]); params += param(Param.k_0, scaleFactor); break; case Stereographic: case Transverse_Mercator_also_known_as_Gauss_Kruger: case Transverse_Mercator_modified_for_Danish_System_34_Jylland_Fyn: case Transverse_Mercator_modified_for_Danish_System_45_Bornholm: case Transverse_Mercator_modified_for_Finnish_KKJ: case Transverse_Mercator_modified_for_Sjaelland: scaleFactor = Double.parseDouble(words[8+offset]); params += param(Param.k_0, scaleFactor); break; } // False Easting/Northing switch (proj) { case Albers_Equal_Area_Conic: case Equidistant_Conic_also_known_as_Simple_Conic: case Hotine_Oblique_Mercator: case Lambert_Conformal_Conic: case Lambert_Conformal_Conic_modified_for_Belgium_1972: falseEasting = Double.parseDouble(words[10+offset]); falseNorthing = Double.parseDouble(words[11+offset]); params += param(Param.x_0, falseEasting); params += param(Param.y_0, falseNorthing); break; case Stereographic: case Transverse_Mercator_also_known_as_Gauss_Kruger: case Transverse_Mercator_modified_for_Danish_System_34_Jylland_Fyn: case Transverse_Mercator_modified_for_Danish_System_45_Bornholm: case Transverse_Mercator_modified_for_Finnish_KKJ: case Transverse_Mercator_modified_for_Sjaelland: falseEasting = Double.parseDouble(words[9+offset]); falseNorthing = Double.parseDouble(words[10+offset]); params += param(Param.x_0, falseEasting); params += param(Param.y_0, falseNorthing); break; case New_Zealand_Map_Grid: case Swiss_Oblique_Mercator: case Polyconic: falseEasting = Double.parseDouble(words[8+offset]); falseNorthing = Double.parseDouble(words[9+offset]); params += param(Param.x_0, falseEasting); params += param(Param.y_0, falseNorthing); break; } // Range switch (proj) { case Azimuthal_Equidistant_polar_aspect_only: case Lambert_Azimuthal_Equal_Area_polar_aspect_only: Double.parseDouble(words[8+offset]); // TODO: what's proj4 parameter ? } switch (proj) { case Lambert_Conformal_Conic: if ((datum == GEODETIC_REFERENCE_SYSTEM_1980_GRS_80 || datum == CUSTOM) && equals(originLon, 3.0)) { // This sounds good for Lambert 93 or Lambert CC 9 if (equals(originLat, 46.5) && equals(stdP1, 44.0) && equals(stdP2, 49.0) && equals(falseEasting, 700000.0) && equals(falseNorthing, 6600000.0)) { josmProj = Projections.getProjectionByCode("EPSG:2154"); // Lambert 93 } else if (equals(falseEasting, 1700000.0)) { for (int i = 0; josmProj == null && i < 9; i++) { if (equals(originLat, 42.0+i) && equals(stdP1, 41.25+i) && equals(stdP2, 42.75+i) && equals(falseNorthing, (i+1)*1000000.0 + 200000.0)) { josmProj = Projections.getProjectionByCode("EPSG:"+Integer.toString(3942 + i)); // LambertCC9Zones } } } } break; } // TODO: handle cases with Affine declaration int index = parseAffineUnits(words); // handle cases with Bounds declaration parseBounds(words, index); if (josmProj == null) { Main.info(line); Main.info(params); josmProj = new CustomProjection(params); } } private void parseCoordSysSyntax2(String[] words) { // handle cases with Affine declaration int index = parseAffineUnits(words); units = words[index+1]; parseBounds(words, index+2); } private int parseAffineUnits(String[] words) { // TODO: handle affine units return 2+0; } private void parseBounds(String[] words, int index) { if (index < words.length && "Bounds".equals(words[index])) { // Useless parenthesis... "(minx, miny) (maxx, maxy)" minx = Double.valueOf(words[index+1].substring(1)); miny = Double.valueOf(words[index+2].substring(0, words[index+2].length()-1)); maxx = Double.valueOf(words[index+3].substring(1)); maxy = Double.valueOf(words[index+4].substring(0, words[index+4].length()-1)); if (Main.isTraceEnabled()) { Main.trace(Arrays.toString(words) + " -> "+minx+","+miny+","+maxx+","+maxy); } } } private void parseCoordSys(String[] words) { for (int i = 0; i < words.length; i++) { words[i] = words[i].replace(",", ""); } switch (words[1].toLowerCase()) { case "earth": parseCoordSysSyntax1(words); break; case "nonearth": parseCoordSysSyntax2(words); // CHECKSTYLE.OFF: LineLength // Syntax2 is not meant to be used for maps, and still... # 9592 happened // From MapInfo documentation: // http://testdrive.mapinfo.com/TDC/mxtreme4java.nsf/22fbc128f401ad818525666a00646bda/50100fdbe3e0a85085256a770053be1a/$FILE/coordsys.txt // Use syntax 1 (above) to explicitly define a coordinate system for an Earth map (a map having coordinates which are specified with respect to a // location on the surface of the Earth). The optional Projection parameters dictate what map projection, if any, should be used in conjunction with // the coordinate system. If the Projection clause is omitted, MapBasic uses a longitude, latitude coordinate system using the North American Datum of 1927 (NAD-27). // Use syntax 2 to explicitly define a non-Earth coordinate system, such as the coordinate system used in a floor plan or other CAD drawing. // CHECKSTYLE.ON: LineLength if (handler != null && handler.getMifHandler() != null && handler.getMifHandler().getCoordSysNonEarthProjection() != null) { josmProj = handler.getMifHandler().getCoordSysNonEarthProjection(); } else { josmProj = ChooserLauncher.askForProjection(NullProgressMonitor.INSTANCE); } break; case "layout": case "table": case "window": Main.error("Unsupported CoordSys clause: "+line); break; default: Main.error("Line "+lineNum+". Invalid CoordSys clause: "+line); } } private void parseTransform(String[] words) { // TODO Main.warn("TODO Transform: "+line); } @Override protected void parseColumns(String[] words) { super.parseColumns(words); state = State.READING_COLUMNS; } private void parseData(String[] words) { if (ds == null) { ds = new DataSet(); } } private void parsePoint(String[] words) throws IOException { readAttributes(createNode(words[1], words[2])); } private void parseLine(String[] words) throws IOException { Way line = new Way(); ds.addPrimitive(line); readAttributes(line); line.addNode(createNode(words[1], words[2])); line.addNode(createNode(words[3], words[4])); } private void startPolyLineSegment(boolean initial) throws IOException { Way previousPolyline = polyline; polyline = new Way(); ds.addPrimitive(polyline); if (initial) { readAttributes(polyline); } else if (previousPolyline != null) { // Not sure about how to handle multiple segments. In doubt we create a new way with the same tags polyline.setKeys(previousPolyline.getKeys()); } state = State.READING_POINTS; } private void parsePLine(String[] words) throws IOException { numsections = 1; if (words.length <= 1 || "MULTIPLE".equalsIgnoreCase(words[1])) { numpts = -1; state = State.START_POLYLINE_SEGMENT; if (words.length >= 3) { // pline with multiple sections numsections = Integer.parseInt(words[2]); } } else { numpts = Integer.parseInt(words[1]); // Not described in PDF but found in real files: PLINE XX, with XX = numpoints startPolyLineSegment(true); } } private void parseRegion(String[] words) throws IOException { numpolygons = Integer.parseInt(words[1]); if (numpolygons > 1) { region = new Relation(); region.put("type", "multipolygon"); ds.addPrimitive(region); readAttributes(region); } else { region = null; } state = State.START_POLYGON; } private void parseArc(String[] words) { // TODO Main.warn("TODO Arc: "+line); } private void parseText(String[] words) { // TODO Main.warn("TODO Text: "+line); } private void parseRect(String[] words) { // TODO Main.warn("TODO Rect: "+line); } private void parseRoundRect(String[] words) { // TODO Main.warn("TODO RoundRect: "+line); } private void parseEllipse(String[] words) { // TODO Main.warn("TODO Ellipse: "+line); } private void initializeReaders(InputStream in, File f, Charset cs, int bufSize) throws IOException { stream = in; charset = cs; file = f; Reader isr; // Did you know ? new InputStreamReader(in, charset) has a non-configurable buffer of 8kb :( if (bufSize < 8192) { isr = new InputStreamReaderUnbuffered(in, charset); } else { isr = new InputStreamReader(in, charset); } headerReader = new BufferedReader(isr, bufSize); if (midReader != null) { midReader.close(); } midReader = getDataReader(file, ".mid", charset); } private DataSet parse(InputStream in, File file, ProgressMonitor instance, Charset cs) throws IOException { try { try { // Read header byte per byte until we determine correct charset initializeReaders(in, file, cs, 1); parseHeader(); return ds; } finally { if (midReader != null) { midReader.close(); } } } catch (UnsupportedEncodingException e) { throw new IOException(e); } } @Override protected void parseHeaderLine(String[] words) throws IOException { if (words[0].equalsIgnoreCase("Version")) { parseVersion(words); } else if (words[0].equalsIgnoreCase("Charset")) { // Reinitialize readers with an efficient buffer value now we know for sure the good charset initializeReaders(stream, file, parseCharset(words), 8192); } else if (words[0].equalsIgnoreCase("Delimiter")) { parseDelimiter(words); } else if (words[0].equalsIgnoreCase("Unique")) { parseUnique(words); } else if (words[0].equalsIgnoreCase("Index")) { parseIndex(words); } else if (words[0].equalsIgnoreCase("CoordSys")) { parseCoordSys(words); } else if (words[0].equalsIgnoreCase("Transform")) { parseTransform(words); } else if (words[0].equalsIgnoreCase("Columns")) { parseColumns(words); } else if (words[0].equalsIgnoreCase("Data")) { parseData(words); } else if (ds != null) { if (state == State.START_POLYGON) { numpts = Integer.parseInt(words[0]); polygon = new Way(); ds.addPrimitive(polygon); if (region != null) { region.addMember(new RelationMember("outer", polygon)); } else { readAttributes(polygon); } state = State.READING_POINTS; } else if (state == State.START_POLYLINE_SEGMENT) { numpts = Integer.parseInt(words[0]); startPolyLineSegment(polyline != null); } else if (state == State.READING_POINTS && numpts > 0) { if (josmProj != null) { node = createNode(words[0], words[1]); if (polygon != null) { polygon.addNode(node); } else if (polyline != null) { polyline.addNode(node); } } if (--numpts == 0) { if (numpolygons > -1) { if (!polygon.isClosed()) { polygon.addNode(polygon.firstNode()); } if (--numpolygons > 0) { state = State.START_POLYGON; } else { state = State.END_POLYGON; polygon = null; } } else if (polyline != null) { if (--numsections > 0) { state = State.START_POLYLINE_SEGMENT; } else { state = State.UNKNOWN; polyline = null; } } } } else if (words[0].equalsIgnoreCase("Point")) { parsePoint(words); } else if (words[0].equalsIgnoreCase("Line")) { parseLine(words); } else if (words[0].equalsIgnoreCase("PLine")) { parsePLine(words); } else if (words[0].equalsIgnoreCase("Region")) { parseRegion(words); } else if (words[0].equalsIgnoreCase("Arc")) { parseArc(words); } else if (words[0].equalsIgnoreCase("Text")) { parseText(words); } else if (words[0].equalsIgnoreCase("Rect")) { parseRect(words); } else if (words[0].equalsIgnoreCase("RoundRect")) { parseRoundRect(words); } else if (words[0].equalsIgnoreCase("Ellipse")) { parseEllipse(words); } else if (words[0].equalsIgnoreCase("Pen")) { // Do nothing } else if (words[0].equalsIgnoreCase("Brush")) { // Do nothing } else if (words[0].equalsIgnoreCase("Center")) { // Do nothing } else if (words[0].equalsIgnoreCase("Symbol")) { // Do nothing } else if (words[0].equalsIgnoreCase("Font")) { // Do nothing } else if (!words[0].isEmpty()) { Main.warn("Line "+lineNum+". Unknown clause in data section: "+line); } } else if (state == State.READING_COLUMNS && numcolumns > 0) { columns.add(words[0]); if (--numcolumns == 0) { state = State.UNKNOWN; } } else if (!line.isEmpty()) { Main.warn("Line "+lineNum+". Unknown clause in header: "+line); } } protected void readAttributes(OsmPrimitive p) throws IOException { if (midReader != null) { String midLine = midReader.readLine(); if (midLine != null) { String[] fields = OdUtils.stripQuotesAndExtraChars(midLine.split(delimiter.toString()), delimiter.toString()); if (columns.size() != fields.length) { Main.error("Incoherence between MID and MIF files ("+columns.size()+" columns vs "+fields.length+" fields)"); } for (int i = 0; i < Math.min(columns.size(), fields.length); i++) { String field = fields[i].trim(); /*if (field.startsWith("\"") && field.endsWith("\"")) { field = fields[i].substring(fields[i].indexOf('"')+1, fields[i].lastIndexOf('"')); }*/ if (!field.isEmpty()) { p.put(columns.get(i), field); } } } } } protected Node createNode(String x, String y) { Node node = new Node(josmProj.eastNorth2latlon(new EastNorth(Double.parseDouble(x), Double.parseDouble(y)))); ds.addPrimitive(node); return node; } /** Compare two doubles within a default epsilon */ public static boolean equals(Double a, Double b) { if (a == b) return true; // If the difference is less than epsilon, treat as equal. return Math.abs(a - b) < 0.0000001; } }