目录
前言
停更了接近一个月都在研究一门新语言golang,以及如何利用golang完成一个webgis后端平台的开发。go语言作为一门强类型语言,在web后端开发中目前有高性能、语法简洁、编译速度快、自带高并发的特性,它既没有C/C++这样有着复杂,冗余的语法,又拥有一些弱类型语言的特性,比如自带垃圾回收机制,自带变量类型判断,关系是golang的打包是所有语言中最为先进的,编译器只会打包引入的代码,而不是想java,python一样会将导入的库整体打包。同样一个项目,用go打包出来可能只有几M,而用python打包出来会有几十上百兆。其运行速度和Java不相上下,部分计算还快过java,但是占用的内存比java小太多。不得不说golang是一门伟大的语言,大名鼎鼎的容器dock,yarn都是go语言写出来的。
但是golang的缺点也很明显,就是生态不够完善,参考资料太少。很多功能都没有现成的需要自己手写一一实现,并且golang的各种类库的作者也是非常随意,各种变量,函数想改就改,让使用者门非常苦恼,只有按个看源码来学习功能。
这篇文章主要介绍如何用golang结合postgis数据库,使用gin、grom框架实现后端的MVC的接口搭建。
一、整体思路
一个健全的webgis后端必须实现以下几个功能:
1、postgis数据库和model的绑定。
2、如何将pg库中的要素转换为geojson
3、将前端传入的geojson储存到数据库
4、动态矢量瓦片的实现
5、实现地图数据几何分析功能
6、完成各类复杂业务的分析模型
二、实现步骤
1.postgis数据库和model的绑定
参考grom库的官方文档https://gorm.io/zh_CN/docs/create.html
grom是golang对数据库实现orm操作的第三方库,在github上面获得了广泛的好评,目前支持MySQL, PostgreSQL, SQLite, SQL Server 和 TiDB这几种数据库。
首先在项目中创建一个model的包,并在这个包中定义你需要的数据库映射表。我们设计了几个字段,其中geom为几何要素字段,MultiPolygon为几何要素类型,4326为坐标系
package models
type MyTable struct {
ID uint `gorm:"primary_key"`
Name string `gorm:"type:varchar(255)"`
Bh string `gorm:"type:varchar(255)"`
Geom string `gorm:"type:geometry(MultiPolygon,4326)"`
}
创建一个core.go 存储数据库链接信息,创建一个全局变量DB,注意在go中全局变量首字母名必须为大写
package models
import (
"fmt"
"gorm.io/driver/postgres"
"gorm.io/gorm"
)
var DB *gorm.DB
var err error
func init() {
dsn := "host=localhost user=postgres password=1 dbname=gotest port=5432 sslmode=disable TimeZone=Asia/Shanghai"
DB, err = gorm.Open(postgres.Open(dsn), &gorm.Config{})
if err != nil {
fmt.Println(err)
}
}
2.将pg库中的要素转换为geojson
(1)几何定义
在pg数据库中,几何信息都是通过wkb格式进行存储,所以我们需要将wkb解析为golang中我们可以操作的几何对象,那么在解析wkb之前我们需要定义好全部的几何要素
1、定义点类型,直接用两位浮点切片定义,如果需要z值就三位,我这里目前只需要二维数据就定义的2维。
type Point [2]float64
2、定义线类型,线类型由点类型组成
type LineString []Point
3、定义环类型,该类型主要在存在环岛面的时候使用,环类型的特点就是起点和终点坐标一致
type Ring LineString
4、定义单面类型,该面由环类型切片构成,我们还可以给在该类型中定义一些简单的几何函数
type Polygon []Ring
//判断两个面是否相等
func (p Polygon) Equal(polygon Polygon) bool {
if len(p) != len(polygon) {
return false
}
for i := range p {
if !p[i].Equal(polygon[i]) {
return false
}
}
return true
}
//复制面
func (p Polygon) Clone() Polygon {
if p == nil {
return p
}
np := make(Polygon, 0, len(p))
for _, r := range p {
np = append(np, r.Clone())
}
return np
}
5、定义聚合面MultiPolygon,该要素又面的切片构成
type MultiPolygon []Polygon
6、做一个几何类,整合所有几何要素,在golang中这个被叫做接口
type Geometry interface {
GeoJSONType() string
Dimensions() int
Bound() Bound
}
(2)将wkb解析为几何类型
wkb作为一种开源的二进制格式,存储几何信息具备高性能,占用空间小等特点。WKB编码包括两部分:类型及坐标信息, 类型部分用一个字节表示,其中前四位表示几何类型,后四位表示SRID(空间参考系统编号),坐标信息根据不同几何类型分别编码,如点的坐标用x,y两个double类型表示,线的坐标是一系列的点坐标等。以下代码实现了将wkb转换为上面定义的几何要素。因为每个几何类型的解析方式不一样这里我将每种方式单独做成了函数。
func Unmarshal(data []byte) (Geometry, int, error) {
order, typ, srid, geomData, err := unmarshalByteOrderType(data)
if err != nil {
return nil, 0, err
}
var g Geometry
switch typ {
case pointType:
g, err = unmarshalPoint(order, geomData)
case multiPointType:
g, err = unmarshalMultiPoint(order, geomData)
case lineStringType:
g, err = unmarshalLineString(order, geomData)
case multiLineStringType:
g, err = unmarshalMultiLineString(order, geomData)
case polygonType:
g, err = unmarshalPolygon(order, geomData)
case multiPolygonType:
g, err = unmarshalMultiPolygon(order, geomData)
case geometryCollectionType:
g, _, err := NewDecoder(bytes.NewReader(data)).Decode()
if err == io.EOF || err == io.ErrUnexpectedEOF {
return nil, 0, ErrNotWKB
}
return g, srid, err
default:
return nil, 0, ErrUnsupportedGeometry
}
if err != nil {
return nil, 0, err
}
return g, srid, nil
}
以下是解析MultiPolygon的代码
func readMultiPolygon(r io.Reader, order byteOrder, buf []byte) (geo.MultiPolygon, error) {
num, err := readUint32(r, order, buf[:4])
if err != nil {
return nil, err
}
alloc := num
if alloc > MaxMultiAlloc {
alloc = MaxMultiAlloc
}
result := make(orb.MultiPolygon, 0, alloc)
for i := 0; i < int(num); i++ {
pOrder, typ, _, err := readByteOrderType(r, buf)
if err != nil {
return nil, err
}
if typ != polygonType {
return nil, errors.New("面要素错误")
}
p, err := readPolygon(r, pOrder, buf)
if err != nil {
return nil, err
}
result = append(result, p)
}
return result, nil
}
(3)定义geojson类型
先定义几种基础的结构体
//定义Feature 结构体
type Feature struct {
ID interface{} `json:"id,omitempty"`
Type string `json:"type"`
BBox BBox `json:"bbox,omitempty"`
Geometry geo.Geometry `json:"geometry"` //这里为上一步定义的几何类型
Properties Properties `json:"properties"` //这里为空map类型
}
//定义FeatureCollection 结构体
type FeatureCollection struct {
Type string `json:"type"`
BBox BBox `json:"bbox,omitempty"`
Features []*Feature `json:"features"`
}
(4)数据转换
先将grom查询到的数据库对象传递到该函数,通过reflect映射字段,再将字段信息转换为properties的map对象,最后组装geojson返回
func Makegeojson(myTables []models.MyTable) interface{} {
var FeaturesList []*geojson.Feature
FeaturesList = []*geojson.Feature{}
for _, t := range myTables {
properties := make(map[string]interface{})
v := reflect.ValueOf(t)
tt := reflect.TypeOf(t)
for i := 0; i < v.NumField(); i++ {
if tt.Field(i).Name != "Geom" {
properties[strings.ToLower(tt.Field(i).Name)] = v.Field(i).Interface()
}
}
wkbBytes, _ := hex.DecodeString(strings.Trim(t.Geom, " "))
geom, _ := wkb.Unmarshal(wkbBytes)
feature := geojson.NewFeature(geom)
feature.Properties = properties
FeaturesList = append(FeaturesList, feature)
}
features := geojson.NewFeatureCollection()
features.Features = FeaturesList
GeoJSON, _ := json.Marshal(features)
var obj interface{}
json.Unmarshal(GeoJSON, &obj)
return obj
}
(5)数据返回
type UserController struct{}
func (uc *UserController) OutGeo(c *gin.Context) {
name := c.PostForm("name")
var mytable []models.MyTable
DB := models.DB
DB.Where("Name = ?", name).Find(&mytable)
data := methods.Makegeojson(mytable)
c.JSON(http.StatusOK, data)
}
通过postman调接口,数据完美返回geojson
2.前端传入的geojson储存到数据库
这一步其实和取是一样的,只需要把思路反过来,将geojson解析为我们定义的几何结构,然后再将几何结构解析成wkb。直接上代码。
几何要素转换为wkb
func Marshal(geom geo.Geometry, byteOrder ...binary.ByteOrder) ([]byte, error) {
buf := bytes.NewBuffer(make([]byte, 0, wkbcommon.GeomLength(geom, false)))
e := NewEncoder(buf)
if len(byteOrder) > 0 {
e.SetByteOrder(byteOrder[0])
}
err := e.Encode(geom)
if err != nil {
return nil, err
}
if buf.Len() == 0 {
return nil, nil
}
return buf.Bytes(), nil
}
func GeoJsonToWKB(geo geojson.Feature) string {
TempWkb, _ := wkb.Marshal(geo.Geometry)
WkbHex := hex.EncodeToString(TempWkb)
return WkbHex
}
完成数据存储
func (uc *UserController) InGeo(c *gin.Context) {
var jsonData geojson.FeatureCollection
c.BindJSON(&jsonData)
DB := models.DB
for _, t := range jsonData.Features {
wkb_result := methods.GeoJsonToWKB(*t)
DB.Model(models.MyTable{}).Create(map[string]interface{}{
"Bh": t.Properties["bh"],
"Name": t.Properties["name"],
"geom": clause.Expr{SQL: "ST_GeomFromWKB(decode(?, 'hex'))", Vars: []interface{}{wkb_result}},
})
}
c.JSON(http.StatusOK, "ok")
}
至于更新功能也是一样的,几何更新只需要更新geom字段就行了。
3、其他功能实现
动态矢量瓦片直接用go语言重写我之前博客用python做的那部分即可,至于复杂的地理数据分析可以采用postgis函数实现,复杂的业务分析模块可以直接使用golang调用fme实现,这里就不过多介绍,后期博客会更新相关内容。
总结
golang实在是太COOL了,语法简洁,运行高效,部署简单,打包完美,我愿称之为python之后最好用的语言,唯一的缺陷就是生态还有所欠缺,不过随着开发者们的拥护,我相信golang会有光明的未来。