/* * Geotoolkit - An Open Source Java GIS Toolkit * http://www.geotoolkit.org * * (C) 2013, Geomatys * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; * version 2.1 of the License. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. */ package org.geotoolkit.data.mapinfo.mif; import com.vividsolutions.jts.geom.*; import org.apache.sis.storage.DataStoreException; import org.geotoolkit.data.mapinfo.mif.geometry.*; import org.opengis.referencing.crs.CoordinateReferenceSystem; import org.opengis.referencing.operation.MathTransform; import java.awt.geom.Rectangle2D; import java.io.*; import java.net.URI; import java.text.DecimalFormat; import java.text.SimpleDateFormat; import java.util.Date; import java.util.Scanner; import org.apache.sis.feature.FeatureExt; import org.apache.sis.geometry.Envelope2D; import org.apache.sis.internal.feature.AttributeConvention; import org.apache.sis.internal.feature.Geometries; import org.apache.sis.internal.system.DefaultFactories; import org.apache.sis.util.ArgumentChecks; import org.opengis.feature.Feature; import org.opengis.feature.FeatureType; import org.opengis.feature.IdentifiedType; import org.opengis.feature.Operation; import org.opengis.feature.PropertyNotFoundException; import org.opengis.feature.PropertyType; import org.opengis.filter.FilterFactory; /** * Utility methods and constants for mif/mid parsing. * * @author Alexis Manin (Geomatys) * Date : 20/02/13 */ public final class MIFUtils { public static final String DEFAULT_CHARSET = "ISO-8859-1"; private static final int MAX_CHAR_LENGTH = 255; private static final DecimalFormat NUM_FORMAT = new DecimalFormat(); public static final FilterFactory FF = DefaultFactories.forBuildin(FilterFactory.class); static { NUM_FORMAT.setGroupingUsed(false); NUM_FORMAT.setDecimalSeparatorAlwaysShown(false); NUM_FORMAT.setMaximumFractionDigits(10); NUM_FORMAT.setMaximumIntegerDigits(10); } /** * An enum to list the header labels we can encounter in MIF file. */ static public enum HeaderCategory { // The headers label, stored in logical order of encounter( as told in specification. /** Mif file version */ VERSION, /** character encoding */ CHARSET, /** (Optional) delimiting character in quotation marks */ DELIMITER, /** (Optional) Numbers indicating database column for eventual identifiers. */ UNIQUE, /** (Optional) Numbers for eventual database index. */ INDEX, /** (Optional) Feature CRS. If no provided, data is long/lat format. */ COORDSYS, /** (Optional) Transform coefficients to apply to geometries. */ TRANSFORM, /** The number and definition of the feature attributes */ COLUMNS, /** The beginning of the real data */ DATA; } /** * An enum to bind a MIF column type with the Java class to use for type representation. */ static public enum AttributeType { CHAR(String.class), INTEGER(Integer.class), SMALLINT(Short.class), DECIMAL(Double.class), FLOAT(Float.class), DATE(Date.class), LOGICAL(Boolean.class), LONG(Long.class); public final Class binding; private AttributeType(Class Bind) { binding = Bind; } } static public enum GeometryType { POINT(new MIFPointBuilder()), PLINE(new MIFPolyLineBuilder()), LINE(new MIFLineBuilder()), REGION(new MIFRegionBuilder()), ARC(new MIFArcBuilder()), TEXT(new MIFTextBuilder()), RECTANGLE(new MIFRectangleBuilder()), ROUNDRECT(new MIFRectangleBuilder()), ELLIPSE(new MIFEllipseBuilder()), MULTIPOINT(new MIFMultiPointBuilder()), COLLECTION(new MIFCollectionBuilder()), GEOMETRY(new MIFDefaultGeometryBuilder()); public final MIFGeometryBuilder binding; private GeometryType(MIFGeometryBuilder Binder) { binding = Binder; } /** * Get the type used to build MIF geometry. * * @param crs The coordinate reference system to use in this type. * @param parent The FeatureType we want to inherit from. * @return */ public FeatureType getBinding(CoordinateReferenceSystem crs, FeatureType parent) { return binding.buildType(crs, parent); } public void readGeometry(Scanner reader, Feature toFill, MathTransform toApply) throws DataStoreException { binding.buildGeometry(reader, toFill, toApply); } public String toMIFSyntax(Feature toConvert) throws DataStoreException { return binding.toMIFSyntax(toConvert); } } /** * Build a MIF representation of the given feature's geometry. * @param toConvert The feature we want to extract geometry from. * @return A string which is the feature geometry in MIF syntax, or null if there's no compatible geometry in the * feature * @throws DataStoreException If we get a problem while geometry conversion. */ public static String buildMIFGeometry(Feature toConvert) throws DataStoreException { String mifGeom = null; if (FeatureExt.hasAGeometry(toConvert.getType())) { final GeometryType geomBuilder = identifyFeature(toConvert.getType()); if (geomBuilder != null) { mifGeom = geomBuilder.toMIFSyntax(toConvert); } } return mifGeom; } /** * Check the given FeatureType to know if its geometry property can be managed by MIF writer. Here we check feature * type and not geometry because of style information which can be stored in the feature. * @param toIdentify The feature type to check geometry. * @return The MIF {@link GeometryType} corresponding to this feature. */ public static GeometryType identifyFeature(FeatureType toIdentify) { GeometryType type = null; /* We'll check for the exact featureType first, and if there's no matching, we'll refine our search by checking * the geometry classes. */ final CoordinateReferenceSystem crsParam = FeatureExt.getCRS(toIdentify); FeatureType superParam = null; if(!toIdentify.getSuperTypes().isEmpty()) { superParam = (FeatureType) toIdentify.getSuperTypes().iterator().next(); } for(GeometryType gType : GeometryType.values()) { if(FeatureExt.sameProperties(gType.getBinding(crsParam, superParam), toIdentify, true)) { type = gType; break; } } // for some types, we don't need to get the same featureType, only a matching geometry class will be sufficient. final IdentifiedType geomType; if(type == null && (geomType = findGeometryProperty(toIdentify)) != null) { final Class sourceClass = toGeometryAttribute(geomType).getValueClass(); if(Polygon.class.isAssignableFrom(sourceClass) || MultiPolygon.class.isAssignableFrom(sourceClass)) { type = GeometryType.REGION; } else if(LineString.class.isAssignableFrom(sourceClass) || MultiLineString.class.isAssignableFrom(sourceClass)) { type = GeometryType.PLINE; } else if(Point.class.isAssignableFrom(sourceClass) || Coordinate.class.isAssignableFrom(sourceClass)) { type = GeometryType.POINT; } else if(MultiPoint.class.isAssignableFrom(sourceClass) || CoordinateSequence.class.isAssignableFrom(sourceClass)) { type = GeometryType.MULTIPOINT; } else if(Envelope.class.isAssignableFrom(sourceClass) || Envelope2D.class.isAssignableFrom(sourceClass) || Rectangle2D.class.isAssignableFrom(sourceClass)) { type = GeometryType.RECTANGLE; } else if(GeometryCollection.class.isAssignableFrom(sourceClass)) { type = GeometryType.COLLECTION; } else if (Geometry.class.isAssignableFrom(sourceClass)) { type = GeometryType.GEOMETRY; } } return type; } /** * @param input A feature type to find a geometry attribute into. * @return the first geometry attribute found in given feature type. */ public static IdentifiedType findGeometryProperty(final FeatureType input) { if (input == null || !FeatureExt.hasAGeometry(input)) return null; org.opengis.feature.AttributeType<?> geomType = FeatureExt.getDefaultGeometryAttribute(input); if (geomType != null) { return geomType; } org.opengis.feature.AttributeType found; for (final PropertyType pt : input.getProperties(true)) { found = toGeometryAttribute(pt); if (found != null) return pt; } return null; } /** * Check if given property type is a geometry one. If it is, we're sending * back the exact attribute type of the geometry. * @param input The property type to analyse. * @return The backed geometry type, or null if the given type does not refers */ public static org.opengis.feature.AttributeType toGeometryAttribute(IdentifiedType input) { while (input instanceof Operation) { input = ((Operation) input).getResult(); } if (input instanceof org.opengis.feature.AttributeType && Geometries.isKnownType(((org.opengis.feature.AttributeType) input).getValueClass())) { return (org.opengis.feature.AttributeType) input; } return null; } public static Object getGeometryValue(final Feature input) { IdentifiedType found = findGeometryProperty(input.getType()); if (found != null) { return input.getPropertyValue(found.getName().tip().toString()); } return null; } /** * Retrieve the Java class bound to the given typename. It works only for primitive attribute types. * * @param typename The string to retrieve class from. * @return A class matching given typename, or null if we can't find one. */ public static Class getColumnJavaType(String typename) { ArgumentChecks.ensureNonNull("Type name", typename); Class attClass = null; for(AttributeType type : AttributeType.values()) { if(typename.equalsIgnoreCase(type.name())) { attClass = type.binding; break; } } return attClass; } /** * Get the MIF name bound to the given Java type. You can look at {@link MIFUtils.AttributeType} for supported types. * @param javaBinding The class to retrieve type identifier from. * @return The mif primitive type name, or null if no equivalent can be found. */ public static String getColumnMIFType(Class javaBinding) { ArgumentChecks.ensureNonNull("Java class", javaBinding); String typename = null; for(AttributeType type : AttributeType.values()) { if(type.binding.isAssignableFrom(javaBinding)) { typename = type.name(); // If we get a char or decimal type, we must set length delimiter on it. if (type.equals(AttributeType.CHAR)) { typename = typename+'('+MAX_CHAR_LENGTH+')'; } else if(type.equals(AttributeType.DECIMAL)) { typename = typename+'('+NUM_FORMAT.getMaximumIntegerDigits()+','+NUM_FORMAT.getMaximumFractionDigits()+')'; } else if(type.equals(AttributeType.LONG)){ typename = AttributeType.DECIMAL.name()+'('+NUM_FORMAT.getMaximumIntegerDigits()+','+NUM_FORMAT.getMaximumFractionDigits()+')'; } break; } } return typename; } /** * Retrieve the feature type bound to the given typename. It works only for geometry types. * * It accept a null parameter, because when searching pattern in file, the result could be null. * * @param crs The CRS to use for the geometries. * @param typename The string to retrieve type from (Should be a MIF id, like ROUNDRECT, REGION, etc). * @param parent The feature type to set as parent of this geometryType. * @return A {@link FeatureType} matching given typename, or null if we can't find one. */ public static FeatureType getGeometryType(String typename, CoordinateReferenceSystem crs, FeatureType parent) { if(typename == null || typename.isEmpty()) { return null; } FeatureType geomType = null; for(GeometryType type : GeometryType.values()) { if(typename.equalsIgnoreCase(type.name())) { geomType = type.getBinding(crs, parent); break; } } return geomType; } public static GeometryType getGeometryType(String typename) { if (typename == null || typename.isEmpty()) { return null; } for (GeometryType type : GeometryType.values()) { if (typename.equalsIgnoreCase(type.name())) { return type; } } return null; } /** * Read the given input to build a geometry which type match the given typename * @param typeName The type of geometry to build. * @param reader The scanner to use for reading (at the wanted geometry position). * @param toFill The feature to fill with geometry data. * @throws DataStoreException If geometry can't be read. */ public void readGeometry(String typeName, Scanner reader, Feature toFill, MathTransform toApply) throws DataStoreException { ArgumentChecks.ensureNonNull("Reader", reader); ArgumentChecks.ensureNonNull("Feature to fill", toFill); for(GeometryType type : GeometryType.values()) { if(typeName.equalsIgnoreCase(type.name())) { type.readGeometry(reader, toFill, toApply); return; } } } /** * Parse the given {@link FeatureType} to build a list of types in MIF format (as header COLUMNS category describes them). * @param toWorkWith The FeatureType to parse, can't be null. * @param builder A StringBuilder in which we'll append generated types. If null, a new one is created. */ public static void featureTypeToMIFSyntax(FeatureType toWorkWith, StringBuilder builder) throws DataStoreException { ArgumentChecks.ensureNonNull("FeatureType to convert", toWorkWith); if(builder == null) { builder = new StringBuilder(); } if(builder .length() > 0 && builder.charAt(builder.length()-1) != '\n') { builder.append('\n'); } for(PropertyType desc : toWorkWith.getProperties(true)) { // geometries are not specified in MIF columns. if (AttributeConvention.isGeometryAttribute(desc)) { continue; } final Class valueClass = ((org.opengis.feature.AttributeType)desc).getValueClass(); final String mifType = getColumnMIFType(valueClass); if( mifType == null) { throw new DataStoreException("Type "+valueClass+" has no equivalent in MIF format."); } builder.append('\t').append(desc.getName().tip().toString()).append(' ').append(mifType.toLowerCase()).append('\n'); } } /** * Check if the data pointed by given URL is inside or outside current fileSystem. * @param uri The address of the file to test. * @return true if the URL describe a local file, false otherwise. */ public static boolean isLocal(final URI uri){ return "file".equalsIgnoreCase(uri.getScheme()); } /** * Write a stream into another. * @param in The source inputStream * @param writer The {@link OutputStreamWriter} which will write input stream into destination stream. * @throws IOException If there's a problem connecting to one of the streams. */ public static void write(InputStream in, OutputStreamWriter writer) throws IOException { InputStreamReader reader = new InputStreamReader(in); char[] inBuffer = new char[1024]; int byteRead = 0; while((byteRead = reader.read(inBuffer)) >= 0) { writer.write(inBuffer, 0, byteRead); } } /** * Return a String which is the ready-to-write (for MID file) representation of the given property. * @param value The property value * @return A string which is the value of the given property. Never Null, but can be empty. */ public static String getStringValue(Object value) { if(value == null) { return ""; } if(value instanceof Number) { return NUM_FORMAT.format(value); } else if(value instanceof Date) { SimpleDateFormat format = new SimpleDateFormat("yyyyMMdd"); return format.format(value); } return value.toString(); } /** * * @param container * @param propertyName Name of the * @return The value associated to the queried property name, or null if the * property is not defined. */ public static Object getPropertySafe(final Feature container, final String propertyName) { try { return container.getPropertyValue(propertyName); } catch (PropertyNotFoundException e) { return null; } } }