环境:
参考:《mysql:11.4 Spatial Data Types》
ogc定义的地理空间数据格式。可以存储经纬度地理坐标、空间点、线、面、体等数据。
具体参考:《ogc:OGC Standards》。
wkt和wkb
WKT(well-known text)是一种文本标记语言,该格式由开放地理空间联盟(OGC)制定,用于表示矢量数据中的几何对象,在数据传输与数据库存储时,常用到它的二进制形式,即WKB(well-known binary)。WKT与WKB在GIS中的重要作用在于,他们能利用文本简洁明了的表达矢量空间要素的几何信息,使得几何信息能以字段的形式存储于数据库中。
WKT相比WKB更方便人们理解,具有很高的可读性。
注意:wkt和wkb中均不存储坐标系信息,它们内部只认X、Y。
关于它们定义可以参考:
《博文:2.3. WKT 描述的几何对象》
《博文:2.4. WKB 描述的几何对象》
创建表和插入数据
create table test(
t_geometry geometry,
t_point point SRID 4326,
-- t_curve curve, -- 报错: 不支持
t_linestring linestring SRID 4326,
-- t_line line -- 报错: 不支持
-- t_LinearRing LinearRing -- 报错: 不支持
-- t_Surface Surface -- 报错: 不支持
t_Polygon Polygon SRID 4326,
t_GeometryCollection GeometryCollection,
t_MultiPoint MultiPoint SRID 4326,
-- t_MultiCurve MultiCurve -- 报错: 不支持
t_MultiLineString MultiLineString SRID 4326,
-- t_MultiSurface MultiSurface -- 报错: 不支持
t_MultiPolygon MultiPolygon
)
insert into test(t_geometry) values(ST_GeomFromText('POINT(15 20)'));
insert into test(t_point) values(ST_GeomFromText('POINT(15 20)',4326));
insert into test(t_linestring) values(ST_GeomFromText('LINESTRING(0 0, 10 10, 20 25, 50 60)',4326));
insert into test(t_Polygon) values(ST_GeomFromText('POLYGON((0 0,10 0,10 10,0 10,0 0),(5 5,7 5,7 7,5 7, 5 5))',4326));
insert into test(t_GeometryCollection) values(GEOMETRYCOLLECTION(ST_GeomFromText('POINT(10 10)'), ST_GeomFromText('POINT(30 30)')));
insert into test(t_MultiPoint) values(ST_GeomFromText('MULTIPOINT(0 0, 20 20, 60 60)',4326));
insert into test(t_MultiLineString) values(ST_GeomFromText('MULTILINESTRING((10 10, 20 20), (15 15, 30 15))',4326));
insert into test(t_MultiPolygon) values(ST_GeomFromText('MULTIPOLYGON(((0 0,10 0,10 10,0 10,0 0)),((5 5,7 5,7 7,5 7, 5 5)))',4326));
SELECT
ST_AsText(t_geometry),
ST_AsText(t_point),
ST_AsText(t_linestring),
ST_AsText(t_Polygon),
ST_AsText(t_GeometryCollection),
ST_AsText(t_MultiPoint),
ST_AsText(t_MultiLineString),
ST_AsText(t_MultiPolygon)
FROM test;
select c.TABLE_SCHEMA ,c.TABLE_NAME ,c.COLUMN_NAME ,c.COLUMN_TYPE ,c.SRS_ID
from information_schema.`COLUMNS` c where c.TABLE_SCHEMA ='test' and c.TABLE_NAME ='test'
当我们需要存储地理数据时是需要指定坐标系的,如果我们仅存储几何数据就不用了。
比如:
那么,我们定义列的时候应该怎么抉择?
如果我们能知道存储地理位置的坐标,那么我们应该在定义时声明它的坐标系,声明后mysql将支持更多的地理操作,否则,很难支持!!!
另外,当我们声明列的坐标系后,我们在插入数据时也要声明数据的坐标系,不声明或声明的与定义的不一致都会报错,如下:
create table test(
id int auto_increment primary key,
t_geometry geometry,
t_point point srid 4326
)
-- 正常: 定义时没有坐标系,可以插入任意坐标系数据
insert into test(t_geometry) values(ST_GeomFromText('POINT(15 20)',4326));
-- 正常:定义时没有坐标系,插入时也可以不指定坐标系
insert into test(t_geometry) values(ST_GeomFromText('POINT(15 20)'));-- 正常
-- 正常:定义时声明坐标系(4326),插入4326的坐标数据没毛病
insert into test(t_point) values(ST_GeomFromText('POINT(15 20)',4326));
-- 报错:定义时声明坐标系(4326),插入4336的坐标数据会报错
-- insert into test(t_point) values(ST_GeomFromText('POINT(15 20)',4336));
-- 报错:定义时声明坐标系(4326),插入时也必须显示声明4326
-- insert into test(t_point) values(ST_GeomFromText('POINT(15 20)'));
下面是mysql文档中关于wkb标准的介绍
mysql在实现的时候仅实现了7中类型的结构(Point, LineString, Polygon, MultiPoint, MultiLineString, MultiPolygon and GeometryCollection),其实,wkb标准还有很多格式,如:
mysql内部存储数据的时候在wkb标准前增加4个字节表示坐标系。
参考:
《博文:mysql 8.x 中的 geometry 变更/缓存命中/SRID/EPSG/WKT等问题》
《Axis Order Confusion》
在使用wkt的时候,我经常迷惑,到底是 POINT (113.720512 34.773274)
还是POINT (34.773274 113.720512 )
?
首先,我们明确,wkt和wkb它们都不存储地理坐标系,它们也不认什么经纬度,它们只认XY,所以对wkt和wkb来说,怎么存都一样。
其次,在先后顺序上并不是那么统一,有的软件是精度在前,有的是维度在前。
其实,到底应该哪个在前,地理坐标系中有定义,如:4326的定义(http://epsg.io/4326):
所以,mysql中默认认为4326坐标系下的wkt应该是POINT (34.773274 113.720512 )
(但注意,mysql中允许我们手动指定它的顺序)。
注意,mysql存储的时候是将精度(long)存到X的位置,将维度(lat)存储到Y的位置。
看下面的sql:
-- 默认4326坐标系: point(lat long) 存储: long -> X, lat -> Y
insert into test(t_geometry) values(ST_GeomFromText('POINT (34.773274 113.720512)',4326));
-- 声明wkt中的顺序: point(long lat) 存储: long -> X, lat -> Y
insert into test(t_geometry) values(ST_GeomFromText('POINT (113.720512 34.773274)',4326,'axis-order=long-lat'));
-- 上面两条数据存储到数据库是一样的,不过后者不声明顺序的话会报错,因为"维度113.720512"超范围了(-90.90)
默认情况,我们用程序读出来的地理数据是mysql内部的二进制格式,如果我们需要用专业的地理软件处理应该怎么样呢?
直接看示例:
//mysql中的存储: srid=4326,type:point,x:113.720512,y:34.773274
//mysql中的16进制: "E6100000010100000009185DDE1C6E5C406E6C76A4FA624140";
//移除前4个字节
var hex = "010100000009185DDE1C6E5C406E6C76A4FA624140";
var bs = BytesToHexConverter.FromHex(hex);
var reader = new NetTopologySuite.IO.WKBReader(new NtsGeometryServices(PrecisionModel.Floating.Value, 4326));
var point = reader.Read(bs) as Point;
var wkt = point.AsText();
//输出: srid=4326,wkt=POINT (113.720512 34.773274)
Console.WriteLine($"srid={point.SRID},wkt={wkt}");
你可能注意到,NetTopologySuite
在输出wkt的时候怎么不是POINT (34.773274 113.720512)
,4326的坐标系中不是定义的维度在前吗?
所以说,到底哪个在前在不同软件中还是有区别的?
public static void Main()
{
ReadMySqlGeometryToBytes();
//ReadMySqlWKBGeometry("0000000001010000000000000000002E400000000000003440");
//ReadMySqlWKBGeometry("E6100000010100000000000000000034400000000000002E40");
//ReadMySqlWKBGeometry("E6100000010100000009185DDE1C6E5C406E6C76A4FA624140");
Console.WriteLine("ok");
}
public static void ReadMySqlGeometryToBytes()
{
var connString = "Server=127.0.0.1;Database=test;Uid=root;Pwd=123456;";
var conn = new MySqlConnector.MySqlConnection(connString);
conn.Open();
var cmd = conn.CreateCommand();
cmd.CommandText = "drop table if exists test";
cmd.ExecuteNonQuery();
cmd.CommandText = @"create table test(
id int auto_increment primary key,
t_geometry geometry
)";
cmd.ExecuteNonQuery();
cmd.CommandText = "insert into test(t_geometry) values(ST_GeomFromText('POINT(15 20)'));";
cmd.ExecuteNonQuery();
cmd.CommandText = "insert into test(t_geometry) values(ST_GeomFromText('POINT(15 20)',4326));";
cmd.ExecuteNonQuery();
cmd.CommandText = "insert into test(t_geometry) values(ST_GeomFromText('point(34.773274 113.720512)',4326));";
cmd.ExecuteNonQuery();
cmd.CommandText = "insert into test(t_geometry) values(ST_GeomFromText('point(113.720512 34.773274)',4326,'axis-order=long-lat'));";
cmd.ExecuteNonQuery();
cmd.CommandText = @"
select id,t_geometry,
st_astext(t_geometry) as stastext,
st_astext(t_geometry,'axis-order=long-lat') as sttextlonglat,
st_astext(t_geometry,'axis-order=lat-long') as sttextlatlong
from test
";
var reader = cmd.ExecuteReader();
while (reader.Read())
{
//id
var id = (int)reader[0];
//t_geometry二进制
var t_geometry = (byte[])reader[1];
var hex = Convert.ToHexString(t_geometry);
//st_astext(t_geometry) as stastext,
var stastext = reader[2].ToString();
//st_astext(t_geometry,'axis-order=long-lat') 强制精度在前
var sttextlonglat = reader[3].ToString();
//st_astext(t_geometry,'axis-order=lat-long') 强制维度在前
var sttextlatlong = reader[4].ToString();
Console.WriteLine($"id={id},t_geometry={hex},stastext={stastext},sttextlonglat={sttextlonglat},sttextlatlong={sttextlatlong}");
}
reader.Close();
conn.Close();
Console.WriteLine("ok");
Console.ReadLine();
}
public static void ReadMySqlWKBGeometry(string hex)
{
var bs = BytesToHexConverter.FromHex(hex);
var actBs = new byte[bs.Length - 4];
Array.Copy(bs, 4, actBs, 0, bs.Length - 4);
int srid = 0;
int byteOrder = 0;
int geometryType = 0;
double x = 0;
double y = 0;
unsafe
{
//bs[0-3] srid
fixed (byte* ptr = &bs[0])
{
int* p = (int*)ptr;
srid = p[0];
}
//bs[4] 字节序Byte order
byteOrder = bs[4];
//bs[5-8] WKB type
fixed (byte* ptr = &bs[5])
{
int* p = (int*)ptr;
geometryType = p[0];
}
//bs[9-16] x
fixed (byte* ptr = &bs[9])
{
double* p = (double*)ptr;
x = p[0];
}
//bs[17-24] y
fixed (byte* ptr = &bs[17])
{
double* p = (double*)ptr;
y = p[0];
}
}
Console.WriteLine($"--------------源数据: {hex}------------");
Console.WriteLine($"自行读取结果: srid={srid},字节序={byteOrder},wkbtype={geometryType},x={x},y={y}");
var reader = new NetTopologySuite.IO.WKBReader();
var point = reader.Read(actBs) as Point;
//tx.SRID = srid;
var wkt = point!.AsText();
Console.WriteLine($"NetTopologySuite 读取结果: srid={point.SRID},wkbtype={point.GeometryType},wkt={wkt}");
}