目录
前言
一、Shapefile的属性列表信息
1、属性表格信息
2、属性表格包含的要素
二、GeoTools对属性表格的解析
1、常规解析方法
2、基于dbf文件的属性信息读取
三、总结
前言
ESRI Shapefile(shp),或简称shapefile,是美国环境系统研究所公司(ESRI)开发的一种空间数据开放格式。该文件格式已经成为了地理信息软件界的一个开放标准,这表明ESRI公司在全球的地理信息系统市场的重要性。Shapefile也是一种重要的交换格式,它能够在ESRI与其他公司的产品之间进行数据互操作。Shapefile文件用于描述几何体对象:点,折线与多边形。例如,Shapefile文件可以存储井、河流、湖泊等空间对象的几何位置。除了几何位置,shp文件也可以存储这些空间对象的属性,例如一条河流的名字,一个城市的温度等等。
Shapefile属于一种矢量图形格式,它能够保存几何图形的位置及相关属性。但这种格式没法存储地理数据的拓扑信息。Shapefile在九十年代初的ArcView GIS的第二个版本被首次应用。许多自由的程序或商业的程序都可以读取Shapefile。Shapefile是一种比较原始的矢量数据存储方式,它仅仅能够存储几何体的位置数据,而无法在一个文件之中同时存储这些几何体的属性数据。因此,Shapefile还必须附带一个二维表用于存储Shapefile中每个几何体的属性信息。Shapefile中许多几何体能够代表复杂的地理事物,并为他们提供强大而精确的计算能力。
Shapefile文件指的是一种文件存储的方法,实际上该种文件格式是由多个文件组成的。其中,要组成一个Shapefile,有三个文件是必不可少的,它们分别是".shp", ".shx"与 ".dbf"文件。表示同一数据的一组文件其文件名前缀应该相同。例如,存储一个关于湖的几何与属性数据,就必须有lake.shp,lake.shx与lake.dbf三个文件。而其中“真正”的Shapefile的后缀为shp,然而仅有这个文件数据是不完整的,必须要把其他两个附带上才能构成一组完整的地理数据。除了这三个必须的文件以外,还有八个可选的文件,使用它们可以增强空间数据的表达能力。所有的文件名都必须遵循MS DOS的8.3文件名标准(文件前缀名8个字符,后缀名3个字符,如shapefil.shp),以方便与一些老的应用程序保持兼容性,尽管现在许多新的程序都能够支持长文件名。此外,所有的文件都必须位于同一个目录之中。
由于Shapefile有着广泛的适用性,是很多公共地理服务所采用的矢量文件规范之一。因此有必要对Shapefile文件进行深度的解析。如果将shapefile类比为一个数据库,那么它就是一个database,而其中的属性表则是一张一张的物理表。他们两存在天然的对应关系。虽然在前面的博客中,我们已经对shapefile的解析有了基本的认识。但是对属性表格的解析并没有介绍得很详细。本文则重点讲解在Java当中,如何使用GeoTools来进行属性表格的解析,首先在Qgis中重点介绍属性表格的字段信息,然后介绍一种常见的属性字段信息解析方式,其次介绍基于dbf的属性信息解析方法,直接解析dbf文件的方法在很多网络知识中很少见,但是却非常有用,比如在获取double类型的数值时,想获取精度值,就可以通过这种方式来获取。学习本文,不仅更加熟悉GeoTools对属性表格的解析,同时对于如何快速的将Shapefile的属性模型映射成空间库模型有更进一步的认识。希望对您有一定的帮助。
一、Shapefile的属性列表信息
本文主要是讲解如何来深度解析shapefile的属性表,因此就有必要对属性列表信息进行一个详细的讲解。
1、属性表格信息
shapefile文件其实是文件数据库的一种,它将数据的信息写入到shapefile文件中,然后在其它的电脑终端中可以直接共享这些数据,解析这些属性信息。为了介绍方便,我们将shapefile与常见的关系型数据库进行对比,可以这么进行理解。一个shapefile就是一个database,而一张表就是一个属性表。属性表格中的字段、数据类型、长度、精度等就是与关系型数据库中的数据列是一个意思,而数据行基本就是数据库中一行数据的意思。下面是以某年度某城市POI的矢量数据为例进行讲解,在Qgis中打开字段表如下:
2、属性表格包含的要素
既然与关系型数据库的数据定义类似,那么就有必要对属性表格的每一列的信息进行精准的定义。包括数据类型、数据长度、精度等,下面结合示例数据来进行一个深入讲解。
序号 | 参数 | 参数 |
1 | 名称 | 具体的字段名称,如name |
2 | 别名 | 类比于关系数据库的字段别名 |
3 | 类型 | 数据类型,比如QString |
4 | 类型名 | 数据类型名,比如String,Real |
5 | 长度 | 字段的长度,比如:255 |
6 | 精度 | double等类型的精度,如11 |
7 | 注解 | 其它辅助信息 |
有了上面的信息之后,我们也可以根据属性表格来进行空间表的创建,这样管理起来也比较方便。在数据库中定义好每一个列的数据信息之后,就可以将具体的空间信息填充到每一行中,这时候可能就需要外业和内业的工作人员通力配合。
在了解上面的基本信息后,在下面的章节中,我们就要进行属性表格的深度解析操作。
二、GeoTools对属性表格的解析
本节重点使用两种方法来进行属性表格解析的实战,第一种方法是常规的通过featureType的方式来获取属性表格的信息。第二种是基于GeoTools来读取dbf文件来获取元数据信息。这里不仅说明两种方式的优缺点,也说明两者的具体开发代码,帮助大家来深度掌握GeoTools的属性表解析操作。
1、常规解析方法
首先讲解普通的解析方法,同时这种解析的方式在互联网或者很多博客当中是最常见的。也就是直接解析shp文件。具体的解析步骤流程如下所示:
这里给出详细的遍历代码,如下所示:
public void testAttrLength() throws Exception {
// 指定Shapefile的文件路径
String shpFile = "C:/BaiduDownload/长沙市2020年POI数据集/长沙市2020年POI数据集/长沙POI数据(.shp)/风景名胜.shp";
FileDataStore dataStore = FileDataStoreFinder.getDataStore(new File(shpFile));
ShapefileDataStore shapefileDataStore = new ShapefileDataStore(new File(shpFile).toURI().toURL());
System.out.println(shapefileDataStore.getCharset());
// 打开数据存储
String[] typeNames = dataStore.getTypeNames();
System.out.println(typeNames.length);
for (String type : typeNames) {
System.out.println(type);
}
// 获取特征类型
SimpleFeatureType featureType = dataStore.getSchema(dataStore.getTypeNames()[0]);
CoordinateReferenceSystem crs = featureType.getGeometryDescriptor().getCoordinateReferenceSystem();
System.out.println("坐标参考系统:" + crs);
// 获取属性名
List<String> attributeNames = featureType.getAttributeDescriptors().stream().map(attr -> attr.getLocalName())
.collect(Collectors.toList());
System.out.println("Attributes: " + attributeNames);
System.out.println("以下是属性信息的深度解析:----------------------------------------");
List<AttributeDescriptor> attrDescList = featureType.getAttributeDescriptors();
System.out.println(attrDescList.size());
for (AttributeDescriptor attrDesc : attrDescList) {
System.out.println(attrDesc);
System.out.println("属性详情:");
AttributeType attrType = attrDesc.getType();
System.out.println("userData=========>" + attrType.getUserData());
System.out.println("数据库数据类型:" + convertShpFieldType2H2GISOrPG(attrType.getBinding()));
System.out.println(attrType);
System.out.println("name=" + attrDesc.getName() + "\tLocalName=" + attrDesc.getLocalName() + "\t"
+ attrDesc.getMaxOccurs() + "\t" + attrDesc.getMinOccurs());
List<Filter> restrictions = attrType.getRestrictions();
System.out.println("过滤器为 : " + restrictions);
for (Filter filter : restrictions) {
if (filter instanceof IsLessThenOrEqualToImpl) {
IsLessThenOrEqualToImpl impl = (IsLessThenOrEqualToImpl) filter;
Expression exp1 = impl.getExpression1();
Expression exp2 = impl.getExpression2();
System.out.println(exp1.getClass());
System.out.println(exp2.getClass());
if (exp1 instanceof LengthFunction && exp2 instanceof LiteralExpressionImpl) {
LengthFunction length = (LengthFunction) exp1;
LiteralExpressionImpl literal = (LiteralExpressionImpl) exp2;
System.out.println( length.getName() + ":\t" + literal.getValue());
}
}
}
System.out.println("**************************************************");
}
}
可以看到,这里关于数据的属性列表信息,尤其是获取字段的长度和精度时,是通过在过滤器中的具体子类来实现的。在Filter列表中,一定会用到两个表达式对象,其中的一个是:IsLessThenOrEqualToImpl,另一个则是LiteralExpressionImpl,通过这两个类来实现长度的控制。因此如果需要在代码中获取具体的长度,可以通过这两个表达式对象来获取。同时,在进行数据的解析时,虽然我们在前面对两者进行了简单的类比,但毕竟两者不是一个东西,因此在进行数据的存储时,具体的设置还是有一定的区分的,比如对应的具体数据类型,这里我们来提供一个类来进行转换。
/**
* convert shpFileType to db field type
** 备注:目前arcgis的字段类型有:短整型 长整型 浮点型 双精度 文本 日期
* @param value
* @return
*/
public static String convertShpFieldType2H2GISOrPG(Class<?> value) throws Exception {
if (value == String.class) {//文本
return "varchar";
}
if (value == Integer.class) {//短整型
return "int";
}
if (value == Long.class) {//长整型
return "bigint";
}
if (value == Double.class || value == Float.class) {//浮点型 双精度 保留精度,比如在金币上运算更安全
return "numeric";
}
if (value == Date.class) {
return "TIMESTAMP WITH TIME ZONE ";//日期, 使用包含时区的来处理
}
if (Geometry.class.isAssignableFrom(value)) {
return "geometry";
}
//除了上述,真不知道还会有啥了。除非arcgis的shp又增加了新类型?!那无能为力了,抛出异常吧
throw new Exception("不支持的类型!" + value.getName());
//
//if (value.getSuperclass().getName().equals(String.class))
// return null;
}
在GeoTools中,关于Filter的相关类,非常的庞大,这里不全部展开,以防止抓不住重点。先给一个Filter的类图给朋友们,感兴趣的可以先了解一下。
通过以上的步骤和示例代码,我们可以在控制台中输出以下信息:
为了方便展示字段的解析,这里特意将复制一段处理的输出信息:
AttributeDescriptorImpl WGS84_纬 <Double:Double> nillable 0:1
属性详情:
userData=========>{}
数据库数据类型:numeric
AttributeTypeImpl Double<Double>
restrictions=[ length([.]) <= 19 ]
name=WGS84_纬 LocalName=WGS84_纬 1 0
过滤器为 : [[ length([.]) <= 19 ]]
class org.geotools.filter.LengthFunction
class org.geotools.filter.LiteralExpressionImpl
length: 19
可以看到,通过上述的代码就可以获取属性表格的字段名、字段类型、字段的长度等信息,有了这些信息,我们其实可以做的事情很多了。不知道大家发现一个问题没有,在上述的代码中,并没有返回精度,即11位小数的长度。上述代码是无法实现的。那么有没有什么办法来实现精度值的获取呢。
2、基于dbf文件的属性信息读取
众所周知,在shapefile文件夹中,除了shp文件之外,另外一个prj和dbf文件也是缺一不可的。既然我们无法通过读取shp来实现精度的精确读取,那么是否可以从这两个文件当中来获取呢?shp文件有我们定义好的空间信息,而大量的属性表格,我们是存放在dbf中,因此我们来测试读取dbf文件看能否找到另外的一条大路。
幸运的是,在Geotools当中,官方提供了DbaseFileReader这个类,通过这个类实现对dbf文件的读取,之前也查找了一些相关的资料,但是很少有博主写这种实现模式。这里提供基于DbaseFileReader的实现方式,代码实现更简单,性能更好。其原理非常简单,通过DbaseFileReader来获取DbaseFileHeader,然后就可以遍历DbaseFileHeader中的信息,就可以实现所有信息的识别。代码如下:
/**
* - 读取dbf文件
* @throws IOException
*/
@Test
public void testReadDbf() throws IOException {
File dbfFile = new File("C:/BaiduDownload/长沙市2020年POI数据集/长沙市2020年POI数据集/长沙POI数据(.shp)/风景名胜.shp");
// 打开 DBF 文件
ShpFiles shpFile = new ShpFiles(dbfFile);
System.out.println(Charset.defaultCharset().toString());
DbaseFileReader dbfReader = new DbaseFileReader(shpFile, true, Charset.defaultCharset());
// 读取 DBF 文件的头信息
DbaseFileHeader header = dbfReader.getHeader();
System.out.println(header.getNumFields());
System.out.println(header.getNumRecords());
for (int i = 0; i < header.getNumFields(); i++) {
System.out.println(header.getFieldName(i) + "\t" + header.getFieldLength(i) + "\t" + header.getFieldType(i)
+ "\t" + header.getFieldDecimalCount(i));
}
// 关闭 DBF 文件读取器
dbfReader.close();
}
这个类的方法如下:
Modifier and Type | Method | Description |
---|---|---|
void | close() | Clean up all resources associated with this reader.Highly recomended. |
protected int | fill(ByteBuffer buffer, ReadableByteChannel channel) | |
DbaseFileHeader | getHeader() | Get the header from this file. |
boolean | hasNext() | Query the reader as to whether there is another record. |
String | id() | An id for the reader. |
static void | main(String[] args) | |
void | read() | Reads the next record into memory. |
Object[] | readEntry() | Get the next record (entry). |
Object[] | readEntry(Object[] entry) | Copy the next entry into the array. |
Object[] | readEntry(Object[] entry, int offset) | Copy the next record into the array starting at offset. |
Object | readField(int fieldNum) | Reads a single field from the current record and returns it. |
DbaseFileReader.Row | readRow() | |
void | skip() | Skip the next record. |
void | transferTo(DbaseFileWriter writer) | Transfer, by bytes, the next record to the writer. |
想要获取属性就是通过获取Head对象即可实现属性列表的深度解析。返回DbaseFileHead,它的方法如下表所示:
Modifier and Type | Method | Description |
---|---|---|
void | addColumn(String inFieldName, char inFieldType, int inFieldLength, int inDecimalCount) | Add a column to this DbaseFileHeader. |
Class<?> | getFieldClass(int i) | Determine the most appropriate Java Class for representing the data in the field. |
int | getFieldDecimalCount(int inIndex) | Get the decimal count of this field. |
int | getFieldLength(int inIndex) | Returns the field length in bytes. |
String | getFieldName(int inIndex) | Get the field name. |
char | getFieldType(int inIndex) | Get the character class of the field. |
int | getHeaderLength() | Get the length of the header |
int | getLargestFieldSize() | Get the largest field size of this table. |
Date | getLastUpdateDate() | Get the date this file was last updated. |
long | getLengthForRecords(int records) | Returns the expected file size for the given number of records in the file |
int | getNumFields() | Return the number of fields in the records. |
int | getNumRecords() | Return the number of records in the file |
int | getRecordLength() | Get the length of the records in bytes. |
void | readHeader(ByteBuffer in) | Read the header data from the DBF file. |
void | readHeader(ReadableByteChannel channel) | Read the header data from the DBF file. |
int | removeColumn(String inFieldName) | Remove a column from this DbaseFileHeader. |
void | setNumRecords(int inNumRecords) | Set the number of records in the file |
String | toString() | Get a simple representation of this header. |
void | writeHeader(WritableByteChannel out) | Write the header data to the DBF file. |
在Heade对象中,要想获取字段的长度和精度,主要是使用下列两个方法,分别是:getFieldLength(index)和getFieldDecimalCount(index)。在IDE中运行以上程序后,可以在控制台中看到如下输出:
名称 254 C 0
大类 254 C 0
中类 254 C 0
小类 254 C 0
地址 254 C 0
省 254 C 0
市 254 C 0
区 254 C 0
WGS84_经 19 F 11
WGS84_纬 19 F 11
可以看到,这里不仅输出了数据的长度,同时还输出了数据的精度。到此,使用第二种方式来进行dbf文件的解析已经成功实现。
三、总结
以上就是本文的主要内容,本文则重点讲解在Java当中,如何使用GeoTools来进行属性表格的解析,首先在Qgis中重点介绍属性表格的字段信息,然后介绍一种常见的属性字段信息解析方式,其次介绍基于dbf的属性信息解析方法,直接解析dbf文件的方法在很多网络知识中很少见,但是却非常有用,比如在获取double类型的数值时,想获取精度值,就可以通过这种方式来获取。学习本文,不仅更加熟悉GeoTools对属性表格的解析,同时对于如何快速的将Shapefile的属性模型映射成空间库模型有更进一步的认识。行文仓促,难免有许多不足之处,如有不足还请各位专家博主在评论区留言指出,不胜感激。