初始
This commit is contained in:
19
api/v1/common/captcha.go
Normal file
19
api/v1/common/captcha.go
Normal file
@ -0,0 +1,19 @@
|
||||
/*
|
||||
* @desc:验证码参数
|
||||
* @company:云南奇讯科技有限公司
|
||||
* @Author: yixiaohu
|
||||
* @Date: 2022/3/2 17:47
|
||||
*/
|
||||
|
||||
package common
|
||||
|
||||
import "github.com/gogf/gf/v2/frame/g"
|
||||
|
||||
type CaptchaReq struct {
|
||||
g.Meta `path:"/get" tags:"验证码" method:"get" summary:"获取验证码"`
|
||||
}
|
||||
type CaptchaRes struct {
|
||||
g.Meta `mime:"application/json"`
|
||||
Key string `json:"key"`
|
||||
Img string `json:"img"`
|
||||
}
|
491
api/v1/common/coryCommon/baiduOCR.go
Normal file
491
api/v1/common/coryCommon/baiduOCR.go
Normal file
@ -0,0 +1,491 @@
|
||||
package coryCommon
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"github.com/gogf/gf/v2/net/gclient"
|
||||
"github.com/gogf/gf/v2/os/gctx"
|
||||
"github.com/tiger1103/gfast-cache/cache"
|
||||
commonService "github.com/tiger1103/gfast/v3/internal/app/common/service"
|
||||
"github.com/tiger1103/gfast/v3/library/liberr"
|
||||
tool "github.com/tiger1103/gfast/v3/utility/coryUtils"
|
||||
"golang.org/x/net/context"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
/**
|
||||
功能:
|
||||
文字识别:百度API识别身份证、银行卡内容、人脸检测、人脸对比
|
||||
*/
|
||||
|
||||
// OcrReq 请求体参数 身份证和银行卡都是此结构体,
|
||||
type OcrReq struct {
|
||||
Image string `json:"image"` // 二选一:图像数据,base64编码后进行urlencode,需去掉编码头(data:image/jpeg;base64, ) 要求base64编码和urlencode后大小不超过4M,最短边至少15px,最长边最大4096px,支持jpg/jpeg/png/bmp格式
|
||||
Url string `json:"url"` // 二选一:图片完整URL,URL长度不超过1024字节,URL对应的图片base64编码后大小不超过4M,最短边至少15px,最长边最大4096px,支持jpg/jpeg/png/bmp格式,当image字段存在时url字段失效 请注意关闭URL防盗链
|
||||
IdCardSide string `json:"id_card_side"` // 身份证需要此参数(人头面填写:front,国徽面填写:back) 银行卡不需要
|
||||
DetectPhoto bool `json:"detect_photo"` // 是否检测身份证进行裁剪,默认不检测。可选值:true-检测身份证并返回证照的 base64 编码及位置信息
|
||||
}
|
||||
|
||||
/*
|
||||
1、clientId 必须参数,应用的APIKey
|
||||
2、clientSecret 必须参数,应用的Secret Key;
|
||||
3、存在redis的数据前缀
|
||||
*/
|
||||
var clientId = ""
|
||||
var clientSecret = ""
|
||||
var cacheRedis = "zmGoBaiDuOcrAccessToken"
|
||||
|
||||
func init() {
|
||||
one, err := g.Cfg().Get(gctx.New(), "baiDuYun.clientId")
|
||||
if err != nil {
|
||||
fmt.Println("百度云API未找到!")
|
||||
}
|
||||
two, err := g.Cfg().Get(gctx.New(), "baiDuYun.clientSecret")
|
||||
if err != nil {
|
||||
fmt.Println("百度云Secret未找到!")
|
||||
}
|
||||
clientId = one.String()
|
||||
clientSecret = two.String()
|
||||
}
|
||||
|
||||
/*
|
||||
IDCardInfo 获取身份证相关数据
|
||||
*/
|
||||
type IDCardInfo struct {
|
||||
WordsResult WordsResult `json:"words_result"`
|
||||
WordsResultNum int `json:"words_result_num"`
|
||||
IDCardNumberType int `json:"idcard_number_type"`
|
||||
ImageStatus string `json:"image_status"`
|
||||
LogID int64 `json:"log_id"`
|
||||
Photo string `json:"photo"`
|
||||
}
|
||||
type WordsResult struct {
|
||||
Name Field `json:"姓名"`
|
||||
Nation Field `json:"民族"`
|
||||
Address Field `json:"住址"`
|
||||
CitizenIdentification Field `json:"公民身份号码"`
|
||||
Birth Field `json:"出生"`
|
||||
Gender Field `json:"性别"`
|
||||
ExpirationDate Field `json:"失效日期"`
|
||||
IssuingAuthority Field `json:"签发机关"`
|
||||
IssueDate Field `json:"签发日期"`
|
||||
}
|
||||
type Field struct {
|
||||
Location Location `json:"location"`
|
||||
Words string `json:"words"`
|
||||
}
|
||||
type Location struct {
|
||||
Top int `json:"top"`
|
||||
Left int `json:"left"`
|
||||
Width int `json:"width"`
|
||||
Height int `json:"height"`
|
||||
}
|
||||
|
||||
func ImgOCR(vr OcrReq) (m map[string]interface{}) {
|
||||
//请求路径+token
|
||||
baseUrl := "https://aip.baidubce.com/rest/2.0/ocr/v1/idcard"
|
||||
//先从缓存里面捞取token,如果没得就重新获取token
|
||||
var atStr = redisCacheStr()
|
||||
// 构造 URL 参数
|
||||
params := url.Values{}
|
||||
params.Set("access_token", atStr)
|
||||
// 构造完整的请求 URL
|
||||
requestURL := fmt.Sprintf("%s?%s", baseUrl, params.Encode())
|
||||
|
||||
response, err := gclient.New().ContentJson().ContentType("application/x-www-form-urlencoded").Post(gctx.New(), requestURL, vr)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
var dataInfo = strings.ReplaceAll(response.ReadAllString(), " ", "")
|
||||
|
||||
//解析数据
|
||||
bodyData := []byte(dataInfo)
|
||||
var idCardInfo IDCardInfo
|
||||
err = json.Unmarshal(bodyData, &idCardInfo)
|
||||
if err != nil {
|
||||
fmt.Println("Failed to parse JSON data:", err)
|
||||
return
|
||||
}
|
||||
m = make(map[string]interface{})
|
||||
//身份证正反面颠倒了,直接返回空
|
||||
if idCardInfo.ImageStatus == "reversed_side" {
|
||||
return
|
||||
}
|
||||
|
||||
result := idCardInfo.WordsResult
|
||||
//if idCardInfo.Photo != "" {
|
||||
// m["pacePhoto"] = idCardInfo.Photo
|
||||
//}
|
||||
if result.Name.Words != "" {
|
||||
m["userName"] = result.Name.Words
|
||||
}
|
||||
if result.Nation.Words != "" {
|
||||
m["sfzNation"] = result.Nation.Words
|
||||
}
|
||||
if result.Address.Words != "" {
|
||||
m["sfzSite"] = result.Address.Words
|
||||
}
|
||||
if result.CitizenIdentification.Words != "" {
|
||||
m["sfzNumber"] = result.CitizenIdentification.Words
|
||||
}
|
||||
if result.Birth.Words != "" {
|
||||
str, _ := tool.New().TimeCycle(result.Birth.Words)
|
||||
m["sfzBirth"] = str
|
||||
}
|
||||
if result.Gender.Words != "" {
|
||||
var se = result.Gender.Words
|
||||
if se == "男" {
|
||||
se = "1"
|
||||
} else if se == "女" {
|
||||
se = "2"
|
||||
} else {
|
||||
se = "3"
|
||||
}
|
||||
m["sex"] = se
|
||||
}
|
||||
if result.ExpirationDate.Words != "" {
|
||||
str, err := tool.New().TimeCycle(result.ExpirationDate.Words)
|
||||
if err == nil {
|
||||
m["sfzEnd"] = str
|
||||
} else {
|
||||
str := result.ExpirationDate.Words
|
||||
m["sfzEnd"] = str
|
||||
}
|
||||
}
|
||||
if result.IssuingAuthority.Words != "" {
|
||||
m["IssuingAuthority"] = result.IssuingAuthority.Words
|
||||
}
|
||||
if result.IssueDate.Words != "" {
|
||||
str, _ := tool.New().TimeCycle(result.IssueDate.Words)
|
||||
m["sfzStart"] = str
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
/*
|
||||
BankData 获取银行卡相关数据
|
||||
针对卡号、有效期、发卡行、卡片类型、持卡人5个关键字段进行结构化识别,识别准确率超过99%
|
||||
*/
|
||||
type BankData struct {
|
||||
ValidDate string `json:"valid_date"` //有效期
|
||||
BankCardNumber string `json:"bank_card_number"` //银行卡卡号
|
||||
BankName string `json:"bank_name"` //银行名,不能识别时为空
|
||||
BankCardType int `json:"bank_card_type"` //银行卡类型,0:不能识别; 1:借记卡; 2:贷记卡(原信用卡大部分为贷记卡); 3:准贷记卡; 4:预付费卡
|
||||
HolderName string `json:"holder_name"` //持卡人姓名,不能识别时为空
|
||||
}
|
||||
|
||||
type Result struct {
|
||||
Res BankData `json:"result"` //具体数据
|
||||
Direction int `json:"direction"` //图像方向。 - - 1:未定义; - 0:正向; - 1:逆时针90度; - 2:逆时针180度; - 3:逆时针270度
|
||||
LogID int64 `json:"log_id"` //请求标识码,随机数,唯一。
|
||||
}
|
||||
|
||||
func ImgYhkOCR(vr OcrReq) (m map[string]interface{}) {
|
||||
m = make(map[string]interface{})
|
||||
//请求路径+token
|
||||
baseUrl := "https://aip.baidubce.com/rest/2.0/ocr/v1/bankcard"
|
||||
//先从缓存里面捞取token
|
||||
var atStr = redisCacheStr()
|
||||
// 构造 URL 参数
|
||||
params := url.Values{}
|
||||
params.Set("access_token", atStr)
|
||||
// 构造完整的请求 URL
|
||||
requestURL := fmt.Sprintf("%s?%s", baseUrl, params.Encode())
|
||||
response, err := gclient.New().ContentJson().ContentType("application/x-www-form-urlencoded").Post(gctx.New(), requestURL, vr)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
//解析数据
|
||||
allString := response.ReadAllString()
|
||||
bodyData := []byte(allString)
|
||||
var result Result
|
||||
err = json.Unmarshal(bodyData, &result)
|
||||
if err != nil {
|
||||
fmt.Println("Failed to parse JSON data:", err)
|
||||
return
|
||||
}
|
||||
if result.Res.ValidDate != "" {
|
||||
m["ValidDate"] = result.Res.ValidDate
|
||||
}
|
||||
if result.Res.BankCardNumber != "" {
|
||||
m["yhkNumber"] = strings.ReplaceAll(result.Res.BankCardNumber, " ", "")
|
||||
}
|
||||
if result.Res.BankName != "" {
|
||||
m["yhkOpeningBank"] = result.Res.BankName
|
||||
}
|
||||
if result.Res.BankCardType >= 0 {
|
||||
m["BankCardType"] = result.Res.BankCardType
|
||||
}
|
||||
if result.Res.HolderName != "" {
|
||||
m["yhkCardholder"] = result.Res.HolderName
|
||||
}
|
||||
return m
|
||||
|
||||
}
|
||||
|
||||
/*
|
||||
HumanFaceReq 请求参数 人脸识别+人脸检测
|
||||
*/
|
||||
type HumanFaceReq struct {
|
||||
Image string `json:"image"` //图片此处填写base64
|
||||
ImageType string `json:"image_type" gf:"default:BASE64" ` //图片类型-BASE64-URL-BASE64(此封装固定用base64)
|
||||
FaceField string `json:"face_field" gf:"default:face_type,quality"` //包括age,expression,face_shape,gender,glasses,landmark,landmark150, quality,eye_status,emotion,face_type,mask,spoofing信息 逗号分隔. 默认只返回face_token、人脸框、概率和旋转角度
|
||||
}
|
||||
|
||||
/*
|
||||
HumanFaceRep 返回参数 人脸识别
|
||||
*/
|
||||
type HumanFaceRep struct {
|
||||
ErrorCode int `json:"error_code"`
|
||||
ErrorMsg string `json:"error_msg"`
|
||||
LogId int `json:"log_id"`
|
||||
Timestamp int `json:"timestamp"`
|
||||
Cached int `json:"cached"`
|
||||
Result struct {
|
||||
FaceNum int `json:"face_num"`
|
||||
FaceList []struct {
|
||||
FaceToken string `json:"face_token"`
|
||||
Location struct {
|
||||
Left float64 `json:"left"`
|
||||
Top float64 `json:"top"`
|
||||
Width int `json:"width"`
|
||||
Height int `json:"height"`
|
||||
Rotation int `json:"rotation"`
|
||||
} `json:"location"`
|
||||
FaceProbability float64 `json:"face_probability"`
|
||||
Angle struct {
|
||||
Yaw float64 `json:"yaw"`
|
||||
Pitch float64 `json:"pitch"`
|
||||
Roll float64 `json:"roll"`
|
||||
} `json:"angle"`
|
||||
FaceType struct {
|
||||
Type string `json:"type"`
|
||||
Probability float64 `json:"probability"`
|
||||
} `json:"face_type"`
|
||||
Quality struct {
|
||||
Occlusion struct {
|
||||
LeftEye float64 `json:"left_eye"`
|
||||
RightEye float64 `json:"right_eye"`
|
||||
Nose float64 `json:"nose"`
|
||||
Mouth float64 `json:"mouth"`
|
||||
LeftCheek float64 `json:"left_cheek"`
|
||||
RightCheek float64 `json:"right_cheek"`
|
||||
ChinContour float64 `json:"chin_contour"`
|
||||
} `json:"occlusion"`
|
||||
Blur float64 `json:"blur"`
|
||||
Illumination float64 `json:"illumination"`
|
||||
Completeness int64 `json:"completeness"`
|
||||
} `json:"quality"`
|
||||
} `json:"face_list"`
|
||||
} `json:"result"`
|
||||
}
|
||||
|
||||
func HumanFace(hf *HumanFaceReq) (err error) {
|
||||
ctx := gctx.New()
|
||||
err = g.Try(ctx, func(ctx context.Context) {
|
||||
err = nil
|
||||
//1、请求地址+token
|
||||
url := "https://aip.baidubce.com/rest/2.0/face/v3/detect?access_token=" + redisCacheStr()
|
||||
marshal, err := json.Marshal(hf)
|
||||
if err != nil {
|
||||
liberr.ErrIsNil(ctx, err)
|
||||
return
|
||||
}
|
||||
//3、准备请求
|
||||
payload := strings.NewReader(string(marshal))
|
||||
client := &http.Client{}
|
||||
req, err := http.NewRequest("POST", url, payload)
|
||||
if err != nil {
|
||||
liberr.ErrIsNil(ctx, err)
|
||||
return
|
||||
}
|
||||
//4、设置请求头
|
||||
req.Header.Add("Content-Type", "application/json")
|
||||
//5、发送请求
|
||||
res, err := client.Do(req)
|
||||
if err != nil {
|
||||
liberr.ErrIsNil(ctx, err)
|
||||
return
|
||||
}
|
||||
defer res.Body.Close()
|
||||
//6、返回数据
|
||||
body, err := io.ReadAll(res.Body)
|
||||
if err != nil {
|
||||
liberr.ErrIsNil(ctx, err)
|
||||
return
|
||||
}
|
||||
var aaa = body
|
||||
//解析数据
|
||||
var result HumanFaceRep
|
||||
err = json.Unmarshal(aaa, &result)
|
||||
if err != nil {
|
||||
liberr.ErrIsNil(ctx, err)
|
||||
return
|
||||
}
|
||||
if result.ErrorCode != 0 {
|
||||
if result.ErrorMsg == "pic not has face" {
|
||||
err = errors.New("这张照片没有人脸")
|
||||
liberr.ErrIsNil(ctx, err)
|
||||
} else {
|
||||
err = errors.New(result.ErrorMsg)
|
||||
liberr.ErrIsNil(ctx, err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
//1、人脸置信度,范围【0~1】,代表这是一张人脸的概率,0最小、1最大。其中返回0或1时,数据类型为Integer
|
||||
dataInfo := result.Result.FaceList[0]
|
||||
reliabilityOne := dataInfo.FaceProbability
|
||||
if reliabilityOne != 1.0 {
|
||||
err = errors.New("识别不清晰!")
|
||||
}
|
||||
//2、判断是否真是人脸 human: 真实人脸 cartoon: 卡通人脸
|
||||
reliabilityTwo := dataInfo.FaceType.Type
|
||||
if reliabilityTwo == "cartoon" {
|
||||
err = errors.New("请传入真实人脸!")
|
||||
} else {
|
||||
//判断可信度 置信度,范围0~1
|
||||
reliabilityThree := dataInfo.FaceType.Probability
|
||||
if reliabilityThree < 0.8 {
|
||||
err = errors.New("请勿化妆太过夸张!")
|
||||
}
|
||||
}
|
||||
//3、人脸模糊程度,范围[0~1],0表示清晰,1表示模糊
|
||||
reliabilityFour := dataInfo.Quality.Blur
|
||||
if reliabilityFour >= 0.1 {
|
||||
err = errors.New("人脸过于模糊!")
|
||||
}
|
||||
//4、光线太暗 0~255 值越大光线越好
|
||||
reliabilityFive := dataInfo.Quality.Illumination
|
||||
if reliabilityFive < 80.0 {
|
||||
err = errors.New("光线太暗!")
|
||||
}
|
||||
//5、人脸是否完整 1完整 0不完整
|
||||
reliabilitySix := dataInfo.Quality.Completeness
|
||||
if reliabilitySix != 1 {
|
||||
err = errors.New("请确定人脸在图框内!")
|
||||
}
|
||||
return
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// ComparisonRep 人脸检测返回数据
|
||||
type ComparisonRep struct {
|
||||
ErrorCode int `json:"error_code"`
|
||||
ErrorMsg string `json:"error_msg"`
|
||||
LogId int `json:"log_id"`
|
||||
Timestamp int `json:"timestamp"`
|
||||
Cached int `json:"cached"`
|
||||
Result struct {
|
||||
Score float64 `json:"score"` //人脸相似度得分,推荐阈值80分
|
||||
FaceList []struct {
|
||||
FaceToken string `json:"face_token"`
|
||||
} `json:"face_list"`
|
||||
} `json:"result"`
|
||||
}
|
||||
|
||||
func Comparison(arrObject []*HumanFaceReq) (score float64, err error) {
|
||||
//1、请求地址+token
|
||||
url := "https://aip.baidubce.com/rest/2.0/face/v3/match?access_token=" + redisCacheStr()
|
||||
//2、请求参数转字符串json
|
||||
marshal, err := json.Marshal(arrObject)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
payload := strings.NewReader(string(marshal))
|
||||
//3、准备post请求
|
||||
client := &http.Client{}
|
||||
req, err := http.NewRequest("POST", url, payload)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return
|
||||
}
|
||||
//4、设置请求头
|
||||
req.Header.Add("Content-Type", "application/json")
|
||||
//5、发送请求关闭连接
|
||||
res, err := client.Do(req)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return
|
||||
}
|
||||
defer res.Body.Close()
|
||||
//6、数据结果
|
||||
body, err := ioutil.ReadAll(res.Body)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return
|
||||
}
|
||||
//7、解析数据
|
||||
var result ComparisonRep
|
||||
err = json.Unmarshal(body, &result)
|
||||
if err != nil {
|
||||
fmt.Println("Failed to parse JSON data:", err)
|
||||
return
|
||||
}
|
||||
score = result.Result.Score
|
||||
return score, err
|
||||
}
|
||||
|
||||
/*
|
||||
AccessTokenResponse 获取Access_token,有效期(秒为单位,有效期30天);
|
||||
*/
|
||||
type AccessTokenResponse struct {
|
||||
AccessToken string `json:"access_token"`
|
||||
}
|
||||
|
||||
func AccessTokenFunc() (str string) {
|
||||
url := "https://aip.baidubce.com/oauth/2.0/token?client_id=" + clientId + "&client_secret=" + clientSecret + "&grant_type=client_credentials"
|
||||
payload := strings.NewReader(``)
|
||||
client := &http.Client{}
|
||||
req, err := http.NewRequest("POST", url, payload)
|
||||
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return
|
||||
}
|
||||
req.Header.Add("Content-Type", "application/json")
|
||||
req.Header.Add("Accept", "application/json")
|
||||
|
||||
res, err := client.Do(req)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return
|
||||
}
|
||||
defer res.Body.Close()
|
||||
|
||||
body, err := ioutil.ReadAll(res.Body)
|
||||
var tokenResponse AccessTokenResponse
|
||||
json.Unmarshal(body, &tokenResponse)
|
||||
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return
|
||||
}
|
||||
return tokenResponse.AccessToken
|
||||
}
|
||||
|
||||
// 缓存捞取token,如果没得就重新获取token
|
||||
func redisCacheStr() (atStr string) {
|
||||
atStr = ""
|
||||
ctx := gctx.New()
|
||||
prefix := g.Cfg().MustGet(ctx, "system.cache.prefix").String()
|
||||
gfCache := cache.New(prefix)
|
||||
at := commonService.Cache().Get(ctx, gfCache.CachePrefix+cacheRedis)
|
||||
if at == nil || at.String() == "" {
|
||||
atStr = AccessTokenFunc()
|
||||
//存储到redis,时间为29天
|
||||
commonService.Cache().Set(ctx, cacheRedis, atStr, time.Hour*24*29)
|
||||
} else {
|
||||
atStr = at.String()
|
||||
}
|
||||
//atStr = "24.c8814d3fc7961820f0e23ee9d80cf96c.2592000.1696671067.282335-38777216"
|
||||
return atStr
|
||||
}
|
104
api/v1/common/coryCommon/base64ToImg.go
Normal file
104
api/v1/common/coryCommon/base64ToImg.go
Normal file
@ -0,0 +1,104 @@
|
||||
package coryCommon
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/gogf/gf/v2/os/gfile"
|
||||
"io/ioutil"
|
||||
"math/rand"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Base64ToImgFunc 将base64转成图片保存在本地
|
||||
func Base64ToImgFunc(base64Str string, numTyoe string, cdPath string) (outputPath string, err error) {
|
||||
// 获取当前时间+随机数得到文件名
|
||||
currentTime := time.Now()
|
||||
timestamp := currentTime.UnixNano() / int64(time.Millisecond)
|
||||
randomNum := rand.Intn(1000)
|
||||
uniqueFileName := fmt.Sprintf("%d_%d", timestamp, randomNum)
|
||||
// 用户指定的本地文件路径
|
||||
//ynr := Ynr(Portrait + "/")
|
||||
ynr := Ynr(cdPath + "/")
|
||||
path := ynr + uniqueFileName + ".png"
|
||||
path = filepath.ToSlash(path)
|
||||
// Base64编码的图像字符串
|
||||
b64 := "data:image/png;"
|
||||
base64Image := ""
|
||||
if strings.Contains(base64Str, "base64,") { // 判断是否有【base64,】如果有就替换
|
||||
base64Image = strings.Replace(base64Str, base64Str[:strings.Index(base64Str, "base64,")], b64, 1)
|
||||
} else {
|
||||
base64Image = b64 + "base64," + base64Str
|
||||
}
|
||||
|
||||
// 调用函数将Base64图像保存到指定路径
|
||||
err = SaveBase64ImageToFile(base64Image, path)
|
||||
if err != nil {
|
||||
return
|
||||
} else {
|
||||
if numTyoe == "1" {
|
||||
outputPath = strings.Replace(path, "resource/public", "file", 1)
|
||||
return
|
||||
} else if numTyoe == "2" {
|
||||
outputPath = strings.Replace(path, "resource/public", "wxfile", 1)
|
||||
return
|
||||
} else {
|
||||
err = errors.New("第二参数只能为1 or 2!")
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func Base64ToFileFunc(base64Str string, filePath string, suffix string, numTyoe string) (outputPath string, err error) {
|
||||
|
||||
// 获取当前时间+随机数得到文件名
|
||||
timestamp := time.Now().UnixNano() / int64(time.Millisecond)
|
||||
uniqueFileName := fmt.Sprintf("%d_%d", timestamp, rand.Intn(1000))
|
||||
// 用户指定的本地文件路径,filePath路径最后必须是/
|
||||
path := filepath.ToSlash(filePath + uniqueFileName + suffix)
|
||||
// 调用函数将Base64图像保存到指定路径
|
||||
err = SaveBase64ImageToFile(base64Str, path)
|
||||
if err != nil {
|
||||
return
|
||||
} else {
|
||||
if numTyoe == "1" {
|
||||
outputPath = strings.Replace(path, "resource/public", "file", 1)
|
||||
return
|
||||
} else if numTyoe == "2" {
|
||||
outputPath = strings.Replace(path, "resource/public", "wxfile", 1)
|
||||
return
|
||||
} else {
|
||||
err = errors.New("第二参数只能为1 or 2!")
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// SaveBase64ImageToFile 将Base64编码的图像保存到指定的本地文件路径
|
||||
func SaveBase64ImageToFile(base64Image string, outputPath string) error {
|
||||
if len(outputPath) > 0 && outputPath[0] == '/' {
|
||||
outputPath = outputPath[1:]
|
||||
}
|
||||
getwd, _ := os.Getwd()
|
||||
outputPath = gfile.Join(getwd, outputPath)
|
||||
outputPath = strings.ReplaceAll(outputPath, "\\", "/")
|
||||
// 1. 解码Base64字符串
|
||||
parts := strings.Split(base64Image, ",")
|
||||
if len(parts) != 2 {
|
||||
return errors.New("Base64字符串格式不正确!")
|
||||
}
|
||||
data, err := base64.StdEncoding.DecodeString(parts[1])
|
||||
if err != nil {
|
||||
return errors.New("转码错误!")
|
||||
}
|
||||
|
||||
// 2. 将字节数组保存为图像文件
|
||||
err = ioutil.WriteFile(outputPath, data, 0644)
|
||||
if err != nil {
|
||||
return errors.New("报错图像失败!")
|
||||
}
|
||||
return nil
|
||||
}
|
107
api/v1/common/coryCommon/basics.go
Normal file
107
api/v1/common/coryCommon/basics.go
Normal file
@ -0,0 +1,107 @@
|
||||
package coryCommon
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"github.com/tiger1103/gfast/v3/internal/app/system/dao"
|
||||
"reflect"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
func New() *coryCom {
|
||||
return &coryCom{}
|
||||
}
|
||||
|
||||
type coryCom struct{}
|
||||
|
||||
// CreateByOrUpdateBy 专门用来反射结构体中的创建人和更新人,然后返回回去,避免重复造轮子去写重复代码
|
||||
/**
|
||||
*使用实例代码
|
||||
* by := coryCommon.New().CreateByOrUpdateBy(ctx, res)
|
||||
* infoRes := by.(model.BusCompanyInfoRes)
|
||||
* res = &infoRes
|
||||
*
|
||||
*其中 updateByFieldVal.Set(reflect.ValueOf(updateByValue.Interface().(*gvar.Var).String())) 必须类型一致
|
||||
*/
|
||||
func (s *coryCom) CreateByOrUpdateBy(ctx context.Context, data interface{}) (dataRes interface{}) {
|
||||
// 使用反射获取 data 的值和类型信息
|
||||
val := reflect.ValueOf(data)
|
||||
typ := reflect.TypeOf(data)
|
||||
|
||||
// 判断 data 是否为指针类型,并获取实际值
|
||||
if val.Kind() == reflect.Ptr {
|
||||
val = val.Elem()
|
||||
typ = typ.Elem()
|
||||
}
|
||||
|
||||
flagCreate := true
|
||||
flagUpdate := true
|
||||
// 获取 createBy 字段在结构体中的索引
|
||||
createByField, ok := typ.FieldByName("CreateBy")
|
||||
if !ok {
|
||||
flagCreate = false
|
||||
//return data // 如果结构体中不存在 createBy 字段,则直接返回原始值
|
||||
createByField2, ok2 := typ.FieldByName("CreatedBy")
|
||||
if ok2 {
|
||||
createByField = createByField2
|
||||
flagCreate = true
|
||||
}
|
||||
}
|
||||
updateByField, ok := typ.FieldByName("UpdateBy")
|
||||
if !ok {
|
||||
flagUpdate = false
|
||||
//return data // 如果结构体中不存在 createBy 字段,则直接返回原始值
|
||||
updateByField2, ok2 := typ.FieldByName("UpdatedBy")
|
||||
if ok2 {
|
||||
updateByField = updateByField2
|
||||
flagCreate = true
|
||||
}
|
||||
}
|
||||
if flagCreate {
|
||||
// 判断 createBy 字段的类型是否为 string
|
||||
if createByField.Type.Kind() != reflect.String {
|
||||
// 如果 createBy 字段的类型不是 string,请根据需要进行相应的处理或返回错误
|
||||
// 此处假设 createBy 必须为 string 类型,如果不是则返回错误
|
||||
return errors.New("createBy字段类型不匹配")
|
||||
}
|
||||
// 获取原始的 createBy 字段的值并获取创建人
|
||||
createId := val.FieldByIndex(createByField.Index).String()
|
||||
ve := SelectByString(ctx, IsNumeric(createId), createId)
|
||||
// 设置 createBy 字段的值为指定参数
|
||||
createByFieldVal := val.FieldByIndex(createByField.Index)
|
||||
createByFieldVal.SetString(ve)
|
||||
}
|
||||
if flagUpdate {
|
||||
// 判断 updateBy 字段的类型是否为 string
|
||||
if updateByField.Type.Kind() != reflect.String {
|
||||
// 如果 createBy 字段的类型不是 string,请根据需要进行相应的处理或返回错误
|
||||
// 此处假设 createBy 必须为 string 类型,如果不是则返回错误
|
||||
return errors.New("updateBy字段类型不匹配")
|
||||
}
|
||||
// 获取原始的 createBy 字段的值并获取创建人
|
||||
updateId := val.FieldByIndex(updateByField.Index).String()
|
||||
ve := SelectByString(ctx, IsNumeric(updateId), updateId)
|
||||
// 设置 createBy 字段的值为指定参数
|
||||
updateByFieldVal := val.FieldByIndex(updateByField.Index)
|
||||
updateByFieldVal.SetString(ve)
|
||||
}
|
||||
// 返回修改后的结构体
|
||||
return val.Interface()
|
||||
}
|
||||
|
||||
func IsNumeric(str string) bool {
|
||||
_, err := strconv.ParseFloat(str, 64)
|
||||
return err == nil
|
||||
}
|
||||
|
||||
// SelectByString 根据字符串查询到具体人对应的名称
|
||||
func SelectByString(ctx context.Context, flag bool, str string) (ve string) {
|
||||
if flag {
|
||||
value, _ := dao.SysUser.Ctx(ctx).Fields("user_nickname").Where("id", str).Value()
|
||||
ve = value.String()
|
||||
} else {
|
||||
value, _ := dao.BusConstructionUser.Ctx(ctx).Fields("user_name").Where("openid", str).Value()
|
||||
ve = value.String()
|
||||
}
|
||||
return
|
||||
}
|
17
api/v1/common/coryCommon/camera/cameraEntity.go
Normal file
17
api/v1/common/coryCommon/camera/cameraEntity.go
Normal file
@ -0,0 +1,17 @@
|
||||
package camera
|
||||
|
||||
type LoginCamera struct {
|
||||
URLToken string `json:"URLToken"`
|
||||
TokenTimeout int64 `json:"TokenTimeout"`
|
||||
}
|
||||
|
||||
type ChannelSnapReq struct {
|
||||
Serial string `json:"serial" dc:"设备编号"`
|
||||
Stime int64 `json:"stime" dc:"快照时间, 从录像截取指定时间的历史快照, now 表示取实时快照, 即抓图允许值: now, YYYYMMDDHHmmss"`
|
||||
Format int64 `json:"format" dc:"stime 快照格式 允许值: jpeg, png"`
|
||||
}
|
||||
type ChannelSnapRes struct {
|
||||
Serial string `json:"serial" dc:"设备编号"`
|
||||
Stime int64 `json:"stime" dc:"快照时间, 从录像截取指定时间的历史快照, now 表示取实时快照, 即抓图允许值: now, YYYYMMDDHHmmss"`
|
||||
Format int64 `json:"format" dc:"stime 快照格式 允许值: jpeg, png"`
|
||||
}
|
303
api/v1/common/coryCommon/camera/cameraUtil.go
Normal file
303
api/v1/common/coryCommon/camera/cameraUtil.go
Normal file
@ -0,0 +1,303 @@
|
||||
package camera
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/go-redis/redis/v8"
|
||||
"github.com/gogf/gf/v2/crypto/gmd5"
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"github.com/gogf/gf/v2/os/gctx"
|
||||
"github.com/tiger1103/gfast-cache/cache"
|
||||
"github.com/tiger1103/gfast/v3/api/v1/common/coryCommon"
|
||||
"github.com/tiger1103/gfast/v3/api/v1/system"
|
||||
commonService "github.com/tiger1103/gfast/v3/internal/app/common/service"
|
||||
"github.com/tiger1103/gfast/v3/internal/app/system/dao"
|
||||
"github.com/tiger1103/gfast/v3/internal/app/system/model"
|
||||
"github.com/tiger1103/gfast/v3/internal/app/system/model/do"
|
||||
"github.com/tiger1103/gfast/v3/internal/app/system/service"
|
||||
"github.com/tiger1103/gfast/v3/third/arithmetic/SpartaApi"
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// LoginCameraFunc 登录获取 身份凭证
|
||||
func LoginCameraFunc(ctx context.Context) (token string) {
|
||||
api, _ := g.Cfg().Get(ctx, "LiveGBS.safety.api")
|
||||
acc, _ := g.Cfg().Get(ctx, "LiveGBS.safety.acc")
|
||||
pas, _ := g.Cfg().Get(ctx, "LiveGBS.safety.pas")
|
||||
key := "loginCamera"
|
||||
//从缓存捞取key
|
||||
prefix := g.Cfg().MustGet(ctx, "system.cache.prefix").String()
|
||||
gfCache := cache.New(prefix)
|
||||
get := commonService.Cache().Get(ctx, gfCache.CachePrefix+key)
|
||||
if get != nil && get.String() != "" {
|
||||
token = get.String()
|
||||
return token
|
||||
} else {
|
||||
account := acc.String()
|
||||
password := gmd5.MustEncryptString(pas.String())
|
||||
uri := api.String() + "api/v1/login?username=" + account + "&password=" + password + "&url_token_only=true"
|
||||
response, err := g.Client().Get(gctx.New(), uri)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
var lc *LoginCamera
|
||||
err = json.Unmarshal([]byte(response.ReadAllString()), &lc)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
//将token存储到redis中,tiken默认时间为秒,实际计算为7天,(这里少100秒,防止token过期还存在redis中)
|
||||
commonService.Cache().Set(ctx, key, lc.URLToken, time.Duration(lc.TokenTimeout-100)*time.Second)
|
||||
token = lc.URLToken
|
||||
return token
|
||||
}
|
||||
}
|
||||
|
||||
var Rdb *redis.Client
|
||||
|
||||
// camera.DYFunc()
|
||||
// 发布小程序需要关闭 camera.DYFunc()
|
||||
// DYFunc 连接redis
|
||||
func DYFunc() {
|
||||
ctx := gctx.New()
|
||||
err := g.Try(ctx, func(ctx context.Context) {
|
||||
fmt.Println("redis订阅已开启")
|
||||
// 创建第一个 Redis 连接
|
||||
address, _ := g.Cfg().Get(ctx, "LiveGBS.redis.address")
|
||||
password, _ := g.Cfg().Get(ctx, "LiveGBS.redis.password")
|
||||
// 创建一个 Redis 客户端连接
|
||||
options := &redis.Options{
|
||||
Addr: address.String(), // 替换为你的 Redis 地址和端口
|
||||
Password: password.String(), // 替换为你的 Redis 密码
|
||||
DB: 1, // 替换为你的 Redis 数据库索引
|
||||
}
|
||||
Rdb = redis.NewClient(options)
|
||||
|
||||
// 创建一个订阅频道
|
||||
channelName := "device" // 替换为你要订阅的频道名
|
||||
pubsub := Rdb.Subscribe(context.Background(), channelName)
|
||||
|
||||
// 处理订阅消息的 Goroutine
|
||||
go func() {
|
||||
for {
|
||||
msg, err := pubsub.ReceiveMessage(context.Background())
|
||||
if err != nil {
|
||||
log.Println("Error receiving message:", err)
|
||||
time.Sleep(time.Second) // 出错时等待一段时间后重试
|
||||
continue
|
||||
}
|
||||
var strData = msg.Payload
|
||||
log.Println("Received message:", strData)
|
||||
//1、获取到数据,然后查看是否':'拼接(先不管拼接),不是':'就直接操作数据库
|
||||
split := strings.Split(strData, ":")
|
||||
if len(split) < 2 {
|
||||
onOrOff := strings.Split(split[0], " ")[1]
|
||||
if strings.EqualFold(onOrOff, "on") {
|
||||
onOrOff = "1"
|
||||
} else {
|
||||
onOrOff = "0"
|
||||
}
|
||||
_, err = dao.QianqiCamera.Ctx(ctx).Where("code", split[0]).Update(g.Map{"country_state": onOrOff})
|
||||
//if err != nil {
|
||||
// id, _ := result.LastInsertId()
|
||||
// if id > 0 {
|
||||
// dao.BusCameraChannel.Ctx(ctx).Where("country_id", id).Update(g.Map{"status": onOrOff})
|
||||
// }
|
||||
//}
|
||||
}
|
||||
time.Sleep(time.Second * 3)
|
||||
}
|
||||
}()
|
||||
})
|
||||
fmt.Println("订阅错误问题:", err)
|
||||
}
|
||||
|
||||
// ChannelSnapFunc 快照
|
||||
func ChannelSnapFunc(ctx context.Context, id int64, serial string, code string, projectId int64) (err error) {
|
||||
//获取预制位,前提是 PresetEnable == true
|
||||
pb, err := PeeeresettingBitFunc(gctx.New(), serial, code)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, pi := range pb.PresetItemList {
|
||||
if pi.PresetEnable == true {
|
||||
//0、预置位
|
||||
err = PresettingBitFunc(ctx, serial, code, "goto", pi.PresetID, "")
|
||||
//1、请求接口得到二进制数据
|
||||
suffix := "png"
|
||||
api, _ := g.Cfg().Get(ctx, "LiveGBS.safety.api")
|
||||
tokens := LoginCameraFunc(ctx)
|
||||
uri := api.String() + "api/v1/device/channelsnap?serial=" + serial + "&code=" + code + "&stime=now&format=" + suffix + "&token=" + tokens
|
||||
response, err := g.Client().Get(gctx.New(), uri)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
//2、生成时间文件夹;生成文件名;然后同一斜杠得到完成路径
|
||||
ht := coryCommon.Helmet
|
||||
str := coryCommon.Ynr(ht)
|
||||
fn := coryCommon.FileName("helmet")
|
||||
dir, err := os.Getwd()
|
||||
str = dir + "/" + str + fn + "." + suffix
|
||||
str = filepath.ToSlash(str)
|
||||
//3、创建一个文件来保存图片数据;将响应体中的图片数据复制到文件中
|
||||
file, err := os.Create(str)
|
||||
if err != nil {
|
||||
err = errors.New("创建文件出错")
|
||||
return err
|
||||
}
|
||||
// 创建一个缓冲区,用于读取数据(从 body 中读取数据)
|
||||
var re = response
|
||||
var by = re.Body
|
||||
|
||||
lenStr, err := io.Copy(file, by)
|
||||
if err != nil {
|
||||
err = errors.New("复制图片数据到文件出错")
|
||||
return err
|
||||
} else {
|
||||
file.Close()
|
||||
}
|
||||
if lenStr < 10240 {
|
||||
file.Close() // 关闭文件
|
||||
os.Remove(str)
|
||||
return err
|
||||
}
|
||||
//4、《斯巴达》调用算法接口圈出未带安全帽的人
|
||||
req := SpartaApi.RecognizeReq{
|
||||
CapUrl: str,
|
||||
//RecType: "head smoke belt waste excavator Roller Truck_crane Loader Submersible_drilling_rig Sprinkler Truck_mounted_crane Truck",
|
||||
RecType: "head smoke",
|
||||
Async: "False",
|
||||
CallBackUrl: "",
|
||||
AreaHigh: "",
|
||||
}
|
||||
mp, flag, num, err := SpartaApi.CommonAlgorithmFunc(ctx, &req)
|
||||
if err != nil {
|
||||
os.Remove(str)
|
||||
return err
|
||||
}
|
||||
////4、《ys7》调用算法接口圈出未带安全帽的人
|
||||
//flag, num, err := ys7.InitYs7(str)
|
||||
//if err != nil {
|
||||
// os.Remove(str)
|
||||
// return err
|
||||
//}
|
||||
//5、flag为true表示有违规数据
|
||||
if flag {
|
||||
// 使用range遍历map
|
||||
mpkStr := ""
|
||||
mpvStr := ""
|
||||
for key, value := range mp {
|
||||
mpkStr = mpkStr + key + ","
|
||||
mpvStr = mpvStr + value + "、"
|
||||
}
|
||||
mpkStr = strings.TrimRight(mpkStr, ",")
|
||||
mpvStr = strings.TrimRight(mpvStr, "、")
|
||||
//5、生成数据存储到数据表中(识别记录)
|
||||
path := strings.ReplaceAll(ht, "/resource/public/", coryCommon.GetWd)
|
||||
currentTime := time.Now()
|
||||
dateString := currentTime.Format("2006-01-02")
|
||||
path = path + dateString + "/" + fn + "." + suffix
|
||||
addReq := system.BusTourAddReq{
|
||||
ProjectId: projectId,
|
||||
TourCategory: "2",
|
||||
TourType: "1",
|
||||
Picture: path,
|
||||
Describe: mpvStr,
|
||||
Num: num,
|
||||
TableName: dao.QianqiCamera.Table(),
|
||||
TableId: id,
|
||||
}
|
||||
service.BusTour().Add(ctx, &addReq)
|
||||
//6、生成数据存储到违规记录里面
|
||||
var bvl model.BusViolationLevelInfoRes
|
||||
err = dao.BusViolationLevel.Ctx(ctx).Where("tour_type", mpkStr).Fields("id,grade").Scan(&bvl)
|
||||
recordAddReq := do.BusViolationRecord{
|
||||
ProjectId: projectId,
|
||||
LevelId: bvl.Id,
|
||||
Level: bvl.Grade,
|
||||
TourType: mpkStr,
|
||||
DataSource: "camera",
|
||||
//Picture: path,
|
||||
//WxOrPc: "1",
|
||||
}
|
||||
dao.BusViolationRecord.Ctx(ctx).Insert(recordAddReq)
|
||||
}
|
||||
time.Sleep(20 * time.Second)
|
||||
}
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
/*
|
||||
PresettingBitFunc 设备控制-预置位控制
|
||||
serial: 国标号
|
||||
code: 通道号
|
||||
command: 指令
|
||||
preset: 预置位编号
|
||||
name: 预置位名称
|
||||
*/
|
||||
func PresettingBitFunc(ctx context.Context, serial string, code string, command string, preset int, name string) (err error) {
|
||||
if name != "" && (name == "set" || name == "goto" || name == "remove") {
|
||||
err = errors.New("控制指令允许值: set, goto, remove")
|
||||
return err
|
||||
}
|
||||
if !(preset > 0 && preset <= 255) {
|
||||
err = errors.New("预置位编号范围为:1~255")
|
||||
return err
|
||||
}
|
||||
tokens := LoginCameraFunc(ctx)
|
||||
api, _ := g.Cfg().Get(ctx, "LiveGBS.safety.api")
|
||||
uri := api.String() + "api/v1/control/preset?serial=" + serial +
|
||||
"&code=" + code +
|
||||
"&command=" + command +
|
||||
"&preset=" + strconv.Itoa(preset) +
|
||||
"&token=" + tokens
|
||||
if name != "" {
|
||||
uri = uri + "&name=" + name
|
||||
}
|
||||
g.Client().Get(gctx.New(), uri)
|
||||
return err
|
||||
}
|
||||
|
||||
func PeeeresettingBitFunc(ctx context.Context, serial string, code string) (pb *PeeeresettingBitEntity, err error) {
|
||||
tokens := LoginCameraFunc(ctx)
|
||||
api, _ := g.Cfg().Get(ctx, "LiveGBS.safety.api")
|
||||
uri := api.String() + "api/v1/device/fetchpreset?serial=" + serial +
|
||||
"&code=" + code +
|
||||
"&token=" + tokens +
|
||||
"&fill=false"
|
||||
ft, errft := g.Client().Get(gctx.New(), uri)
|
||||
var str = ft.ReadAllString()
|
||||
err = json.Unmarshal([]byte(str), &pb)
|
||||
if err != nil {
|
||||
if strings.Contains(str, "offline") {
|
||||
err = errors.New("当前设备不在线!")
|
||||
} else if strings.Contains(str, "not found") {
|
||||
err = errors.New("当前设备沒找到!")
|
||||
} else {
|
||||
err = errft
|
||||
}
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
type PeeeresettingBitEntity struct {
|
||||
DeviceID string `json:"DeviceID"`
|
||||
Result string `json:"Result"`
|
||||
SumNum int `json:"SumNum"`
|
||||
PresetItemList []*PeeeresettingBitListEntity `json:"PresetItemList"`
|
||||
}
|
||||
|
||||
type PeeeresettingBitListEntity struct {
|
||||
PresetID int `json:"PresetID"`
|
||||
PresetName string `json:"PresetName"`
|
||||
PresetEnable bool `json:"PresetEnable"`
|
||||
}
|
58
api/v1/common/coryCommon/constant.go
Normal file
58
api/v1/common/coryCommon/constant.go
Normal file
@ -0,0 +1,58 @@
|
||||
package coryCommon
|
||||
|
||||
const Global = "xny.yj-3d.com"
|
||||
|
||||
var GlobalPath = "http://" + Global + ":7363"
|
||||
var GlobalFile = "/file"
|
||||
|
||||
// ==========================错误类型==========================
|
||||
|
||||
// SYSERRHINT 系统错误
|
||||
var syserrhint = "系统错误,请联系管理员!"
|
||||
|
||||
// ImportFile 系统错误
|
||||
var ImportFile = "导入文件有误!"
|
||||
|
||||
// ===========================文件===========================
|
||||
|
||||
var GetWd = "/file/" // GetWd 静态文件路径
|
||||
var LargeFileClt = "/resource/public/clt/" // LargeFileClt 文件上传地址-【倾斜模型/光伏板】
|
||||
var LargeFileShp = "/resource/public/shp/" // LargeFileShp 文件上传地址-shp 红线
|
||||
var ProjectSplitTable = "/resource/public/projectSplitTable/" // ProjectSplitTable 项目划分表临时文件
|
||||
var Temporary = "/resource/public/temporary" // Temporary 临时文件存放地址
|
||||
var LargeFilePrivacy = "/resource/public/privacy/" // LargeFilePrivacy 身份证银行卡的存储位置
|
||||
var Portrait = "/resource/public/portrait" // Portrait 人像 - 人脸存储的地方(人脸对比的人脸和打卡的人脸都存放在此处)
|
||||
var Helmet = "/resource/public/upload_file/" // Helmet 公共位置
|
||||
var GisModelLib = "/resource/public/mx/" // GisModelLib 模型库
|
||||
var Uav = "/resource/public/uav/" // uav 无人机资源
|
||||
var UavMerge = "resource/public/tif" // uav 无人机资源大图合并资源
|
||||
/*
|
||||
网盘位置
|
||||
*/
|
||||
var Template = "/resource/public/masterMask"
|
||||
var Template2 = "/resource/public/masterMask/dataFolder" //(工程资料)资料文件保存的位置
|
||||
var Template3 = "/resource/public/masterMask/sourceData" //(工程资料)源数据文件保存的位置
|
||||
var Report = "/resource/public/networkDisk/report" //(网盘)科研及专题报告
|
||||
var ProductionDrawing = "/resource/public/networkDisk/productionDrawing" //(网盘)施工图
|
||||
var Completion = "/resource/public/networkDisk/completion" //(网盘)竣工图
|
||||
var QualityMeeting = "/resource/public/networkDisk/qualityMeeting" //(网盘)质量会议
|
||||
var SafetyMeeting = "/resource/public/networkDisk/safetyMeeting" //(网盘)安全会议
|
||||
|
||||
// ==========================文件后缀==========================
|
||||
|
||||
// PictureSuffix 常见图片后缀
|
||||
var PictureSuffix = "JPEG|JPG|HEIF|PNG"
|
||||
|
||||
// ==========================项目常量==========================
|
||||
|
||||
// gispath gis文件
|
||||
var gispath = "gisfile/"
|
||||
|
||||
// commonpath common文件
|
||||
var commonpath = "commonfile/"
|
||||
|
||||
// addFilePath GIS路径
|
||||
var addFilePath = "/yjearth4.0/static/source/"
|
||||
|
||||
// suffix 文件后缀名称
|
||||
var suffix = "clt,pdf,zip"
|
277
api/v1/common/coryCommon/coordinateTransform.go
Normal file
277
api/v1/common/coryCommon/coordinateTransform.go
Normal file
@ -0,0 +1,277 @@
|
||||
package coryCommon
|
||||
|
||||
import (
|
||||
"math"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
// WGS84坐标系:即地球坐标系,国际上通用的坐标系。
|
||||
// GCJ02坐标系:即火星坐标系,WGS84坐标系经加密后的坐标系。Google Maps,高德在用。
|
||||
// BD09坐标系:即百度坐标系,GCJ02坐标系经加密后的坐标系。
|
||||
|
||||
const (
|
||||
X_PI = math.Pi * 3000.0 / 180.0
|
||||
OFFSET = 0.00669342162296594323
|
||||
AXIS = 6378245.0
|
||||
)
|
||||
|
||||
// BD09toGCJ02 百度坐标系->火星坐标系
|
||||
func BD09toGCJ02(lon, lat float64) (float64, float64) {
|
||||
x := lon - 0.0065
|
||||
y := lat - 0.006
|
||||
|
||||
z := math.Sqrt(x*x+y*y) - 0.00002*math.Sin(y*X_PI)
|
||||
theta := math.Atan2(y, x) - 0.000003*math.Cos(x*X_PI)
|
||||
|
||||
gLon := z * math.Cos(theta)
|
||||
gLat := z * math.Sin(theta)
|
||||
|
||||
return gLon, gLat
|
||||
}
|
||||
|
||||
// GCJ02toBD09 火星坐标系->百度坐标系
|
||||
func GCJ02toBD09(lon, lat float64) (float64, float64) {
|
||||
z := math.Sqrt(lon*lon+lat*lat) + 0.00002*math.Sin(lat*X_PI)
|
||||
theta := math.Atan2(lat, lon) + 0.000003*math.Cos(lon*X_PI)
|
||||
|
||||
bdLon := z*math.Cos(theta) + 0.0065
|
||||
bdLat := z*math.Sin(theta) + 0.006
|
||||
|
||||
return bdLon, bdLat
|
||||
}
|
||||
|
||||
// WGS84toGCJ02 WGS84坐标系->火星坐标系
|
||||
func WGS84toGCJ02(lon, lat float64) (float64, float64) {
|
||||
if isOutOFChina(lon, lat) {
|
||||
return lon, lat
|
||||
}
|
||||
|
||||
mgLon, mgLat := delta(lon, lat)
|
||||
|
||||
return mgLon, mgLat
|
||||
}
|
||||
|
||||
// GCJ02toWGS84 火星坐标系->WGS84坐标系
|
||||
func GCJ02toWGS84(lon, lat float64) (float64, float64) {
|
||||
if isOutOFChina(lon, lat) {
|
||||
return lon, lat
|
||||
}
|
||||
|
||||
mgLon, mgLat := delta(lon, lat)
|
||||
|
||||
return lon*2 - mgLon, lat*2 - mgLat
|
||||
}
|
||||
|
||||
// BD09toWGS84 百度坐标系->WGS84坐标系
|
||||
func BD09toWGS84(lon, lat float64) (float64, float64) {
|
||||
lon, lat = BD09toGCJ02(lon, lat)
|
||||
return GCJ02toWGS84(lon, lat)
|
||||
}
|
||||
|
||||
// WGS84toBD09 WGS84坐标系->百度坐标系
|
||||
func WGS84toBD09(lon, lat float64) (float64, float64) {
|
||||
lon, lat = WGS84toGCJ02(lon, lat)
|
||||
return GCJ02toBD09(lon, lat)
|
||||
}
|
||||
|
||||
func delta(lon, lat float64) (float64, float64) {
|
||||
dlat := transformlat(lon-105.0, lat-35.0)
|
||||
dlon := transformlng(lon-105.0, lat-35.0)
|
||||
|
||||
radlat := lat / 180.0 * math.Pi
|
||||
magic := math.Sin(radlat)
|
||||
magic = 1 - OFFSET*magic*magic
|
||||
sqrtmagic := math.Sqrt(magic)
|
||||
|
||||
dlat = (dlat * 180.0) / ((AXIS * (1 - OFFSET)) / (magic * sqrtmagic) * math.Pi)
|
||||
dlon = (dlon * 180.0) / (AXIS / sqrtmagic * math.Cos(radlat) * math.Pi)
|
||||
|
||||
mgLat := lat + dlat
|
||||
mgLon := lon + dlon
|
||||
|
||||
return mgLon, mgLat
|
||||
}
|
||||
|
||||
func transformlat(lon, lat float64) float64 {
|
||||
var ret = -100.0 + 2.0*lon + 3.0*lat + 0.2*lat*lat + 0.1*lon*lat + 0.2*math.Sqrt(math.Abs(lon))
|
||||
ret += (20.0*math.Sin(6.0*lon*math.Pi) + 20.0*math.Sin(2.0*lon*math.Pi)) * 2.0 / 3.0
|
||||
ret += (20.0*math.Sin(lat*math.Pi) + 40.0*math.Sin(lat/3.0*math.Pi)) * 2.0 / 3.0
|
||||
ret += (160.0*math.Sin(lat/12.0*math.Pi) + 320*math.Sin(lat*math.Pi/30.0)) * 2.0 / 3.0
|
||||
return ret
|
||||
}
|
||||
|
||||
func transformlng(lon, lat float64) float64 {
|
||||
var ret = 300.0 + lon + 2.0*lat + 0.1*lon*lon + 0.1*lon*lat + 0.1*math.Sqrt(math.Abs(lon))
|
||||
ret += (20.0*math.Sin(6.0*lon*math.Pi) + 20.0*math.Sin(2.0*lon*math.Pi)) * 2.0 / 3.0
|
||||
ret += (20.0*math.Sin(lon*math.Pi) + 40.0*math.Sin(lon/3.0*math.Pi)) * 2.0 / 3.0
|
||||
ret += (150.0*math.Sin(lon/12.0*math.Pi) + 300.0*math.Sin(lon/30.0*math.Pi)) * 2.0 / 3.0
|
||||
return ret
|
||||
}
|
||||
|
||||
func isOutOFChina(lon, lat float64) bool {
|
||||
return !(lon > 73.66 && lon < 135.05 && lat > 3.86 && lat < 53.55)
|
||||
}
|
||||
|
||||
/*
|
||||
===========================================================================================
|
||||
===========================================================================================
|
||||
===========================================================================================
|
||||
===========================================================================================
|
||||
===========================================================================================
|
||||
===========================================================================================
|
||||
===========================================================================================
|
||||
*/
|
||||
|
||||
// GPSUtil is a utility class for GPS calculations.
|
||||
// 小写方法是私有方法,大写方法是公有方法 可根据需要调整
|
||||
|
||||
func News() *GPSUtil {
|
||||
return &GPSUtil{}
|
||||
}
|
||||
|
||||
type GPSUtil struct {
|
||||
}
|
||||
|
||||
const (
|
||||
pi = 3.1415926535897932384626 // 圆周率
|
||||
x_pi = 3.14159265358979324 * 3000.0 / 180.0 // 圆周率对应的经纬度偏移
|
||||
a = 6378245.0 // 长半轴
|
||||
ee = 0.00669342162296594323 // 扁率
|
||||
)
|
||||
|
||||
func (receiver *GPSUtil) transformLat(x, y float64) float64 {
|
||||
ret := -100.0 + 2.0*x + 3.0*y + 0.2*y*y + 0.1*x*y + 0.2*math.Sqrt(math.Abs(x))
|
||||
ret += (20.0*math.Sin(6.0*x*pi) + 20.0*math.Sin(2.0*x*pi)) * 2.0 / 3.0
|
||||
ret += (20.0*math.Sin(y*pi) + 40.0*math.Sin(y/3.0*pi)) * 2.0 / 3.0
|
||||
ret += (160.0*math.Sin(y/12.0*pi) + 320*math.Sin(y*pi/30.0)) * 2.0 / 3.0
|
||||
return ret
|
||||
}
|
||||
|
||||
func (receiver *GPSUtil) transformlng(x, y float64) float64 {
|
||||
ret := 300.0 + x + 2.0*y + 0.1*x*x + 0.1*x*y + 0.1*math.Sqrt(math.Abs(x))
|
||||
ret += (20.0*math.Sin(6.0*x*pi) + 20.0*math.Sin(2.0*x*pi)) * 2.0 / 3.0
|
||||
ret += (20.0*math.Sin(x*pi) + 40.0*math.Sin(x/3.0*pi)) * 2.0 / 3.0
|
||||
ret += (150.0*math.Sin(x/12.0*pi) + 300.0*math.Sin(x/30.0*pi)) * 2.0 / 3.0
|
||||
return ret
|
||||
}
|
||||
|
||||
func (receiver *GPSUtil) outOfChina(lat, lng float64) bool {
|
||||
if lng < 72.004 || lng > 137.8347 {
|
||||
return true
|
||||
}
|
||||
if lat < 0.8293 || lat > 55.8271 {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (receiver *GPSUtil) transform(lat, lng float64) []float64 {
|
||||
if receiver.outOfChina(lat, lng) {
|
||||
return []float64{lat, lng}
|
||||
}
|
||||
dLat := receiver.transformLat(lng-105.0, lat-35.0)
|
||||
dlng := receiver.transformlng(lng-105.0, lat-35.0)
|
||||
radLat := lat / 180.0 * pi
|
||||
magic := math.Sin(radLat)
|
||||
magic = 1 - ee*magic*magic
|
||||
SqrtMagic := math.Sqrt(magic)
|
||||
dLat = (dLat * 180.0) / ((a * (1 - ee)) / (magic * SqrtMagic) * pi)
|
||||
dlng = (dlng * 180.0) / (a / SqrtMagic * math.Cos(radLat) * pi)
|
||||
mgLat := lat + dLat
|
||||
mglng := lng + dlng
|
||||
return []float64{mgLat, mglng}
|
||||
}
|
||||
|
||||
// WGS84_To_Gcj02 84 to 火星坐标系 (GCJ-02) World Geodetic System ==> Mars Geodetic System
|
||||
// @param lat
|
||||
// @param lng
|
||||
// @return
|
||||
func (receiver *GPSUtil) WGS84_To_Gcj02(lat, lng float64) []float64 {
|
||||
if receiver.outOfChina(lat, lng) {
|
||||
return []float64{lat, lng}
|
||||
}
|
||||
dLat := receiver.transformLat(lng-105.0, lat-35.0)
|
||||
dlng := receiver.transformlng(lng-105.0, lat-35.0)
|
||||
radLat := lat / 180.0 * pi
|
||||
magic := math.Sin(radLat)
|
||||
magic = 1 - ee*magic*magic
|
||||
SqrtMagic := math.Sqrt(magic)
|
||||
dLat = (dLat * 180.0) / ((a * (1 - ee)) / (magic * SqrtMagic) * pi)
|
||||
dlng = (dlng * 180.0) / (a / SqrtMagic * math.Cos(radLat) * pi)
|
||||
mgLat := lat + dLat
|
||||
mglng := lng + dlng
|
||||
return []float64{mgLat, mglng}
|
||||
}
|
||||
|
||||
// GCJ02_To_WGS84
|
||||
// 火星坐标系 (GCJ-02) to WGS84
|
||||
// @param lng
|
||||
// @param lat
|
||||
// @return
|
||||
func (receiver *GPSUtil) GCJ02_To_WGS84(lng, lat float64) []float64 {
|
||||
gps := receiver.transform(lat, lng)
|
||||
lngtitude := lng*2 - gps[1]
|
||||
latitude := lat*2 - gps[0]
|
||||
return []float64{lngtitude, latitude}
|
||||
}
|
||||
|
||||
/**
|
||||
* 火星坐标系 (GCJ-02) 与百度坐标系 (BD-09) 的转换算法 将 GCJ-02 坐标转换成 BD-09 坐标
|
||||
*
|
||||
* @param lat
|
||||
* @param lng
|
||||
*/
|
||||
func (receiver *GPSUtil) gcj02_To_Bd09(lat, lng float64) []float64 {
|
||||
x := lng
|
||||
y := lat
|
||||
z := math.Sqrt(x*x+y*y) + 0.00002*math.Sin(y*x_pi)
|
||||
theta := math.Atan2(y, x) + 0.000003*math.Cos(x*x_pi)
|
||||
templng := z*math.Cos(theta) + 0.0065
|
||||
tempLat := z*math.Sin(theta) + 0.006
|
||||
gps := []float64{tempLat, templng}
|
||||
return gps
|
||||
}
|
||||
|
||||
/**
|
||||
* * 火星坐标系 (GCJ-02) 与百度坐标系 (BD-09) 的转换算法 * * 将 BD-09 坐标转换成GCJ-02 坐标 * * @param
|
||||
* bd_lat * @param bd_lng * @return
|
||||
*/
|
||||
func (receiver *GPSUtil) bd09_To_Gcj02(lat, lng float64) []float64 {
|
||||
x := lng - 0.0065
|
||||
y := lat - 0.006
|
||||
z := math.Sqrt(x*x+y*y) - 0.00002*math.Sin(y*x_pi)
|
||||
theta := math.Atan2(y, x) - 0.000003*math.Cos(x*x_pi)
|
||||
templng := z * math.Cos(theta)
|
||||
tempLat := z * math.Sin(theta)
|
||||
gps := []float64{tempLat, templng}
|
||||
return gps
|
||||
}
|
||||
|
||||
/**将WGS84转为bd09
|
||||
* @param lat
|
||||
* @param lng
|
||||
* @return
|
||||
*/
|
||||
func (receiver *GPSUtil) WGS84_To_bd09(lat, lng float64) []float64 {
|
||||
gcj02 := receiver.WGS84_To_Gcj02(lat, lng)
|
||||
bd09 := receiver.gcj02_To_Bd09(gcj02[0], gcj02[1])
|
||||
return bd09
|
||||
}
|
||||
|
||||
func (receiver *GPSUtil) bd09_To_WGS84(lat, lng float64) []float64 {
|
||||
gcj02 := receiver.bd09_To_Gcj02(lat, lng)
|
||||
WGS84 := receiver.GCJ02_To_WGS84(gcj02[0], gcj02[1])
|
||||
//保留小数点后六位
|
||||
WGS84[0] = receiver.retain6(WGS84[0])
|
||||
WGS84[1] = receiver.retain6(WGS84[1])
|
||||
return WGS84
|
||||
}
|
||||
|
||||
/**保留小数点后六位
|
||||
* @param num
|
||||
* @return
|
||||
*/
|
||||
func (receiver *GPSUtil) retain6(num float64) float64 {
|
||||
value, _ := strconv.ParseFloat(strconv.FormatFloat(num, 'f', 6, 64), 64)
|
||||
return value
|
||||
}
|
329
api/v1/common/coryCommon/downloadFile.go
Normal file
329
api/v1/common/coryCommon/downloadFile.go
Normal file
@ -0,0 +1,329 @@
|
||||
package coryCommon
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"fmt"
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"github.com/gogf/gf/v2/os/gctx"
|
||||
"github.com/tiger1103/gfast/v3/internal/app/system/model"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// CreateZipFile 生成一个压缩文件夹,然后将指定文件夹的数据(文件及文件夹)存放到压缩文件下
|
||||
func CreateZipFile(sourceDir, zipFile string) error {
|
||||
zipFileToCreate, err := os.Create(zipFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer zipFileToCreate.Close()
|
||||
|
||||
zipWriter := zip.NewWriter(zipFileToCreate)
|
||||
defer zipWriter.Close()
|
||||
|
||||
err = filepath.Walk(sourceDir, func(path string, info os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return addFileToZip(zipWriter, path, sourceDir)
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func addFileToZip(zipWriter *zip.Writer, filePath, baseDir string) error {
|
||||
fileToZip, err := os.Open(filePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer fileToZip.Close()
|
||||
|
||||
info, err := fileToZip.Stat()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 获取文件相对路径
|
||||
relPath, err := filepath.Rel(baseDir, filePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 替换路径分隔符确保在压缩文件中使用正斜杠
|
||||
relPath = strings.ReplaceAll(relPath, `\`, "/")
|
||||
|
||||
header, err := zip.FileInfoHeader(info)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
header.Name = relPath
|
||||
|
||||
if info.IsDir() {
|
||||
header.Name += "/"
|
||||
header.Method = zip.Store // Directory
|
||||
} else {
|
||||
header.Method = zip.Deflate // File
|
||||
}
|
||||
|
||||
writer, err := zipWriter.CreateHeader(header)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !info.IsDir() {
|
||||
_, err = io.Copy(writer, fileToZip)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// MultifileDownload 【功能:多文件下载】===【参数:relativelyTemporaryPath相对路径、filesToCopy需要放在压缩包下载的文件】
|
||||
func MultifileDownload(relativelyTemporaryPath string, filesToCopy []string) (path string, err error) {
|
||||
//网络资源下载到本地
|
||||
for i := range filesToCopy {
|
||||
url := filesToCopy[i]
|
||||
pathParts := strings.Split(url, "/")
|
||||
fileName := pathParts[len(pathParts)-1]
|
||||
filePath := filepath.ToSlash(GetCWD() + Temporary + "/" + fileName)
|
||||
err = DownloadFile(url, filePath)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
filesToCopy[i] = filePath
|
||||
}
|
||||
|
||||
// 1、创建临时压缩包
|
||||
zipFile, zipWriter, err := createTempZip(relativelyTemporaryPath)
|
||||
if err != nil {
|
||||
fmt.Println("Error creating temp zip:", err)
|
||||
return "", err
|
||||
}
|
||||
defer func() {
|
||||
zipWriter.Close()
|
||||
zipFile.Close()
|
||||
//暂时不删除、创建了个每月定时清除临时文件的定时器
|
||||
//for i := range filesToCopy {
|
||||
// delFile(filesToCopy[i], 0) //删除临时文件
|
||||
//}
|
||||
//go delFile(zipFile.Name(), 20) // 删除临时压缩文件,20秒后执行,防止文件还没下载完成就给删除了
|
||||
}()
|
||||
|
||||
// 2、复制文件夹到压缩包
|
||||
for _, filePath := range filesToCopy {
|
||||
err := copyFileToZip(zipWriter, filePath)
|
||||
if err != nil {
|
||||
fmt.Printf("Error copying %s to zip: %s\n", filePath, err)
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
path = strings.ReplaceAll(filepath.ToSlash(zipFile.Name()), filepath.ToSlash(GetCWD())+"/resource/public", "/file") //如果服务器同步需要注意wxfile
|
||||
return
|
||||
}
|
||||
|
||||
// 创建临时压缩包,并且提供写入数据
|
||||
func createTempZip(relativelyTemporaryPath string) (*os.File, *zip.Writer, error) {
|
||||
cwd := GetCWD() + relativelyTemporaryPath
|
||||
zipFile, err := os.CreateTemp(cwd, "temp_zip_*.zip") //*自动分配
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
zipWriter := zip.NewWriter(zipFile)
|
||||
return zipFile, zipWriter, nil
|
||||
}
|
||||
|
||||
// 复制文件到压缩包
|
||||
func copyFileToZip(zipWriter *zip.Writer, filePath string) error {
|
||||
file, err := os.Open(filePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer file.Close()
|
||||
// 获取文件信息
|
||||
info, err := file.Stat()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// 创建zip文件中的文件头
|
||||
header, err := zip.FileInfoHeader(info)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// 指定文件名
|
||||
header.Name = filepath.Base(filePath)
|
||||
// 创建zip文件中的文件
|
||||
writer, err := zipWriter.CreateHeader(header)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// 复制文件内容到zip文件中
|
||||
_, err = io.Copy(writer, file)
|
||||
return err
|
||||
}
|
||||
|
||||
// delFile 删除文件
|
||||
func delFile(file string, second int) {
|
||||
if second > 0 {
|
||||
time.Sleep(time.Duration(second) * time.Second)
|
||||
}
|
||||
if err := os.Remove(file); err != nil {
|
||||
fmt.Println("Failed to delete temporary file:", err)
|
||||
}
|
||||
}
|
||||
|
||||
// DownloadFile URL资源下载到自定位置
|
||||
func DownloadFile(url, localPath string) error {
|
||||
// 发起HTTP GET请求
|
||||
resp, err := http.Get(url)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
// 检查响应状态码
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return fmt.Errorf("HTTP request failed with status code: %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
// 创建本地文件
|
||||
file, err := os.Create(localPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
// 将HTTP响应体的内容拷贝到本地文件
|
||||
_, err = io.Copy(file, resp.Body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// RemoveAllFilesInDirectory 删除指定目录下的所有文件 例子:"D:\\Cory\\go\\中煤\\zmkg-back\\resource\\public\\temporary"
|
||||
func RemoveAllFilesInDirectory(directoryPath string) error {
|
||||
// 获取指定目录下的所有文件和子目录
|
||||
files, err := filepath.Glob(filepath.Join(directoryPath, "*"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// 遍历所有文件并删除
|
||||
for _, file := range files {
|
||||
if err := os.RemoveAll(file); err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Println("Deleted:", file)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetFiles 获取目录下所有文件(包括文件夹中的文件)
|
||||
func GetFiles(folder string) (filesList []string) {
|
||||
files, _ := ioutil.ReadDir(folder)
|
||||
for _, file := range files {
|
||||
if file.IsDir() {
|
||||
GetFiles(folder + "/" + file.Name())
|
||||
} else {
|
||||
filesList = append(filesList, file.Name())
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// GetAllFile 获取目录下直属所有文件(不包括文件夹及其中的文件)
|
||||
func GetAllFile(pathname string) (s []string, err error) {
|
||||
rd, err := ioutil.ReadDir(pathname)
|
||||
if err != nil {
|
||||
fmt.Println("read dir fail:", err)
|
||||
return s, err
|
||||
}
|
||||
|
||||
for _, fi := range rd {
|
||||
if !fi.IsDir() {
|
||||
fullName := pathname + "/" + fi.Name()
|
||||
s = append(s, fullName)
|
||||
}
|
||||
}
|
||||
return s, nil
|
||||
}
|
||||
|
||||
// Xcopy 复制文件
|
||||
func Xcopy(source, target string) (err error) {
|
||||
// 打开源文件
|
||||
sourceFile, err := os.Open(source)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer sourceFile.Close()
|
||||
|
||||
// 创建目标文件
|
||||
destinationFile, err := os.Create(target)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer destinationFile.Close()
|
||||
|
||||
// 使用 io.Copy() 函数复制文件内容
|
||||
_, err = io.Copy(destinationFile, sourceFile)
|
||||
return err
|
||||
}
|
||||
|
||||
// CustomizationMultifileDownload 定制下载(安全考试专用)
|
||||
func CustomizationMultifileDownload(relativelyTemporaryPath string, mw []*model.ModelWeChatPdfWoRes) (path string, err error) {
|
||||
//1、创建文件夹
|
||||
paht := filepath.ToSlash(GetCWD() + "/" + Ynr(Temporary+"/"))
|
||||
folder := paht + FileName("aqks") //文件夹名
|
||||
folder = filepath.ToSlash(folder)
|
||||
folder = folder[0 : len(folder)-1]
|
||||
err = os.MkdirAll(folder, 0777)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
//2、网络资源下载到本地
|
||||
for i := range mw {
|
||||
url := mw[i].Path
|
||||
str := folder + "/" + mw[i].UserName + mw[i].Openid
|
||||
os.MkdirAll(str, 0777) //根据名字创建子目录
|
||||
//url看看是几个文件
|
||||
fileNum := strings.Split(url, ",")
|
||||
for j := range fileNum {
|
||||
if fileNum[j] != "" {
|
||||
//因为pdf是另外一个服务器所以需要下载,但是有的又是本地服务器,所以直接复制
|
||||
if strings.Contains(fileNum[j], "/wxfile/") {
|
||||
pathstr := g.Cfg().MustGet(gctx.New(), "cory").String() + fileNum[j]
|
||||
pathParts := strings.Split(pathstr, "/")
|
||||
filePath := str + "/" + pathParts[len(pathParts)-1]
|
||||
err = DownloadFile(pathstr, filePath) //下载网络图片
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
} else {
|
||||
source := FileToFunc(fileNum[j], 2)
|
||||
pathParts := strings.Split(fileNum[j], "/")
|
||||
target := str + "/" + pathParts[len(pathParts)-1]
|
||||
err := Xcopy(source, target)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
//3、压缩成压缩包zip
|
||||
path = paht + FileName("aqks") + ".zip"
|
||||
err = CreateZipFile(folder, path)
|
||||
return
|
||||
}
|
515
api/v1/common/coryCommon/excelUtil/excel.go
Normal file
515
api/v1/common/coryCommon/excelUtil/excel.go
Normal file
@ -0,0 +1,515 @@
|
||||
package excelUtil
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/tiger1103/gfast/v3/api/v1/common/coryCommon"
|
||||
"github.com/tiger1103/gfast/v3/api/wxApplet/wxApplet"
|
||||
"github.com/xuri/excelize/v2"
|
||||
"os"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
// ExcelOne 每条数据一个工作簿
|
||||
func ExcelOne(oneDateOneList []*wxApplet.PunchingCardRecordOne) (file string, filePath string, fileName string) {
|
||||
f := excelize.NewFile()
|
||||
defer func() {
|
||||
if err := f.Close(); err != nil {
|
||||
fmt.Println(err)
|
||||
}
|
||||
}()
|
||||
|
||||
for x, one := range oneDateOneList {
|
||||
itoa := strconv.Itoa(x + 1)
|
||||
// -----创建一个工作表
|
||||
_, err := f.NewSheet("Sheet" + itoa)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return
|
||||
}
|
||||
// -----表数据
|
||||
//设置1、2行高度
|
||||
err = f.SetRowHeight("Sheet"+itoa, 1, 36)
|
||||
err = f.SetRowHeight("Sheet"+itoa, 2, 14)
|
||||
//1、左侧固定 ABC的1~2
|
||||
f.SetCellValue("Sheet"+itoa, "A1", "序号")
|
||||
f.MergeCell("Sheet"+itoa, "A1", "A2")
|
||||
|
||||
f.SetCellValue("Sheet"+itoa, "B1", "姓名")
|
||||
f.MergeCell("Sheet"+itoa, "B1", "B2")
|
||||
|
||||
f.SetCellValue("Sheet"+itoa, "C1", "")
|
||||
f.MergeCell("Sheet"+itoa, "C1", "C2")
|
||||
//2、左侧固定 ABC的3~4
|
||||
f.SetCellValue("Sheet"+itoa, "A3", "1")
|
||||
f.MergeCell("Sheet"+itoa, "A3", "A4")
|
||||
|
||||
f.SetCellValue("Sheet"+itoa, "B3", one.Name)
|
||||
f.MergeCell("Sheet"+itoa, "B3", "B4")
|
||||
|
||||
f.SetCellValue("Sheet"+itoa, "C3", "打卡记录")
|
||||
f.SetCellValue("Sheet"+itoa, "C4", "工时")
|
||||
|
||||
//3、中间数据---------从4开始创建列
|
||||
var num = "0" //循环生成列
|
||||
var numInt = 1 //循环列对应的编号
|
||||
var numi = 0 //循环过后需要的列
|
||||
for i := 0; i < len(one.PunchCard); i++ {
|
||||
num = getColumnName(4 + i)
|
||||
f.SetCellValue("Sheet"+itoa, num+"2", numInt) //循环列对应的编号
|
||||
f.SetCellValue("Sheet"+itoa, num+"3", one.PunchCard[i].Clock) //循环生成列 时间
|
||||
f.SetCellValue("Sheet"+itoa, num+"4", one.PunchCard[i].Hour) //循环生成列 统计
|
||||
numi = 4 + i
|
||||
numInt = numInt + 1
|
||||
//最后一个总计
|
||||
if i == len(one.PunchCard)-1 {
|
||||
f.SetCellValue("Sheet"+itoa, num+"5", "总计") //循环生成列
|
||||
}
|
||||
}
|
||||
f.MergeCell("Sheet"+itoa, getColumnName(4)+"1", getColumnName(numi)+"1")
|
||||
f.SetCellValue("Sheet"+itoa, num+"1", "8月")
|
||||
|
||||
numi = numi + 1
|
||||
num = getColumnName(numi)
|
||||
//4、右侧不清楚
|
||||
f.SetCellValue("Sheet"+itoa, num+"1", "合计工时")
|
||||
f.SetCellValue("Sheet"+itoa, num+"4", one.SumHour)
|
||||
f.SetCellValue("Sheet"+itoa, num+"5", one.SumHour)
|
||||
f.MergeCell("Sheet"+itoa, num+"1", num+"2")
|
||||
|
||||
numi = numi + 1
|
||||
num = getColumnName(numi)
|
||||
f.SetCellValue("Sheet"+itoa, num+"1", "合计工天")
|
||||
f.SetCellValue("Sheet"+itoa, num+"4", one.SumDay)
|
||||
f.SetCellValue("Sheet"+itoa, num+"5", one.SumDay)
|
||||
f.MergeCell("Sheet"+itoa, num+"1", num+"2")
|
||||
|
||||
numi = numi + 1
|
||||
num = getColumnName(numi)
|
||||
f.SetCellValue("Sheet"+itoa, num+"1", "工人签名")
|
||||
f.MergeCell("Sheet"+itoa, num+"1", num+"2")
|
||||
|
||||
numi = numi + 1
|
||||
num = getColumnName(numi)
|
||||
f.SetCellValue("Sheet"+itoa, num+"1", "备注")
|
||||
f.MergeCell("Sheet"+itoa, num+"1", num+"2")
|
||||
}
|
||||
|
||||
// 设置工作簿的默认工作表
|
||||
f.SetActiveSheet(1)
|
||||
|
||||
// 根据指定路径保存文件
|
||||
str := FileName()
|
||||
filePath = coryCommon.Temporary + "/" + str
|
||||
getwd, err := os.Getwd()
|
||||
err = f.SaveAs(getwd + filePath)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return "", "", ""
|
||||
}
|
||||
return getwd + filePath, filePath, str
|
||||
}
|
||||
|
||||
// ExcelTwo 多条数据在一个工作簿
|
||||
func ExcelTwo(oneDateOneList []*wxApplet.PunchingCardRecordOne) (file string, filePath string, fileName string) {
|
||||
|
||||
f := excelize.NewFile()
|
||||
defer func() {
|
||||
if err := f.Close(); err != nil {
|
||||
fmt.Println(err)
|
||||
}
|
||||
}()
|
||||
|
||||
bottomStyleId, err := f.NewStyle(&excelize.Style{
|
||||
Border: []excelize.Border{
|
||||
{Type: "bottom", Color: "000000", Style: 1},
|
||||
//{Type: "diagonalDown", Color: "000000", Style: 5},
|
||||
//{Type: "diagonalUp", Color: "A020F0", Style: 6},
|
||||
},
|
||||
//背景
|
||||
Fill: excelize.Fill{
|
||||
Type: "pattern",
|
||||
Color: []string{"#e6ecf0"},
|
||||
Pattern: 1,
|
||||
},
|
||||
////字体
|
||||
//Font: &excelize.Font{
|
||||
// Color: "#ffffff", // 字体颜色,这里使用蓝色 (#0000FF)
|
||||
//},
|
||||
})
|
||||
|
||||
//全样式
|
||||
styleId, err := f.NewStyle(&excelize.Style{
|
||||
//边框
|
||||
Border: []excelize.Border{
|
||||
{Type: "left", Color: "000000", Style: 1},
|
||||
{Type: "top", Color: "000000", Style: 1},
|
||||
{Type: "bottom", Color: "000000", Style: 1},
|
||||
{Type: "right", Color: "000000", Style: 1},
|
||||
},
|
||||
//居中
|
||||
Alignment: &excelize.Alignment{
|
||||
Vertical: "center", // 上下居中
|
||||
Horizontal: "center", // 左右居中
|
||||
},
|
||||
//背景
|
||||
Fill: excelize.Fill{
|
||||
Type: "pattern",
|
||||
Color: []string{"#e6ecf0"},
|
||||
Pattern: 1,
|
||||
},
|
||||
////字体
|
||||
//Font: &excelize.Font{
|
||||
// Color: "#ffffff", // 字体颜色,这里使用蓝色 (#0000FF)
|
||||
//},
|
||||
})
|
||||
// 每组数据隔N行另起
|
||||
bookNum := 0
|
||||
for x, one := range oneDateOneList {
|
||||
if one.Name == "" {
|
||||
continue
|
||||
}
|
||||
itoa := "1" //第一个工作簿
|
||||
var num = "0" //循环生成列
|
||||
var numInt = 1 //循环列对应的编号
|
||||
var numi = 0 //循环过后需要的列
|
||||
for i := 0; i < len(one.PunchCard); i++ {
|
||||
//设置1、2行高度
|
||||
err := f.SetRowHeight("Sheet"+itoa, bookNum+1, 36)
|
||||
err = f.SetRowHeight("Sheet"+itoa, bookNum+2, 14)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return
|
||||
}
|
||||
//1、左侧固定 ABC的1~2
|
||||
f.SetCellValue("Sheet"+itoa, "A"+strconv.Itoa((bookNum+1)), "序号")
|
||||
f.MergeCell("Sheet"+itoa, "A"+strconv.Itoa((bookNum+1)), "A"+strconv.Itoa((bookNum+2)))
|
||||
f.SetCellStyle("Sheet"+itoa, "A"+strconv.Itoa((bookNum+1)), "A"+strconv.Itoa((bookNum+2)), styleId)
|
||||
|
||||
f.SetCellValue("Sheet"+itoa, "B"+strconv.Itoa((bookNum+1)), "姓名")
|
||||
f.MergeCell("Sheet"+itoa, "B"+strconv.Itoa((bookNum+1)), "B"+strconv.Itoa((bookNum+2)))
|
||||
f.SetCellStyle("Sheet"+itoa, "B"+strconv.Itoa((bookNum+1)), "B"+strconv.Itoa((bookNum+2)), styleId)
|
||||
|
||||
f.SetCellValue("Sheet"+itoa, "C"+strconv.Itoa((bookNum+1)), "")
|
||||
f.MergeCell("Sheet"+itoa, "C"+strconv.Itoa((bookNum+1)), "C"+strconv.Itoa((bookNum+2)))
|
||||
f.SetCellStyle("Sheet"+itoa, "C"+strconv.Itoa((bookNum+1)), "C"+strconv.Itoa((bookNum+2)), styleId)
|
||||
//2、左侧固定 ABC的3~4
|
||||
f.SetCellValue("Sheet"+itoa, "A"+strconv.Itoa((bookNum+3)), x+1)
|
||||
f.MergeCell("Sheet"+itoa, "A"+strconv.Itoa((bookNum+3)), "A"+strconv.Itoa((bookNum+4)))
|
||||
f.SetCellStyle("Sheet"+itoa, "A"+strconv.Itoa((bookNum+3)), "A"+strconv.Itoa((bookNum+4)), styleId)
|
||||
|
||||
f.SetCellValue("Sheet"+itoa, "B"+strconv.Itoa((bookNum+3)), one.Name)
|
||||
f.MergeCell("Sheet"+itoa, "B"+strconv.Itoa((bookNum+3)), "B"+strconv.Itoa((bookNum+4)))
|
||||
f.SetCellStyle("Sheet"+itoa, "B"+strconv.Itoa((bookNum+3)), "B"+strconv.Itoa((bookNum+4)), styleId)
|
||||
|
||||
//f.SetCellValue("Sheet"+itoa, "c"+strconv.Itoa((bookNum+3)), "打卡记录")
|
||||
f.SetCellValue("Sheet"+itoa, "c"+strconv.Itoa((bookNum+3)), "工时")
|
||||
f.MergeCell("Sheet"+itoa, "c"+strconv.Itoa((bookNum+3)), "c"+strconv.Itoa((bookNum+4)))
|
||||
f.SetCellStyle("Sheet"+itoa, "c"+strconv.Itoa((bookNum+3)), "c"+strconv.Itoa((bookNum+4)), styleId)
|
||||
|
||||
//3、中间数据---------从4开始创建列
|
||||
num = getColumnName(4 + i)
|
||||
f.SetCellValue("Sheet"+itoa, num+strconv.Itoa((bookNum+2)), numInt) //循环列对应的编号
|
||||
f.SetCellStyle("Sheet"+itoa, num+strconv.Itoa((bookNum+2)), num+strconv.Itoa((bookNum+2)), styleId) //循环列对应的编号
|
||||
f.SetCellValue("Sheet"+itoa, num+strconv.Itoa((bookNum+3)), one.PunchCard[i].Clock) //循环生成列 时间
|
||||
f.SetCellStyle("Sheet"+itoa, num+strconv.Itoa((bookNum+3)), num+strconv.Itoa((bookNum+3)), styleId) //循环生成列 时间
|
||||
f.SetCellValue("Sheet"+itoa, num+strconv.Itoa((bookNum+4)), one.PunchCard[i].Hour) //循环生成列 统计
|
||||
f.SetCellStyle("Sheet"+itoa, num+strconv.Itoa((bookNum+4)), num+strconv.Itoa((bookNum+4)), styleId) //循环生成列 统计
|
||||
numi = 4 + i
|
||||
numInt = numInt + 1
|
||||
//最后一个总计
|
||||
if i == len(one.PunchCard)-1 {
|
||||
f.SetCellValue("Sheet"+itoa, num+strconv.Itoa((bookNum+5)), "总计") //循环生成列
|
||||
f.SetCellStyle("Sheet"+itoa, num+strconv.Itoa((bookNum+5)), "总计", styleId) //循环生成列
|
||||
}
|
||||
}
|
||||
f.SetCellValue("Sheet"+itoa, "D"+strconv.Itoa((bookNum+1)), one.Years)
|
||||
f.MergeCell("Sheet"+itoa, getColumnName(4)+strconv.Itoa(bookNum+1), getColumnName(numi)+strconv.Itoa(bookNum+1))
|
||||
f.SetCellStyle("Sheet"+itoa, getColumnName(4)+strconv.Itoa(bookNum+1), getColumnName(numi)+strconv.Itoa(bookNum+1), styleId)
|
||||
|
||||
numi = numi + 1
|
||||
num = getColumnName(numi)
|
||||
//4、右侧不清楚
|
||||
f.SetCellValue("Sheet"+itoa, num+strconv.Itoa((bookNum+1)), "合计工时")
|
||||
f.MergeCell("Sheet"+itoa, num+strconv.Itoa(bookNum+1), getColumnName(numi)+strconv.Itoa(bookNum+3))
|
||||
f.SetCellStyle("Sheet"+itoa, num+strconv.Itoa(bookNum+1), getColumnName(numi)+strconv.Itoa(bookNum+3), styleId)
|
||||
|
||||
f.SetCellValue("Sheet"+itoa, num+strconv.Itoa((bookNum+4)), one.SumHour)
|
||||
f.MergeCell("Sheet"+itoa, num+strconv.Itoa(bookNum+4), getColumnName(numi)+strconv.Itoa(bookNum+4))
|
||||
f.SetCellStyle("Sheet"+itoa, num+strconv.Itoa(bookNum+4), getColumnName(numi)+strconv.Itoa(bookNum+4), styleId)
|
||||
|
||||
f.SetCellValue("Sheet"+itoa, num+strconv.Itoa((bookNum+5)), one.SumHour)
|
||||
f.MergeCell("Sheet"+itoa, num+strconv.Itoa(bookNum+5), getColumnName(numi)+strconv.Itoa(bookNum+5))
|
||||
f.SetCellStyle("Sheet"+itoa, num+strconv.Itoa(bookNum+5), getColumnName(numi)+strconv.Itoa(bookNum+5), styleId)
|
||||
|
||||
numi = numi + 1
|
||||
num = getColumnName(numi)
|
||||
f.SetCellValue("Sheet"+itoa, num+strconv.Itoa((bookNum+1)), "合计工天")
|
||||
f.MergeCell("Sheet"+itoa, num+strconv.Itoa(bookNum+1), getColumnName(numi)+strconv.Itoa(bookNum+3))
|
||||
f.SetCellStyle("Sheet"+itoa, num+strconv.Itoa(bookNum+1), getColumnName(numi)+strconv.Itoa(bookNum+3), styleId)
|
||||
|
||||
f.SetCellValue("Sheet"+itoa, num+strconv.Itoa((bookNum+4)), one.SumDay)
|
||||
f.MergeCell("Sheet"+itoa, num+strconv.Itoa(bookNum+4), getColumnName(numi)+strconv.Itoa(bookNum+4))
|
||||
f.SetCellStyle("Sheet"+itoa, num+strconv.Itoa(bookNum+4), getColumnName(numi)+strconv.Itoa(bookNum+4), styleId)
|
||||
|
||||
f.SetCellValue("Sheet"+itoa, num+strconv.Itoa((bookNum+5)), one.SumDay)
|
||||
f.MergeCell("Sheet"+itoa, num+strconv.Itoa(bookNum+5), getColumnName(numi)+strconv.Itoa(bookNum+5))
|
||||
f.SetCellStyle("Sheet"+itoa, num+strconv.Itoa(bookNum+5), getColumnName(numi)+strconv.Itoa(bookNum+5), styleId)
|
||||
|
||||
numi = numi + 1
|
||||
num = getColumnName(numi)
|
||||
f.SetCellValue("Sheet"+itoa, num+strconv.Itoa((bookNum+1)), "工人签名")
|
||||
f.MergeCell("Sheet"+itoa, num+strconv.Itoa((bookNum+1)), num+strconv.Itoa((bookNum+3)))
|
||||
f.SetCellStyle("Sheet"+itoa, num+strconv.Itoa((bookNum+1)), num+strconv.Itoa((bookNum+3)), styleId)
|
||||
|
||||
f.SetCellValue("Sheet"+itoa, num+strconv.Itoa((bookNum+4)), "")
|
||||
f.MergeCell("Sheet"+itoa, num+strconv.Itoa((bookNum+4)), num+strconv.Itoa((bookNum+5)))
|
||||
f.SetCellStyle("Sheet"+itoa, num+strconv.Itoa((bookNum+4)), num+strconv.Itoa((bookNum+5)), styleId)
|
||||
|
||||
numi = numi + 1
|
||||
num = getColumnName(numi)
|
||||
f.SetCellValue("Sheet"+itoa, num+strconv.Itoa((bookNum+1)), "备注")
|
||||
f.MergeCell("Sheet"+itoa, num+strconv.Itoa((bookNum+1)), num+strconv.Itoa((bookNum+3)))
|
||||
f.SetCellStyle("Sheet"+itoa, num+strconv.Itoa((bookNum+1)), num+strconv.Itoa((bookNum+3)), styleId)
|
||||
|
||||
f.SetCellValue("Sheet"+itoa, num+strconv.Itoa((bookNum+4)), "")
|
||||
f.MergeCell("Sheet"+itoa, num+strconv.Itoa((bookNum+4)), num+strconv.Itoa((bookNum+5)))
|
||||
f.SetCellStyle("Sheet"+itoa, num+strconv.Itoa((bookNum+4)), num+strconv.Itoa((bookNum+5)), styleId)
|
||||
|
||||
f.SetCellStyle("Sheet"+itoa, "A"+strconv.Itoa((bookNum+5)), getColumnName(numi-4)+strconv.Itoa((bookNum+5)), bottomStyleId)
|
||||
|
||||
bookNum = bookNum + 10
|
||||
}
|
||||
|
||||
// 设置工作簿的默认工作表
|
||||
f.SetActiveSheet(1)
|
||||
|
||||
// 根据指定路径保存文件
|
||||
str := FileName()
|
||||
filePath = coryCommon.Temporary + "/" + str
|
||||
getwd, err := os.Getwd()
|
||||
err = f.SaveAs(getwd + filePath)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return "", "", ""
|
||||
}
|
||||
return getwd + filePath, filePath, str
|
||||
}
|
||||
|
||||
// ExcelThree 多个工作簿,每个工作簿有多条数据
|
||||
func ExcelThree(gey []*wxApplet.GroupEntity) (file string, filePath string, fileName string) {
|
||||
f := excelize.NewFile()
|
||||
defer func() {
|
||||
if err := f.Close(); err != nil {
|
||||
fmt.Println(err)
|
||||
}
|
||||
}()
|
||||
|
||||
bottomStyleId, err := f.NewStyle(&excelize.Style{
|
||||
Border: []excelize.Border{
|
||||
{Type: "bottom", Color: "000000", Style: 1},
|
||||
//{Type: "diagonalDown", Color: "000000", Style: 5},
|
||||
//{Type: "diagonalUp", Color: "A020F0", Style: 6},
|
||||
},
|
||||
//背景
|
||||
Fill: excelize.Fill{
|
||||
Type: "pattern",
|
||||
Color: []string{"#e6ecf0"},
|
||||
Pattern: 1,
|
||||
},
|
||||
////字体
|
||||
//Font: &excelize.Font{
|
||||
// Color: "#ffffff", // 字体颜色,这里使用蓝色 (#0000FF)
|
||||
//},
|
||||
})
|
||||
|
||||
//全样式
|
||||
styleId, err := f.NewStyle(&excelize.Style{
|
||||
//边框
|
||||
Border: []excelize.Border{
|
||||
{Type: "left", Color: "000000", Style: 1},
|
||||
{Type: "top", Color: "000000", Style: 1},
|
||||
{Type: "bottom", Color: "000000", Style: 1},
|
||||
{Type: "right", Color: "000000", Style: 1},
|
||||
},
|
||||
//居中
|
||||
Alignment: &excelize.Alignment{
|
||||
Vertical: "center", // 上下居中
|
||||
Horizontal: "center", // 左右居中
|
||||
},
|
||||
//背景
|
||||
Fill: excelize.Fill{
|
||||
Type: "pattern",
|
||||
Color: []string{"#e6ecf0"},
|
||||
Pattern: 1,
|
||||
},
|
||||
////字体
|
||||
//Font: &excelize.Font{
|
||||
// Color: "#ffffff", // 字体颜色,这里使用蓝色 (#0000FF)
|
||||
//},
|
||||
})
|
||||
|
||||
//工作簿
|
||||
for _, data := range gey {
|
||||
//itoa := strconv.Itoa(y + 1)
|
||||
itoa := data.GroupName
|
||||
_, err = f.NewSheet(itoa)
|
||||
|
||||
oneDateOneList := data.PunchingCardRecordOne
|
||||
//工作簿里面的数据
|
||||
bookNum := 0 // 每组数据隔N行另起
|
||||
for x, one := range oneDateOneList {
|
||||
//itoa := "1" //第一个工作簿
|
||||
var num = "0" //循环生成列
|
||||
var numInt = 1 //循环列对应的编号
|
||||
var numi = 0 //循环过后需要的列
|
||||
for i := 0; i < len(one.PunchCard); i++ {
|
||||
//设置1、2行高度
|
||||
err := f.SetRowHeight(itoa, bookNum+1, 36)
|
||||
err = f.SetRowHeight(itoa, bookNum+2, 14)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return
|
||||
}
|
||||
//1、左侧固定 ABC的1~2
|
||||
f.SetCellValue(itoa, "A"+strconv.Itoa((bookNum+1)), "序号")
|
||||
f.MergeCell(itoa, "A"+strconv.Itoa((bookNum+1)), "A"+strconv.Itoa((bookNum+2)))
|
||||
f.SetCellStyle(itoa, "A"+strconv.Itoa((bookNum+1)), "A"+strconv.Itoa((bookNum+2)), styleId)
|
||||
|
||||
f.SetCellValue(itoa, "B"+strconv.Itoa((bookNum+1)), "姓名")
|
||||
f.MergeCell(itoa, "B"+strconv.Itoa((bookNum+1)), "B"+strconv.Itoa((bookNum+2)))
|
||||
f.SetCellStyle(itoa, "B"+strconv.Itoa((bookNum+1)), "B"+strconv.Itoa((bookNum+2)), styleId)
|
||||
|
||||
f.SetCellValue(itoa, "C"+strconv.Itoa((bookNum+1)), "")
|
||||
f.MergeCell(itoa, "C"+strconv.Itoa((bookNum+1)), "C"+strconv.Itoa((bookNum+2)))
|
||||
f.SetCellStyle(itoa, "C"+strconv.Itoa((bookNum+1)), "C"+strconv.Itoa((bookNum+2)), styleId)
|
||||
//2、左侧固定 ABC的3~4
|
||||
f.SetCellValue(itoa, "A"+strconv.Itoa((bookNum+3)), x+1)
|
||||
f.MergeCell(itoa, "A"+strconv.Itoa((bookNum+3)), "A"+strconv.Itoa((bookNum+4)))
|
||||
f.SetCellStyle(itoa, "A"+strconv.Itoa((bookNum+3)), "A"+strconv.Itoa((bookNum+4)), styleId)
|
||||
|
||||
f.SetCellValue(itoa, "B"+strconv.Itoa((bookNum+3)), one.Name)
|
||||
f.MergeCell(itoa, "B"+strconv.Itoa((bookNum+3)), "B"+strconv.Itoa((bookNum+4)))
|
||||
f.SetCellStyle(itoa, "B"+strconv.Itoa((bookNum+3)), "B"+strconv.Itoa((bookNum+4)), styleId)
|
||||
|
||||
//f.SetCellValue(itoa, "c"+strconv.Itoa((bookNum+3)), "打卡记录")
|
||||
f.SetCellValue(itoa, "c"+strconv.Itoa((bookNum+3)), "工时")
|
||||
f.MergeCell(itoa, "c"+strconv.Itoa((bookNum+3)), "c"+strconv.Itoa((bookNum+4)))
|
||||
f.SetCellStyle(itoa, "c"+strconv.Itoa((bookNum+3)), "c"+strconv.Itoa((bookNum+4)), styleId)
|
||||
|
||||
//3、中间数据---------从4开始创建列
|
||||
num = getColumnName(4 + i)
|
||||
f.SetCellValue(itoa, num+strconv.Itoa((bookNum+2)), numInt) //循环列对应的编号
|
||||
f.SetCellStyle(itoa, num+strconv.Itoa((bookNum+2)), num+strconv.Itoa((bookNum+2)), styleId) //循环列对应的编号
|
||||
f.SetCellValue(itoa, num+strconv.Itoa((bookNum+3)), one.PunchCard[i].Clock) //循环生成列 时间
|
||||
f.SetCellStyle(itoa, num+strconv.Itoa((bookNum+3)), num+strconv.Itoa((bookNum+3)), styleId) //循环生成列 时间
|
||||
f.SetCellValue(itoa, num+strconv.Itoa((bookNum+4)), one.PunchCard[i].Hour) //循环生成列 统计
|
||||
f.SetCellStyle(itoa, num+strconv.Itoa((bookNum+4)), num+strconv.Itoa((bookNum+4)), styleId) //循环生成列 统计
|
||||
numi = 4 + i
|
||||
numInt = numInt + 1
|
||||
//最后一个总计
|
||||
if i == len(one.PunchCard)-1 {
|
||||
f.SetCellValue(itoa, num+strconv.Itoa((bookNum+5)), "总计") //循环生成列
|
||||
f.SetCellStyle(itoa, num+strconv.Itoa((bookNum+5)), "总计", styleId) //循环生成列
|
||||
}
|
||||
}
|
||||
f.SetCellValue(itoa, "D"+strconv.Itoa((bookNum+1)), one.Years)
|
||||
f.MergeCell(itoa, getColumnName(4)+strconv.Itoa(bookNum+1), getColumnName(numi)+strconv.Itoa(bookNum+1))
|
||||
f.SetCellStyle(itoa, getColumnName(4)+strconv.Itoa(bookNum+1), getColumnName(numi)+strconv.Itoa(bookNum+1), styleId)
|
||||
|
||||
numi = numi + 1
|
||||
num = getColumnName(numi)
|
||||
//4、右侧不清楚
|
||||
f.SetCellValue(itoa, num+strconv.Itoa((bookNum+1)), "合计工时")
|
||||
f.MergeCell(itoa, num+strconv.Itoa(bookNum+1), getColumnName(numi)+strconv.Itoa(bookNum+3))
|
||||
f.SetCellStyle(itoa, num+strconv.Itoa(bookNum+1), getColumnName(numi)+strconv.Itoa(bookNum+3), styleId)
|
||||
|
||||
f.SetCellValue(itoa, num+strconv.Itoa((bookNum+4)), one.SumHour)
|
||||
f.MergeCell(itoa, num+strconv.Itoa(bookNum+4), getColumnName(numi)+strconv.Itoa(bookNum+4))
|
||||
f.SetCellStyle(itoa, num+strconv.Itoa(bookNum+4), getColumnName(numi)+strconv.Itoa(bookNum+4), styleId)
|
||||
|
||||
f.SetCellValue(itoa, num+strconv.Itoa((bookNum+5)), one.SumHour)
|
||||
f.MergeCell(itoa, num+strconv.Itoa(bookNum+5), getColumnName(numi)+strconv.Itoa(bookNum+5))
|
||||
f.SetCellStyle(itoa, num+strconv.Itoa(bookNum+5), getColumnName(numi)+strconv.Itoa(bookNum+5), styleId)
|
||||
|
||||
numi = numi + 1
|
||||
num = getColumnName(numi)
|
||||
f.SetCellValue(itoa, num+strconv.Itoa((bookNum+1)), "合计工天")
|
||||
f.MergeCell(itoa, num+strconv.Itoa(bookNum+1), getColumnName(numi)+strconv.Itoa(bookNum+3))
|
||||
f.SetCellStyle(itoa, num+strconv.Itoa(bookNum+1), getColumnName(numi)+strconv.Itoa(bookNum+3), styleId)
|
||||
|
||||
f.SetCellValue(itoa, num+strconv.Itoa((bookNum+4)), one.SumDay)
|
||||
f.MergeCell(itoa, num+strconv.Itoa(bookNum+4), getColumnName(numi)+strconv.Itoa(bookNum+4))
|
||||
f.SetCellStyle(itoa, num+strconv.Itoa(bookNum+4), getColumnName(numi)+strconv.Itoa(bookNum+4), styleId)
|
||||
|
||||
f.SetCellValue(itoa, num+strconv.Itoa((bookNum+5)), one.SumDay)
|
||||
f.MergeCell(itoa, num+strconv.Itoa(bookNum+5), getColumnName(numi)+strconv.Itoa(bookNum+5))
|
||||
f.SetCellStyle(itoa, num+strconv.Itoa(bookNum+5), getColumnName(numi)+strconv.Itoa(bookNum+5), styleId)
|
||||
|
||||
numi = numi + 1
|
||||
num = getColumnName(numi)
|
||||
f.SetCellValue(itoa, num+strconv.Itoa((bookNum+1)), "工人签名")
|
||||
f.MergeCell(itoa, num+strconv.Itoa((bookNum+1)), num+strconv.Itoa((bookNum+3)))
|
||||
f.SetCellStyle(itoa, num+strconv.Itoa((bookNum+1)), num+strconv.Itoa((bookNum+3)), styleId)
|
||||
|
||||
f.SetCellValue(itoa, num+strconv.Itoa((bookNum+4)), "")
|
||||
f.MergeCell(itoa, num+strconv.Itoa((bookNum+4)), num+strconv.Itoa((bookNum+5)))
|
||||
f.SetCellStyle(itoa, num+strconv.Itoa((bookNum+4)), num+strconv.Itoa((bookNum+5)), styleId)
|
||||
|
||||
numi = numi + 1
|
||||
num = getColumnName(numi)
|
||||
f.SetCellValue(itoa, num+strconv.Itoa((bookNum+1)), "备注")
|
||||
f.MergeCell(itoa, num+strconv.Itoa((bookNum+1)), num+strconv.Itoa((bookNum+3)))
|
||||
f.SetCellStyle(itoa, num+strconv.Itoa((bookNum+1)), num+strconv.Itoa((bookNum+3)), styleId)
|
||||
|
||||
f.SetCellValue(itoa, num+strconv.Itoa((bookNum+4)), "")
|
||||
f.MergeCell(itoa, num+strconv.Itoa((bookNum+4)), num+strconv.Itoa((bookNum+5)))
|
||||
f.SetCellStyle(itoa, num+strconv.Itoa((bookNum+4)), num+strconv.Itoa((bookNum+5)), styleId)
|
||||
|
||||
f.SetCellStyle(itoa, "A"+strconv.Itoa((bookNum+5)), getColumnName(numi-4)+strconv.Itoa((bookNum+5)), bottomStyleId)
|
||||
|
||||
//err = f.SetCellStyle("Sheet1", "A"+strconv.Itoa((bookNum+1)), "A"+strconv.Itoa((bookNum+5)), leftStyleId)
|
||||
//err = f.SetCellStyle("Sheet1", "I"+strconv.Itoa((bookNum+1)), "I"+strconv.Itoa((bookNum+5)), rightStyleId)
|
||||
//err = f.SetCellStyle("Sheet1", "A"+strconv.Itoa((bookNum+1)), "I"+strconv.Itoa((bookNum+1)), topStyleId)
|
||||
//err = f.SetCellStyle("Sheet1", "A"+strconv.Itoa((bookNum+5)), "I"+strconv.Itoa((bookNum+5)), bottomStyleId)
|
||||
|
||||
bookNum = bookNum + 10
|
||||
}
|
||||
}
|
||||
|
||||
// 设置工作簿的默认工作表
|
||||
f.SetActiveSheet(1)
|
||||
|
||||
// 根据指定路径保存文件
|
||||
str := FileName()
|
||||
filePath = coryCommon.Temporary + "/" + str
|
||||
getwd, err := os.Getwd()
|
||||
err = f.SaveAs(getwd + filePath)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return "", "", ""
|
||||
}
|
||||
return getwd + filePath, filePath, str
|
||||
}
|
||||
|
||||
// 根据列索引获取列的字母标识 比如1就是A 30就是AD
|
||||
func getColumnName(index int) string {
|
||||
var columnName string
|
||||
for index > 0 {
|
||||
mod := (index - 1) % 26
|
||||
columnName = string('A'+mod) + columnName
|
||||
index = (index - 1) / 26
|
||||
}
|
||||
return columnName
|
||||
}
|
||||
|
||||
// FileName 生成时间戳文件名
|
||||
func FileName() (str string) {
|
||||
// 获取当前时间
|
||||
currentTime := time.Now()
|
||||
// 格式化时间戳为字符串
|
||||
timestamp := currentTime.Format("20060102150405")
|
||||
// 生成文件名
|
||||
fileName := fmt.Sprintf("zm_%s.xlsx", timestamp)
|
||||
return fileName
|
||||
}
|
55
api/v1/common/coryCommon/excelUtil/excelEntity.go
Normal file
55
api/v1/common/coryCommon/excelUtil/excelEntity.go
Normal file
@ -0,0 +1,55 @@
|
||||
package excelUtil
|
||||
|
||||
type Style struct {
|
||||
Border []Border
|
||||
Fill Fill
|
||||
Font *Font
|
||||
Alignment *Alignment
|
||||
Protection *Protection
|
||||
NumFmt int
|
||||
DecimalPlaces int
|
||||
CustomNumFmt *string
|
||||
NegRed bool
|
||||
}
|
||||
|
||||
type Fill struct {
|
||||
Type string
|
||||
Pattern int
|
||||
Color []string
|
||||
Shading int
|
||||
}
|
||||
|
||||
type Protection struct {
|
||||
Hidden bool
|
||||
Locked bool
|
||||
}
|
||||
|
||||
type Font struct {
|
||||
Bold bool
|
||||
Italic bool
|
||||
Underline string
|
||||
Family string
|
||||
Size float64
|
||||
Strike bool
|
||||
Color string
|
||||
ColorIndexed int
|
||||
ColorTheme *int
|
||||
ColorTint float64
|
||||
VertAlign string
|
||||
}
|
||||
type Border struct {
|
||||
Type string
|
||||
Color string
|
||||
Style int
|
||||
}
|
||||
type Alignment struct {
|
||||
Horizontal string
|
||||
Indent int
|
||||
JustifyLastLine bool
|
||||
ReadingOrder uint64
|
||||
RelativeIndent int
|
||||
ShrinkToFit bool
|
||||
TextRotation int
|
||||
Vertical string
|
||||
WrapText bool
|
||||
}
|
209
api/v1/common/coryCommon/imgData.go
Normal file
209
api/v1/common/coryCommon/imgData.go
Normal file
@ -0,0 +1,209 @@
|
||||
package coryCommon
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
"image"
|
||||
_ "image/gif"
|
||||
_ "image/jpeg"
|
||||
_ "image/png"
|
||||
"io/ioutil"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
MinShortEdge = 15
|
||||
MaxLongEdge = 4096
|
||||
MaxSize = 1024 * 1024 // 4MB
|
||||
SupportedExts = ".jpg .jpeg .png .bmp"
|
||||
)
|
||||
|
||||
func ImgDataBase64(filePath string) (err error, encodedURL string) {
|
||||
file, err := os.Open(filePath)
|
||||
if err != nil {
|
||||
fmt.Println("打开图像文件失败:", err)
|
||||
err = errors.New("打开图像文件失败")
|
||||
return
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
fileInfo, err := file.Stat()
|
||||
if err != nil {
|
||||
fmt.Println("读取图像文件信息失败:", err)
|
||||
err = errors.New("读取图像文件信息失败")
|
||||
return
|
||||
}
|
||||
|
||||
fileSize := fileInfo.Size()
|
||||
if fileSize > MaxSize {
|
||||
fmt.Println("图像文件大小超过限制:", fileSize)
|
||||
err = errors.New("图像文件大小超过限制")
|
||||
return
|
||||
}
|
||||
|
||||
imgData, err := ioutil.ReadAll(file)
|
||||
if err != nil {
|
||||
fmt.Println("读取图像文件内容失败:", err)
|
||||
err = errors.New("读取图像文件内容失败")
|
||||
return
|
||||
}
|
||||
|
||||
bounds, format, err := image.DecodeConfig(bytes.NewReader(imgData))
|
||||
if err != nil {
|
||||
fmt.Println("解码图像配置失败:", err)
|
||||
err = errors.New("解码图像配置失败")
|
||||
return
|
||||
}
|
||||
|
||||
if format != "jpeg" && format != "jpg" && format != "png" && format != "bmp" {
|
||||
fmt.Println("不支持的图像格式:", format)
|
||||
//err = errors.New("不支持的图像格式")
|
||||
err = errors.New("支持的图像格式为:jpg、png、gif、bmp")
|
||||
return
|
||||
}
|
||||
|
||||
width := bounds.Width
|
||||
height := bounds.Height
|
||||
shortEdge := width
|
||||
if height < width {
|
||||
shortEdge = height
|
||||
}
|
||||
|
||||
if shortEdge < MinShortEdge {
|
||||
fmt.Println("图像尺寸的最短边小于要求:", shortEdge)
|
||||
str := "图像尺寸的最短边小于要求:" + strconv.Itoa(shortEdge)
|
||||
err = errors.New(str)
|
||||
return
|
||||
}
|
||||
|
||||
if width > MaxLongEdge || height > MaxLongEdge {
|
||||
fmt.Println("图像尺寸的最长边超过限制:", width, height)
|
||||
str := "图像尺寸的最长边超过限制:" + strconv.Itoa(width) + strconv.Itoa(height)
|
||||
err = errors.New(str)
|
||||
return
|
||||
}
|
||||
|
||||
// Base64编码图像数据
|
||||
encodedStr := base64.StdEncoding.EncodeToString(imgData)
|
||||
|
||||
// URL编码
|
||||
//urlEncodedStr := url.QueryEscape(encodedStr)
|
||||
|
||||
//fmt.Println("Base64编码并URL编码后的图像数据:", urlEncodedStr)
|
||||
return err, encodedStr
|
||||
}
|
||||
|
||||
/*
|
||||
EncodeAndUrlEncodeImage 图片文件的绝对路径
|
||||
|
||||
功能:
|
||||
图像数据,base64编码后进行urlencode,需去掉编码头(data:image/jpeg;base64, )要求base64编码和urlencode后大小不超过N兆,最短边至少15px,最长边最大4096px,支持jpg/jpeg/png/bmp格式
|
||||
*/
|
||||
func EncodeAndUrlEncodeImage(filePath string, size int64) (string, error) {
|
||||
// 检查文件是否存在
|
||||
_, err := os.Stat(filePath)
|
||||
if os.IsNotExist(err) {
|
||||
return "", fmt.Errorf("文件 %s 不存在", filePath)
|
||||
}
|
||||
|
||||
// 检查文件大小
|
||||
fileSize := getFileSize(filePath)
|
||||
if fileSize > (size * MaxSize) {
|
||||
return "", fmt.Errorf("文件大小超过限制")
|
||||
}
|
||||
|
||||
// 检查图像尺寸
|
||||
img, err := loadImage(filePath)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("无法加载图像: %s", err)
|
||||
}
|
||||
|
||||
bounds := img.Bounds()
|
||||
if !isValidSize(bounds) {
|
||||
return "", fmt.Errorf("图像尺寸不符合要求")
|
||||
}
|
||||
|
||||
// 读取文件
|
||||
fileData, err := ioutil.ReadFile(filePath)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("无法读取文件: %s", err)
|
||||
}
|
||||
|
||||
// 获取文件后缀名
|
||||
ext := filepath.Ext(filePath)
|
||||
|
||||
// 检查是否支持的文件格式
|
||||
if !isSupportedFormat(ext) {
|
||||
return "", fmt.Errorf("不支持的文件格式")
|
||||
}
|
||||
|
||||
// 将图像数据进行 base64 编码
|
||||
encodedData := base64.StdEncoding.EncodeToString(fileData)
|
||||
|
||||
// 去掉编码头(如:"data:image/jpeg;base64,")
|
||||
encodedData = removeEncodingHeader(encodedData, ext)
|
||||
|
||||
//// 对 base64 编码后的数据进行 URL 编码
|
||||
//encodedData = urlEncode(encodedData)
|
||||
|
||||
return encodedData, nil
|
||||
}
|
||||
|
||||
func getFileSize(filePath string) int64 {
|
||||
fileInfo, err := os.Stat(filePath)
|
||||
if err != nil {
|
||||
return 0
|
||||
}
|
||||
return fileInfo.Size()
|
||||
}
|
||||
|
||||
func loadImage(filePath string) (image.Image, error) {
|
||||
file, err := os.Open(filePath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
img, _, err := image.Decode(file)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return img, nil
|
||||
}
|
||||
|
||||
func isValidSize(bounds image.Rectangle) bool {
|
||||
width := bounds.Dx()
|
||||
height := bounds.Dy()
|
||||
if width < MinShortEdge || height < MinShortEdge {
|
||||
return false
|
||||
}
|
||||
if width > MaxLongEdge || height > MaxLongEdge {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func isSupportedFormat(ext string) bool {
|
||||
ext = strings.ToLower(ext)
|
||||
return strings.Contains(SupportedExts, ext)
|
||||
}
|
||||
|
||||
func removeEncodingHeader(encodedData string, ext string) string {
|
||||
header := fmt.Sprintf("data:image/%s;base64,", ext[1:])
|
||||
|
||||
if strings.HasPrefix(encodedData, header) {
|
||||
encodedData = strings.TrimPrefix(encodedData, header)
|
||||
}
|
||||
|
||||
return encodedData
|
||||
}
|
||||
|
||||
func urlEncode(data string) string {
|
||||
return url.PathEscape(data)
|
||||
}
|
38
api/v1/common/coryCommon/largeNumber.go
Normal file
38
api/v1/common/coryCommon/largeNumber.go
Normal file
@ -0,0 +1,38 @@
|
||||
package coryCommon
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"math/big"
|
||||
)
|
||||
|
||||
// PercentageFunc 百分比计算(精度很高:比如说总数100,完成100,最终得到结果为99.99999999%,那么会直接操作成100)
|
||||
// precision:设置精度
|
||||
// total:总数据量
|
||||
// finish:完成度
|
||||
func PercentageFunc(precision uint, total, finish float64) (consequence float64, err error) {
|
||||
if total == 0 {
|
||||
err = errors.New("总数据不能为初始值")
|
||||
return 0, err
|
||||
}
|
||||
if precision == 0 {
|
||||
err = errors.New("精度不能为初始值")
|
||||
return 0, err
|
||||
}
|
||||
consequence = 0
|
||||
// 定义大浮点数
|
||||
numerator := big.NewFloat(100)
|
||||
denominator := big.NewFloat(total)
|
||||
result := big.NewFloat(finish)
|
||||
// 设置精度
|
||||
//var precision uint = 100 // 设置为你需要的精度
|
||||
numerator.SetPrec(precision)
|
||||
denominator.SetPrec(precision)
|
||||
result.SetPrec(precision)
|
||||
// 计算结果
|
||||
result.Quo(result, denominator)
|
||||
result.Mul(result, numerator)
|
||||
// 截取到两位小数
|
||||
resultRounded, _ := result.Float64()
|
||||
consequence = float64(int(resultRounded*100)) / 100 // 保留两位小数
|
||||
return
|
||||
}
|
111
api/v1/common/coryCommon/map.go
Normal file
111
api/v1/common/coryCommon/map.go
Normal file
@ -0,0 +1,111 @@
|
||||
package coryCommon
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/golang/geo/s2"
|
||||
toolTurf "github.com/tiger1103/gfast/v3/api/v1/common/tool/turf"
|
||||
"github.com/tomchavakis/geojson/geometry"
|
||||
"github.com/tomchavakis/turf-go"
|
||||
)
|
||||
|
||||
// DetailedMap shp文件数据
|
||||
type DetailedMap struct {
|
||||
Positions []struct {
|
||||
Lng float64 `json:"lng"`
|
||||
Lat float64 `json:"lat"`
|
||||
Alt float64 `json:"alt"`
|
||||
} `json:"positions"`
|
||||
Width string `json:"width"`
|
||||
Color string `json:"color"`
|
||||
Alpha string `json:"alpha"`
|
||||
Name string `json:"name"`
|
||||
Property string `json:"property"`
|
||||
TxtMemo string `json:"TxtMemo"`
|
||||
ShapeLeng string `json:"Shape_Leng"`
|
||||
ShapeArea string `json:"Shape_Area"`
|
||||
Range struct {
|
||||
MinX float64 `json:"min_x"`
|
||||
MinY float64 `json:"min_y"`
|
||||
MaxX float64 `json:"max_x"`
|
||||
MaxY float64 `json:"max_y"`
|
||||
} `json:"range"`
|
||||
}
|
||||
|
||||
// RectangularFrameRange 是否在矩形框范围内,是否在打卡范围内
|
||||
func RectangularFrameRange(dataInfo DetailedMap, locationLng float64, locationLat float64) (flag bool) {
|
||||
//1、组装数据 84坐标
|
||||
polygon := [][]float64{}
|
||||
for _, data := range dataInfo.Positions {
|
||||
polygon = append(polygon, []float64{data.Lng, data.Lat})
|
||||
}
|
||||
//3、判断位置
|
||||
distance := toolTurf.BooleanPointInPolygon([]float64{locationLng, locationLat}, polygon)
|
||||
if distance {
|
||||
fmt.Println("点在矩形框内")
|
||||
return true
|
||||
} else {
|
||||
fmt.Println("点在矩形框外")
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// FEIQI_RectangularFrameRange 是否在矩形框范围内,是否在打卡范围内 !!!!!!!!!!!!!!!!!有问题
|
||||
func FEIQI_RectangularFrameRange(dataInfo DetailedMap, locationLng float64, locationLat float64) (flag bool) {
|
||||
//1、组装数据
|
||||
var pl geometry.Polygon
|
||||
var ls []geometry.LineString
|
||||
var pt []geometry.Point
|
||||
for _, data := range dataInfo.Positions {
|
||||
wgs84 := LatLng{Latitude: data.Lat, Longitude: data.Lng}
|
||||
t1 := WGS84ToEPSG900913(wgs84)
|
||||
var p geometry.Point
|
||||
p.Lng = t1.Latitude
|
||||
p.Lat = t1.Longitude
|
||||
pt = append(pt, p)
|
||||
}
|
||||
var lsTwo geometry.LineString
|
||||
lsTwo.Coordinates = pt
|
||||
ls = append(ls, lsTwo)
|
||||
pl.Coordinates = append(pl.Coordinates, lsTwo)
|
||||
//2、当前人所在位置
|
||||
locationLng, locationLat = GCJ02toWGS84(locationLng, locationLat)
|
||||
wgs84 := LatLng{Latitude: locationLng, Longitude: locationLat}
|
||||
t1 := WGS84ToEPSG900913(wgs84)
|
||||
myPoint := geometry.Point{
|
||||
Lng: t1.Longitude,
|
||||
Lat: t1.Latitude,
|
||||
}
|
||||
//3、判断myPoint是否在pl框内
|
||||
distance, _ := turf.PointInPolygon(myPoint, pl)
|
||||
if distance {
|
||||
fmt.Println("点在矩形框内")
|
||||
return true
|
||||
} else {
|
||||
fmt.Println("点在矩形框外")
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
type LatLng struct {
|
||||
Latitude float64
|
||||
Longitude float64
|
||||
}
|
||||
|
||||
// WGS84ToEPSG900913 将WGS84坐标转换为EPSG-900913坐标
|
||||
func WGS84ToEPSG900913(wgs84 LatLng) LatLng {
|
||||
// 将WGS84坐标转换为s2.LatLng
|
||||
ll := s2.LatLngFromDegrees(wgs84.Latitude, wgs84.Longitude)
|
||||
|
||||
// 创建s2.Point
|
||||
p := s2.PointFromLatLng(ll)
|
||||
|
||||
// 计算EPSG-900913坐标
|
||||
//epsgX, epsgY := p.Normalize()
|
||||
normalize := p.Normalize()
|
||||
|
||||
// 将坐标范围映射到EPSG-900913的范围
|
||||
epsgX := normalize.X * 20037508.34 / 180.0
|
||||
epsgY := normalize.Y * 20037508.34 / 180.0
|
||||
|
||||
return LatLng{Latitude: epsgY, Longitude: epsgX}
|
||||
}
|
23
api/v1/common/coryCommon/md5.go
Normal file
23
api/v1/common/coryCommon/md5.go
Normal file
@ -0,0 +1,23 @@
|
||||
package coryCommon
|
||||
|
||||
import (
|
||||
"crypto/md5"
|
||||
"encoding/hex"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// MD5Hash 将字符串进行 MD5 加密,并返回 32 位长度的 MD5 散列值
|
||||
func MD5Hash(input string) string {
|
||||
// 将输入字符串转换为小写,以确保不区分大小写
|
||||
input = strings.ToLower(input)
|
||||
// 创建 MD5 散列对象
|
||||
md5Hash := md5.New()
|
||||
// 将输入字符串转换为字节数组,并传递给 MD5 散列对象
|
||||
_, _ = md5Hash.Write([]byte(input))
|
||||
// 计算 MD5 散列值
|
||||
hashBytes := md5Hash.Sum(nil)
|
||||
// 将散列值转换为 32 位的十六进制字符串
|
||||
md5HashString := hex.EncodeToString(hashBytes)
|
||||
|
||||
return md5HashString
|
||||
}
|
69
api/v1/common/coryCommon/restapi.go
Normal file
69
api/v1/common/coryCommon/restapi.go
Normal file
@ -0,0 +1,69 @@
|
||||
package coryCommon
|
||||
|
||||
import (
|
||||
"github.com/gogf/gf/v2/net/gclient"
|
||||
"github.com/gogf/gf/v2/os/gctx"
|
||||
)
|
||||
|
||||
// 高德地图API https://lbs.amap.com/api/webservice/guide/api/georegeo
|
||||
|
||||
// InverseGeocoding 逆地理编码
|
||||
func InverseGeocoding(location string) (we string) {
|
||||
//请求路径
|
||||
key := "3bbede95174c607a1ed4c479d3f637cc"
|
||||
requestURL := "https://restapi.amap.com/v3/geocode/regeo?location=" + location + "&key=" + key
|
||||
|
||||
response, err := gclient.New().ContentJson().ContentType("application/x-www-form-urlencoded").Get(gctx.New(), requestURL)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
var dataInfo = ""
|
||||
dataInfo = response.ReadAllString()
|
||||
return dataInfo
|
||||
}
|
||||
|
||||
type InverseGeocodingRep struct {
|
||||
Status string `json:"status"`
|
||||
Regeocode Regeocode `json:"regeocode"`
|
||||
Info string `json:"info"`
|
||||
Infocode string `json:"infocode"`
|
||||
}
|
||||
|
||||
type Regeocode struct {
|
||||
FormattedAddress string `json:"formatted_address"`
|
||||
AddressComponent AddressComponent `json:"addressComponent"`
|
||||
}
|
||||
|
||||
type AddressComponent struct {
|
||||
//City string `json:"city"`
|
||||
Province string `json:"province"`
|
||||
Adcode string `json:"adcode"`
|
||||
District string `json:"district"`
|
||||
Towncode string `json:"towncode"`
|
||||
//StreetNumber StreetNumber `json:"streetNumber"`
|
||||
Country string `json:"country"`
|
||||
Township string `json:"township"`
|
||||
//BusinessAreas []One `json:"businessAreas"`
|
||||
//Building Two `json:"building"`
|
||||
//Neighborhood Two `json:"neighborhood"`
|
||||
Citycode string `json:"citycode"`
|
||||
}
|
||||
|
||||
type StreetNumber struct {
|
||||
Number string `json:"number"`
|
||||
Location string `json:"location"`
|
||||
Direction string `json:"direction"`
|
||||
Distance string `json:"distance"`
|
||||
Street string `json:"street"`
|
||||
}
|
||||
|
||||
type One struct {
|
||||
Location string `json:"location"`
|
||||
Name string `json:"name"`
|
||||
Id string `json:"id"`
|
||||
}
|
||||
|
||||
type Two struct {
|
||||
Name []string `json:"name"`
|
||||
Type []string `json:"type"`
|
||||
}
|
473
api/v1/common/coryCommon/uploadFile.go
Normal file
473
api/v1/common/coryCommon/uploadFile.go
Normal file
@ -0,0 +1,473 @@
|
||||
package coryCommon
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gogf/gf/v2/net/ghttp"
|
||||
"github.com/tiger1103/gfast/v3/library/liberr"
|
||||
"golang.org/x/exp/rand"
|
||||
)
|
||||
|
||||
func UploadFileS(ctx context.Context, fileHeader []*ghttp.UploadFile, fileUrl string, typeStr string) (pathStr string, err error) {
|
||||
for i := range fileHeader {
|
||||
var str string
|
||||
if typeStr == "1" {
|
||||
str, err = UploadFile(ctx, fileHeader[i], fileUrl)
|
||||
}
|
||||
if typeStr == "2" {
|
||||
str, err = UploadFileTwo(ctx, fileHeader[i], fileUrl)
|
||||
}
|
||||
if err != nil {
|
||||
liberr.ErrIsNil(ctx, err)
|
||||
return
|
||||
}
|
||||
pathStr = pathStr + ResourcePublicToFunc("/"+str, 0) + ","
|
||||
}
|
||||
pathStr = pathStr[0 : len(pathStr)-1]
|
||||
return
|
||||
}
|
||||
|
||||
// UploadFile [文件名称为原来的文件名称] 上传文件(流) 也可以将http图片上传 返回:resource/public/upload_file/2023-11-28/tmp_c7858f0fd98d74cbe2c095125871aaf19c749c209ae33f5f.png
|
||||
func UploadFile(ctx context.Context, fileHeader *ghttp.UploadFile, fileUrl string) (str string, err error) {
|
||||
// 获取上传的文件流
|
||||
if fileHeader == nil {
|
||||
log.Println("Failed to get file")
|
||||
liberr.ErrIsNil(ctx, err, "Failed to get file")
|
||||
return "", err
|
||||
}
|
||||
ynr := Ynr(fileUrl)
|
||||
// 在当前目录创建一个新文件用于保存上传的数据
|
||||
lj := "/" + ynr + fileHeader.Filename
|
||||
destFilePath := filepath.Join(".", lj)
|
||||
destFile, err := os.Create(destFilePath)
|
||||
if err != nil {
|
||||
log.Println("Failed to create destination file:", err)
|
||||
liberr.ErrIsNil(ctx, err)
|
||||
return "", err
|
||||
}
|
||||
defer destFile.Close()
|
||||
|
||||
// 创建一个内存缓冲区作为文件数据的临时存储
|
||||
buffer := make([]byte, 4096)
|
||||
|
||||
// 记录已接收的数据量,用于计算上传进度
|
||||
var totalReceived int64
|
||||
|
||||
// 获取文件上传的数据流
|
||||
file, err := fileHeader.Open()
|
||||
if err != nil {
|
||||
log.Println("Failed to open file:", err)
|
||||
liberr.ErrIsNil(ctx, err)
|
||||
return "", err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
// 循环读取文件流,直到读取完整个文件
|
||||
for {
|
||||
// 从文件流中读取数据到缓冲区
|
||||
n, err := file.Read(buffer)
|
||||
if err != nil && err != io.EOF {
|
||||
log.Println("Failed to read file:", err)
|
||||
liberr.ErrIsNil(ctx, err)
|
||||
return "", err
|
||||
}
|
||||
|
||||
// 如果缓冲区没有数据,则表示已读取完整个文件
|
||||
if n == 0 {
|
||||
break
|
||||
}
|
||||
|
||||
// 将缓冲区的数据写入目标文件
|
||||
_, err = destFile.Write(buffer[:n])
|
||||
if err != nil {
|
||||
log.Println("Failed to write file:", err)
|
||||
liberr.ErrIsNil(ctx, err)
|
||||
return "", err
|
||||
}
|
||||
|
||||
// 更新已接收的数据量
|
||||
totalReceived += int64(n)
|
||||
}
|
||||
|
||||
//// 返回上传文件的路径给客户端
|
||||
//uploadPath, err := filepath.Abs(destFilePath)
|
||||
//if err != nil {
|
||||
// log.Println("Failed to get absolute file path:", err)
|
||||
// liberr.ErrIsNil(ctx, err)
|
||||
// return "", err
|
||||
//}
|
||||
//统一路径斜杠为/
|
||||
str = filepath.ToSlash(lj)
|
||||
return str, err
|
||||
}
|
||||
|
||||
func UploadUniqueFile(ctx context.Context, fileHeader *ghttp.UploadFile, fileUrl string) (str string, err error) {
|
||||
// 获取上传的文件流
|
||||
if fileHeader == nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
ynr := Ynr(fileUrl)
|
||||
|
||||
// 在当前目录创建一个新文件用于保存上传的数据
|
||||
// 在文件名中添加一个唯一的标识符,例如当前的时间戳
|
||||
lj := fmt.Sprintf("%s_%d_%s", ynr, time.Now().Unix(), fileHeader.Filename)
|
||||
destFilePath := filepath.Join(".", lj)
|
||||
destFile, err := os.Create(destFilePath)
|
||||
if err != nil {
|
||||
log.Println("Failed to create destination file:", err)
|
||||
liberr.ErrIsNil(ctx, err)
|
||||
return "", err
|
||||
}
|
||||
defer destFile.Close()
|
||||
|
||||
// 创建一个内存缓冲区作为文件数据的临时存储
|
||||
buffer := make([]byte, 4096)
|
||||
|
||||
// 记录已接收的数据量,用于计算上传进度
|
||||
var totalReceived int64
|
||||
|
||||
// 获取文件上传的数据流
|
||||
file, err := fileHeader.Open()
|
||||
if err != nil {
|
||||
log.Println("Failed to open file:", err)
|
||||
liberr.ErrIsNil(ctx, err)
|
||||
return "", err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
// 循环读取文件流,直到读取完整个文件
|
||||
for {
|
||||
// 从文件流中读取数据到缓冲区
|
||||
n, err := file.Read(buffer)
|
||||
if err != nil && err != io.EOF {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// 如果缓冲区没有数据,则表示已读取完整个文件
|
||||
if n == 0 {
|
||||
break
|
||||
}
|
||||
|
||||
// 将缓冲区的数据写入目标文件
|
||||
_, err = destFile.Write(buffer[:n])
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// 更新已接收的数据量
|
||||
totalReceived += int64(n)
|
||||
}
|
||||
|
||||
// 统一路径斜杠为/
|
||||
str = filepath.ToSlash(lj)
|
||||
return str, err
|
||||
}
|
||||
|
||||
// UploadFileTwo [文件名称为时间戳] 上传文件(流) 也可以将http图片上传 返回:resource/public/upload_file/2023-11-28/tmp_c7858f0fd98d74cbe2c095125871aaf19c749c209ae33f5f.png
|
||||
func UploadFileTwo(ctx context.Context, fileHeader *ghttp.UploadFile, fileUrl string) (str string, err error) {
|
||||
// 获取上传的文件流
|
||||
if fileHeader == nil {
|
||||
log.Println("Failed to get file")
|
||||
liberr.ErrIsNil(ctx, err, "Failed to get file")
|
||||
return "", err
|
||||
}
|
||||
ynr := Ynr(fileUrl)
|
||||
// 在当前目录创建一个新文件用于保存上传的数据
|
||||
lj := /* "/" +*/ ynr + FileName("login") + filepath.Ext(fileHeader.Filename)
|
||||
|
||||
destFilePath := filepath.Join(".", lj)
|
||||
destFile, err := os.Create(destFilePath)
|
||||
if err != nil {
|
||||
log.Println("Failed to create destination file:", err)
|
||||
liberr.ErrIsNil(ctx, err)
|
||||
return "", err
|
||||
}
|
||||
defer destFile.Close()
|
||||
|
||||
// 创建一个内存缓冲区作为文件数据的临时存储
|
||||
buffer := make([]byte, 4096)
|
||||
|
||||
// 记录已接收的数据量,用于计算上传进度
|
||||
var totalReceived int64
|
||||
|
||||
// 获取文件上传的数据流
|
||||
file, err := fileHeader.Open()
|
||||
if err != nil {
|
||||
log.Println("Failed to open file:", err)
|
||||
liberr.ErrIsNil(ctx, err)
|
||||
return "", err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
// 循环读取文件流,直到读取完整个文件
|
||||
for {
|
||||
// 从文件流中读取数据到缓冲区
|
||||
n, err := file.Read(buffer)
|
||||
if err != nil && err != io.EOF {
|
||||
log.Println("Failed to read file:", err)
|
||||
liberr.ErrIsNil(ctx, err)
|
||||
return "", err
|
||||
}
|
||||
|
||||
// 如果缓冲区没有数据,则表示已读取完整个文件
|
||||
if n == 0 {
|
||||
break
|
||||
}
|
||||
|
||||
// 将缓冲区的数据写入目标文件
|
||||
_, err = destFile.Write(buffer[:n])
|
||||
if err != nil {
|
||||
log.Println("Failed to write file:", err)
|
||||
liberr.ErrIsNil(ctx, err)
|
||||
return "", err
|
||||
}
|
||||
|
||||
// 更新已接收的数据量
|
||||
totalReceived += int64(n)
|
||||
}
|
||||
|
||||
//// 返回上传文件的路径给客户端
|
||||
//uploadPath, err := filepath.Abs(destFilePath)
|
||||
//if err != nil {
|
||||
// log.Println("Failed to get absolute file path:", err)
|
||||
// liberr.ErrIsNil(ctx, err)
|
||||
// return "", err
|
||||
//}
|
||||
//统一路径斜杠为/
|
||||
str = filepath.ToSlash(lj)
|
||||
return str, err
|
||||
}
|
||||
|
||||
// Ynr 创建时间文件夹 传递相对路径/resource/public/ 返回相对路径resource/public/2023-11-12/
|
||||
func Ynr(baseDir string) (str string) {
|
||||
// 获取当前时间
|
||||
currentTime := time.Now()
|
||||
// 格式化为年月日的字符串格式
|
||||
dateString := currentTime.Format("2006-01-02")
|
||||
dateString = baseDir + dateString
|
||||
// 在相对路径上添加文件夹
|
||||
destFilePath := filepath.Join(".", dateString)
|
||||
dateString = destFilePath
|
||||
// 检查文件夹是否已存在
|
||||
_, err := os.Stat(dateString)
|
||||
if os.IsNotExist(err) {
|
||||
// 文件夹不存在,创建文件夹
|
||||
err := os.MkdirAll(dateString, os.ModePerm)
|
||||
if err != nil {
|
||||
fmt.Println("创建文件夹失败:", err)
|
||||
return
|
||||
}
|
||||
// fmt.Println("文件夹创建成功:", dateString)
|
||||
} else {
|
||||
// fmt.Println("文件夹已存在:", dateString)
|
||||
}
|
||||
return dateString + "/"
|
||||
}
|
||||
|
||||
// FileName 【前缀】+获取当前时间+随机数得到文件名
|
||||
func FileName(prefix string) (uniqueFileName string) {
|
||||
currentTime := time.Now()
|
||||
timestamp := currentTime.UnixNano() / int64(time.Millisecond)
|
||||
randomNum := rand.Intn(1000)
|
||||
uniqueFileName = fmt.Sprintf("%d_%d", timestamp, randomNum)
|
||||
if prefix != "" {
|
||||
uniqueFileName = prefix + uniqueFileName
|
||||
}
|
||||
return uniqueFileName
|
||||
}
|
||||
|
||||
// FileInfo 传参:相对路径 获取文件信息(文件名称、文件后缀、文件大小(字节))
|
||||
func FileInfo(filePath string) (name string, ext string, size int64) {
|
||||
newPath := ""
|
||||
if strings.Contains(filePath, "wxfile") {
|
||||
newPath = strings.ReplaceAll(filePath, "/wxfile", GetCWD()+"/resource/public")
|
||||
} else {
|
||||
newPath = strings.ReplaceAll(filePath, "/file", GetCWD()+"/resource/public")
|
||||
}
|
||||
newPath = strings.ReplaceAll(newPath, "\\", "/")
|
||||
filePath = newPath
|
||||
// filePath = "D:\\Cory\\go\\中煤\\zmkg-back\\resource\\public\\upload_file\\2023-08-31\\cv6kxb89pd984rt5ze.png"
|
||||
// 获取文件的基本名称(包括扩展名)
|
||||
fileName := filepath.Base(filePath)
|
||||
// 分割文件名和扩展名
|
||||
nameParts := strings.Split(fileName, ".")
|
||||
fileNameWithoutExt := nameParts[0]
|
||||
fileExt := nameParts[1]
|
||||
// 获取文件大小
|
||||
fileInfo, err := os.Stat(filePath)
|
||||
if err != nil {
|
||||
fmt.Println("无法获取文件信息:", err)
|
||||
return
|
||||
}
|
||||
fileSize := fileInfo.Size()
|
||||
// 文件名称、文件后缀、文件大小(字节)
|
||||
name = fileNameWithoutExt
|
||||
ext = fileExt
|
||||
size = fileSize
|
||||
return
|
||||
}
|
||||
|
||||
// OutJson 创建txt写入数据,jsonData数据、absPath 绝对路径(带文件名和后缀)
|
||||
func OutJson(jsonData []byte, absPath string) (flag bool, err error) {
|
||||
absPath = strings.ReplaceAll(absPath, "\\", "/")
|
||||
absPath = strings.ReplaceAll(absPath, "/file", GetCWD()+"/resource/public")
|
||||
flag = false
|
||||
// 创建要写入的文件
|
||||
file, err := os.Create(absPath)
|
||||
if err != nil {
|
||||
fmt.Println("无法创建文件:", err)
|
||||
return
|
||||
}
|
||||
defer file.Close()
|
||||
// 将 JSON 数据写入文件
|
||||
_, err = file.Write(jsonData)
|
||||
if err != nil {
|
||||
fmt.Println("无法写入文件:", err)
|
||||
return
|
||||
}
|
||||
return true, err
|
||||
}
|
||||
|
||||
// PutJson 读取文件
|
||||
func PutJson(filePath string) (jsonData string, err error) {
|
||||
filePath = strings.ReplaceAll(filePath, "/file", GetCWD()+"/resource/public")
|
||||
filePath = strings.ReplaceAll(filePath, "\\", "/")
|
||||
// filePath := "relative/path/to/file.txt"
|
||||
file, err := os.Open(filePath)
|
||||
if err != nil {
|
||||
fmt.Println("打开文件错误:", err)
|
||||
return
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
scanner := bufio.NewScanner(file)
|
||||
// 设置一个足够大的缓冲区
|
||||
const maxScanTokenSize = 128 * 1024
|
||||
buf := make([]byte, maxScanTokenSize)
|
||||
scanner.Buffer(buf, maxScanTokenSize)
|
||||
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
jsonData = jsonData + line
|
||||
}
|
||||
if scanner.Err() != nil {
|
||||
fmt.Println("读取文件错误:", scanner.Err())
|
||||
err = scanner.Err()
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
/*
|
||||
BatchFile 批量刪除文件
|
||||
filePath 相对路径
|
||||
*/
|
||||
func BatchFile(filePath []string) {
|
||||
for _, data := range filePath {
|
||||
// if strings.Contains(data, "file") || strings.Contains(data, "wxfile"){
|
||||
newPath := ""
|
||||
if strings.Contains(data, "/file") {
|
||||
newPath = strings.Replace(data, "/file", GetCWD()+"/resource/public", 1)
|
||||
} else if strings.Contains(data, "/wxfile") {
|
||||
newPath = strings.Replace(data, "/wxfile", GetCWD()+"/resource/public", 1)
|
||||
}
|
||||
os.Remove(strings.ReplaceAll(newPath, "\\", "/"))
|
||||
}
|
||||
}
|
||||
|
||||
// FlagImg 判斷是否是圖片
|
||||
func FlagImg(filePath string) (flag bool) {
|
||||
filePathLower := strings.ToLower(filePath)
|
||||
isImage := strings.HasSuffix(filePathLower, ".png") || strings.HasSuffix(filePathLower, ".jpg") || strings.HasSuffix(filePathLower, ".jpeg") || strings.HasSuffix(filePathLower, ".gif") || strings.HasSuffix(filePathLower, ".bmp")
|
||||
if isImage {
|
||||
flag = true
|
||||
} else {
|
||||
flag = false
|
||||
}
|
||||
return flag
|
||||
}
|
||||
|
||||
// CreateDirectory 判断文件夹是否存在,不存在则创建文件夹
|
||||
func CreateDirectory(folderName string) error {
|
||||
// 检查文件夹是否已经存在
|
||||
_, err := os.Stat(folderName)
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
// 创建文件夹
|
||||
err = os.Mkdir(folderName, 0o755)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// FileToFunc file转resource/public
|
||||
func FileToFunc(path string, num int) (rpath string) {
|
||||
if num == 1 {
|
||||
rpath = strings.Replace(path, GetCWD()+"/file/", "/resource/public/", 1)
|
||||
} else if num == 2 {
|
||||
rpath = strings.Replace(path, "/file/", GetCWD()+"/resource/public/", 1)
|
||||
} else if num == 3 {
|
||||
rpath = strings.Replace(path, "/file/", "/wxfile/", 1)
|
||||
} else if num == 4 {
|
||||
rpath = strings.Replace(path, "/wxfile/", GetCWD()+"/resource/public/", 1)
|
||||
} else if num == 5 {
|
||||
rpath = strings.Replace(path, "/wxfile/", "/file/", 1)
|
||||
} else {
|
||||
rpath = strings.Replace(path, "/file/", "/resource/public/", 1)
|
||||
}
|
||||
rpath = filepath.ToSlash(rpath)
|
||||
return
|
||||
}
|
||||
|
||||
// ResourcePublicToFunc resource/public转file
|
||||
func ResourcePublicToFunc(path string, num int) (rpath string) {
|
||||
if num == 1 {
|
||||
rpath = strings.Replace(path, GetCWD()+"/resource/public/", "/file/", 1)
|
||||
} else if num == 2 {
|
||||
rpath = strings.Replace(path, "/resource/public/", GetCWD()+"/resource/public/", 1)
|
||||
} else if num == 3 {
|
||||
rpath = strings.Replace(path, "/resource/public/", "/wxfile/", 1)
|
||||
} else {
|
||||
rpath = strings.Replace(path, "/resource/public/", "/file/", 1)
|
||||
}
|
||||
rpath = filepath.ToSlash(rpath)
|
||||
return
|
||||
}
|
||||
|
||||
// FormatRestrictionFunc 判断文件后缀是否匹配
|
||||
func FormatRestrictionFunc(path, suffix string) (flag bool) {
|
||||
split := strings.Split(suffix, ",")
|
||||
for _, data := range split {
|
||||
extension := filepath.Ext(path)
|
||||
if extension == data {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// URLCoding 对url的特俗符号进行编码,不对/进行编码(定制编码,将"/file/networkDisk/completion/admin37/2.jpg"的"networkDisk/completion/admin37/2.jpg"进行编码)
|
||||
func URLCoding(path string) (filenPathCoding string) {
|
||||
s2 := path[6 : len(path)-1]
|
||||
split := strings.Split(s2, "/")
|
||||
p := ""
|
||||
for i2 := range split {
|
||||
p = p + url.PathEscape(split[i2]) + "/"
|
||||
}
|
||||
p = p[0 : len(p)-1]
|
||||
filenPathCoding = strings.ReplaceAll(path, s2, p)
|
||||
return
|
||||
}
|
199
api/v1/common/coryCommon/watermark.go
Normal file
199
api/v1/common/coryCommon/watermark.go
Normal file
@ -0,0 +1,199 @@
|
||||
package coryCommon
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/fogleman/gg"
|
||||
"github.com/nfnt/resize"
|
||||
"image/color"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func GetCWD() string {
|
||||
cwd, _ := os.Getwd()
|
||||
return filepath.ToSlash(cwd)
|
||||
}
|
||||
|
||||
func MultiPicture(pictureList string, compere string, meetingDate string, site string, teamName string, labourserviceName string) {
|
||||
//1、分割字符串
|
||||
split := strings.Split(pictureList, ",")
|
||||
//2、循环添加水印
|
||||
for i := range split {
|
||||
filePath := split[i]
|
||||
newPath := strings.ReplaceAll(filePath, "/wxfile", GetCWD()+"/resource/public")
|
||||
newPath = strings.ReplaceAll(newPath, "\\", "/")
|
||||
WatermarkFunc(newPath, compere, meetingDate, site, teamName, labourserviceName)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// WatermarkFunc 给图片设置水印和logo
|
||||
func WatermarkFunc(filePath string, compere string, meetingDate string, site string, teamName string, labourserviceName string) {
|
||||
|
||||
// 检查文件是否存在
|
||||
_, err := os.Stat(filePath)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
fmt.Println("文件不存在")
|
||||
} else {
|
||||
fmt.Println("发生错误:", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
//// 获取文件名和后缀
|
||||
//fileName := filepath.Base(filePath)
|
||||
//fileExt := filepath.Ext(filePath)
|
||||
|
||||
//1、加载图片
|
||||
srcImage, err := gg.LoadImage(filePath)
|
||||
if err != nil {
|
||||
log.Fatalf("打开图片失败: %v", err)
|
||||
}
|
||||
//2、加载logo水印
|
||||
dc := gg.NewContextForImage(srcImage)
|
||||
dc.SetRGB(1, 1, 1)
|
||||
logoImage, err := gg.LoadImage(GetCWD() + "/resource/cory/zmlogo.jpg")
|
||||
if err != nil {
|
||||
log.Fatalf("打开 logo 图片失败: %v", err)
|
||||
}
|
||||
logoWidth := 80.0
|
||||
logoHeight := float64(logoImage.Bounds().Dy()) * (logoWidth / float64(logoImage.Bounds().Dx()))
|
||||
logoImage = resize.Resize(uint(logoWidth), uint(logoHeight), logoImage, resize.Lanczos3)
|
||||
x := float64(logoWidth) + 10.0
|
||||
y := float64(dc.Height()/2) + 96.0
|
||||
dc.DrawImageAnchored(logoImage, int(x), int(y), 1.0, 1.0)
|
||||
//3、设置字体
|
||||
fontPath := GetCWD() + "/resource/cory/msyh.ttc"
|
||||
if err != nil {
|
||||
log.Fatalf("加载字体失败: %v", err)
|
||||
}
|
||||
dc.SetRGB(0, 0, 0)
|
||||
//4、创建矩形框 背景透明
|
||||
boxText := teamName
|
||||
dc.SetRGBA255(0, 99, 175, 100)
|
||||
rectangleX := x
|
||||
rectangleY := y - logoHeight + 1
|
||||
rectangleWidth := len(boxText) * 8
|
||||
rectangleHeight := logoHeight - 1
|
||||
dc.DrawRectangle(rectangleX, rectangleY, float64(rectangleWidth), rectangleHeight)
|
||||
dc.Fill()
|
||||
textFunc(dc, boxText, fontPath, 18.0, rectangleX, rectangleY, float64(rectangleWidth), rectangleHeight, 1)
|
||||
//5、添加文字水印
|
||||
text := "开会宣讲人:" + compere + "\n \n" +
|
||||
"开 会 时 间:" + meetingDate + "\n \n" +
|
||||
//"天 气:多云转晴\n \n" +
|
||||
"地 点:" + site + "\n \n"
|
||||
textX := x - logoWidth
|
||||
textY := y + 10
|
||||
err = dc.LoadFontFace(fontPath, 12)
|
||||
dc.DrawStringWrapped(text, textX, textY, 0.0, 0.0, float64(dc.Width())-textX, 1.2, gg.AlignLeft)
|
||||
//6、创建矩形框 渐变透明
|
||||
boxText = labourserviceName
|
||||
width := len(boxText) * 8
|
||||
height := 30
|
||||
fromAlpha := 0.6
|
||||
toAlpha := 0
|
||||
for x := 0; x <= width; x++ {
|
||||
alpha := fromAlpha - (fromAlpha-float64(toAlpha))*(float64(x)/float64(width))
|
||||
rgba := color.RGBA{G: 99, B: 175, A: uint8(alpha * 255)}
|
||||
dc.SetRGBA255(int(rgba.R), int(rgba.G), int(rgba.B), int(rgba.A))
|
||||
dc.DrawLine(float64(x)+10, y+96, float64(x)+10, y+96+float64(height))
|
||||
dc.StrokePreserve()
|
||||
dc.Fill()
|
||||
}
|
||||
textFunc(dc, boxText, fontPath, 16.0, 10.0, y+96, float64(width), float64(height), 2)
|
||||
//7、保存图片
|
||||
err = dc.SavePNG(filePath)
|
||||
if err != nil {
|
||||
log.Fatalf("保存带水印的图片失败: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func textFunc(dc *gg.Context, boxText string, fontPath string, boxTextSize, rectangleX, rectangleY, rectangleWidth, rectangleHeight float64, num int) {
|
||||
err := dc.LoadFontFace(fontPath, boxTextSize)
|
||||
if err != nil {
|
||||
log.Fatalf("加载字体失败: %v", err)
|
||||
}
|
||||
dc.SetRGB(1, 1, 1)
|
||||
boxTextWidth, boxTextHeight := dc.MeasureString(boxText)
|
||||
boxTextX := 0.00
|
||||
if num == 1 {
|
||||
boxTextX = rectangleX + (rectangleWidth-boxTextWidth)/2
|
||||
} else if num == 2 {
|
||||
boxTextX = 10
|
||||
} else {
|
||||
log.Fatalf("对齐方式错误")
|
||||
}
|
||||
boxTextY := rectangleY + (rectangleHeight-boxTextHeight)/2 + boxTextHeight
|
||||
dc.DrawStringAnchored(boxText, boxTextX, boxTextY, 0.0, 0.0)
|
||||
}
|
||||
|
||||
// 绘制矩形框
|
||||
func Test_draw_rect_text(im_path string, x, y, w, h float64) {
|
||||
// Load image
|
||||
//font_path := GetCWD() + "/resource/cory/msyh.ttc"
|
||||
im, err := gg.LoadImage(im_path)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
// 2 method
|
||||
dc := gg.NewContextForImage(im)
|
||||
|
||||
// Set color and line width
|
||||
dc.SetHexColor("#FF0000")
|
||||
dc.SetLineWidth(4)
|
||||
|
||||
// DrawRoundedRectangle 使用 DrawRoundedRectangle 方法在图像上绘制一个带有圆角的矩形。这里 x, y 是矩形左上角的坐标,w, h 是矩形的宽度和高度,最后的 0 表示圆角的半径为0。
|
||||
dc.DrawRoundedRectangle(x, y, w, h, 0)
|
||||
// Store set
|
||||
dc.Stroke()
|
||||
|
||||
dc.DrawRectangle(x, y, w, h)
|
||||
dc.Clip()
|
||||
|
||||
// Save png image
|
||||
dc.SavePNG(im_path)
|
||||
}
|
||||
|
||||
type TestDrawRectTextEntity struct {
|
||||
ImPath string `json:"im_path"`
|
||||
Coordinates []*CoordinatesListEntity `json:"coordinates"`
|
||||
}
|
||||
|
||||
type CoordinatesListEntity struct {
|
||||
X float64 `json:"x"`
|
||||
Y float64 `json:"y"`
|
||||
W float64 `json:"w"`
|
||||
H float64 `json:"h"`
|
||||
}
|
||||
|
||||
// TestDrawRectTextFunc 同一文件多次绘制矩形框
|
||||
func TestDrawRectTextFunc(entity *TestDrawRectTextEntity) {
|
||||
if entity == nil {
|
||||
return
|
||||
}
|
||||
if len(entity.Coordinates) == 0 {
|
||||
return
|
||||
}
|
||||
// 加载图像
|
||||
im, err := gg.LoadImage(entity.ImPath) // 加载图像一次
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
// 创建上下文
|
||||
dc := gg.NewContextForImage(im)
|
||||
// 设置颜色和线宽
|
||||
dc.SetHexColor("#FF0000")
|
||||
dc.SetLineWidth(4)
|
||||
//绘制和保存矩形
|
||||
for _, zuobiao := range entity.Coordinates {
|
||||
// 绘制矩形
|
||||
dc.DrawRoundedRectangle(zuobiao.X, zuobiao.Y, zuobiao.W, zuobiao.H, 0)
|
||||
}
|
||||
dc.Stroke()
|
||||
// 保存图像
|
||||
dc.SavePNG(entity.ImPath)
|
||||
}
|
166
api/v1/common/coryCommon/weChatNewsFeeds.go
Normal file
166
api/v1/common/coryCommon/weChatNewsFeeds.go
Normal file
@ -0,0 +1,166 @@
|
||||
package coryCommon
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"github.com/gogf/gf/v2/os/gctx"
|
||||
commonService "github.com/tiger1103/gfast/v3/internal/app/common/service"
|
||||
"github.com/tiger1103/gfast/v3/internal/app/system/dao"
|
||||
"github.com/tiger1103/gfast/v3/internal/app/system/model"
|
||||
wxModel "github.com/tiger1103/gfast/v3/internal/app/wxApplet/model"
|
||||
tool "github.com/tiger1103/gfast/v3/utility/coryUtils"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// 微信小程序的消息订阅 https://developers.weixin.qq.com/miniprogram/dev/OpenApiDoc/mp-message-management/subscribe-message/sendMessage.html
|
||||
|
||||
type TokenEntity struct {
|
||||
AccessToken string `json:"accessToken"`
|
||||
ExpiresIn string `json:"expiresIn"`
|
||||
}
|
||||
|
||||
func GetAccessToken() (token string, err error) {
|
||||
var te *TokenEntity
|
||||
appId, _ := g.Cfg().Get(gctx.New(), "wx.appId")
|
||||
appSecret, _ := g.Cfg().Get(gctx.New(), "wx.appSecret")
|
||||
key := "weChatAccessToken"
|
||||
//从缓存捞取key
|
||||
ctx := gctx.New()
|
||||
get := commonService.Cache().Get(ctx, key)
|
||||
if get != nil && get.String() != "" {
|
||||
token = get.String()
|
||||
return "", err
|
||||
} else {
|
||||
uri := "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=" + appId.String() + "&secret=" + appSecret.String()
|
||||
response, err := g.Client().Get(gctx.New(), uri)
|
||||
if err != nil {
|
||||
return "", err
|
||||
} else {
|
||||
allString := response.ReadAllString()
|
||||
err := json.Unmarshal([]byte(allString), &te)
|
||||
if err != nil {
|
||||
return "", err
|
||||
} else {
|
||||
//将token存储到redis中,tiken默认时间为秒,实际计算为2小时,(这里少100秒,防止token过期还存在redis中)
|
||||
num, err := strconv.Atoi(te.ExpiresIn)
|
||||
if err != nil {
|
||||
err = errors.New("过期时间转换失败!")
|
||||
return "", err
|
||||
}
|
||||
commonService.Cache().Set(ctx, key, te.AccessToken, time.Duration(num-100)*time.Second)
|
||||
token = te.AccessToken
|
||||
return token, err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type AppletSubscription struct {
|
||||
Touser string `json:"touser"`
|
||||
TemplateId string `json:"template_id"`
|
||||
MiniprogramState string `json:"MiniprogramState"`
|
||||
Data DataEntity `json:"data"`
|
||||
}
|
||||
|
||||
type DataEntity struct {
|
||||
Date1 ValueEntity `json:"date1"`
|
||||
Thing2 ValueEntity `json:"thing2"`
|
||||
Thing3 ValueEntity `json:"thing3"`
|
||||
}
|
||||
|
||||
type ValueEntity struct {
|
||||
Value string `json:"value"`
|
||||
}
|
||||
|
||||
type MsgEntity struct {
|
||||
ErrCode int `json:"errcode"`
|
||||
ErrMsg int64 `json:"errmsg"`
|
||||
MsgId string `json:"msgid"`
|
||||
}
|
||||
|
||||
// Subscription 微信服务通知(消息订阅)
|
||||
func Subscription(openid string) (msgEntity *MsgEntity, err error) {
|
||||
//1、获取token
|
||||
token, err := GetAccessToken()
|
||||
if err != nil {
|
||||
fmt.Println("获取微信凭证错误!")
|
||||
return
|
||||
}
|
||||
//2、组装数据
|
||||
//now := time.Now()
|
||||
var entity = new(AppletSubscription)
|
||||
entity.Touser = openid
|
||||
entity.TemplateId = "EyBO6gWizF5HwUThYSSm_HuQWgfMrwEkVHPXeEq1Me8"
|
||||
entity.MiniprogramState = "trial"
|
||||
var dataEntity = DataEntity{}
|
||||
dataEntity.Date1 = ValueEntity{Value: time.Now().Format("2006-01-02 15:04:05")}
|
||||
dataEntity.Thing2 = ValueEntity{Value: "您今日还有打卡未完成!"}
|
||||
dataEntity.Thing3 = ValueEntity{Value: "请登录中煤小程序进行打卡操作!"}
|
||||
entity.Data = dataEntity
|
||||
marshal, _ := json.Marshal(entity)
|
||||
|
||||
//3、发起请求
|
||||
msgEntity = new(MsgEntity)
|
||||
uri := "https://api.weixin.qq.com/cgi-bin/message/subscribe/send?access_token=" + token
|
||||
post, err := g.Client().Post(gctx.New(), uri, marshal)
|
||||
|
||||
postBytes, err := json.Marshal(post)
|
||||
if err != nil {
|
||||
fmt.Println("JSON marshaling error:", err)
|
||||
return
|
||||
}
|
||||
if err := json.Unmarshal(postBytes, &msgEntity); err != nil {
|
||||
fmt.Println("解析JSON错误:", err)
|
||||
return nil, err
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func ServiceNoticeFunc(ctx context.Context) {
|
||||
//1、获取所有项目的打卡范围
|
||||
var projectEntity []*model.SysProjectListRes
|
||||
err := dao.SysProject.Ctx(ctx).
|
||||
Where("status", 0).
|
||||
Where("show_hidden", 1).
|
||||
Fields("id,project_name,punch_range").Scan(&projectEntity)
|
||||
if err != nil {
|
||||
fmt.Println("获取项目失败!")
|
||||
}
|
||||
//2、遍历项目获取打卡范围
|
||||
for _, pData := range projectEntity {
|
||||
// 3、每个项目都有两个临时触发器
|
||||
punchRange := pData.PunchRange
|
||||
split := strings.Split(punchRange, ",")
|
||||
for _, cfq := range split {
|
||||
dn, err := tool.TimeStr(cfq)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
}
|
||||
//创建临时定时器
|
||||
if dn > 0 {
|
||||
tempTimer := time.NewTimer(dn)
|
||||
// 在新的 goroutine 中等待临时定时器触发
|
||||
go func() {
|
||||
<-tempTimer.C
|
||||
//4、就获取当前项目下面的所有成员(条件为subscription为1的数据) 下发数据,并存储下发数据的状态
|
||||
var openidList []*wxModel.BusConstructionUserListRes
|
||||
dao.BusConstructionUser.Ctx(ctx).
|
||||
Fields("openid").
|
||||
Where("subscription", "1").
|
||||
Where("status = 0").
|
||||
Where("entry_date is not null and entry_date!='' and (leave_date is null or leave_date = '')").
|
||||
Where("project_id", pData.Id).
|
||||
Scan(&openidList)
|
||||
for _, oi := range openidList {
|
||||
Subscription(oi.Openid)
|
||||
}
|
||||
}()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
110
api/v1/common/coryCommon/weather.go
Normal file
110
api/v1/common/coryCommon/weather.go
Normal file
@ -0,0 +1,110 @@
|
||||
package coryCommon
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/gogf/gf/v2/net/gclient"
|
||||
"github.com/gogf/gf/v2/os/gctx"
|
||||
)
|
||||
|
||||
var key = "00b60ebda96849e694cb570e3d4f5c89"
|
||||
|
||||
// WeatherRep 返回参数 免费天气查询 https://dev.qweather.com/docs/api/weather/weather-daily-forecast/
|
||||
type WeatherRep struct {
|
||||
Code string `json:"code"`
|
||||
UpdateTime string `json:"updateTime"`
|
||||
FxLink string `json:"fxLink"`
|
||||
Daily []struct {
|
||||
FxDate string `json:"fxDate"`
|
||||
Sunrise string `json:"sunrise"`
|
||||
Sunset string `json:"sunset"`
|
||||
Moonrise string `json:"moonrise"`
|
||||
Moonset string `json:"moonset"`
|
||||
MoonPhase string `json:"moonPhase"`
|
||||
MoonPhaseIcon string `json:"moonPhaseIcon"`
|
||||
TempMax string `json:"tempMax"`
|
||||
TempMin string `json:"tempMin"`
|
||||
IconDay string `json:"iconDay"`
|
||||
TextDay string `json:"textDay"`
|
||||
IconNight string `json:"iconNight"`
|
||||
TextNight string `json:"textNight"`
|
||||
Wind360Day string `json:"wind360Day"`
|
||||
WindDirDay string `json:"windDirDay"`
|
||||
WindScaleDay string `json:"windScaleDay"`
|
||||
WindSpeedDay string `json:"windSpeedDay"`
|
||||
Wind360Night string `json:"wind360Night"`
|
||||
WindDirNight string `json:"windDirNight"`
|
||||
WindScaleNight string `json:"windScaleNight"`
|
||||
WindSpeedNight string `json:"windSpeedNight"`
|
||||
Humidity string `json:"humidity"`
|
||||
Precip string `json:"precip"`
|
||||
Pressure string `json:"pressure"`
|
||||
Vis string `json:"vis"`
|
||||
Cloud string `json:"cloud"`
|
||||
UvIndex string `json:"uvIndex"`
|
||||
} `json:"daily"`
|
||||
Refer struct {
|
||||
Sources []string `json:"sources"`
|
||||
License []string `json:"license"`
|
||||
} `json:"refer"`
|
||||
}
|
||||
|
||||
// Weather 传递经纬度 location := "116.41,39.92"
|
||||
func Weather(location string) (we string) {
|
||||
//请求路径
|
||||
//key := ""
|
||||
requestURL := "https://devapi.qweather.com/v7/weather/3d?location=" + location + "&key=" + key
|
||||
|
||||
response, err := gclient.New().ContentJson().ContentType("application/x-www-form-urlencoded").Get(gctx.New(), requestURL)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
var dataInfo = ""
|
||||
dataInfo = response.ReadAllString()
|
||||
return dataInfo
|
||||
}
|
||||
|
||||
type GridPointRes struct {
|
||||
Code string `json:"code"`
|
||||
UpdateTime string `json:"updateTime"`
|
||||
FxLink string `json:"fxLink"`
|
||||
Now struct {
|
||||
ObsTime string `json:"obsTime"`
|
||||
Temp string `json:"temp"`
|
||||
Icon string `json:"icon"`
|
||||
Text string `json:"text"`
|
||||
Wind360 string `json:"wind360"`
|
||||
WindDir string `json:"windDir"`
|
||||
WindScale string `json:"windScale"`
|
||||
WindSpeed string `json:"windSpeed"`
|
||||
Humidity string `json:"humidity"`
|
||||
Precip string `json:"precip"`
|
||||
Pressure string `json:"pressure"`
|
||||
Cloud string `json:"cloud"`
|
||||
Dew string `json:"dew"`
|
||||
} `json:"now"`
|
||||
Refer struct {
|
||||
Sources []string `json:"sources"`
|
||||
License []string `json:"license"`
|
||||
} `json:"refer"`
|
||||
}
|
||||
|
||||
// 格点天气 GridPoint
|
||||
func GridPoint(location string) (gp *GridPointRes, err error) {
|
||||
//请求路径
|
||||
//key := "00b60ebda96849e694cb570e3d4f5c89"
|
||||
requestURL := "https://devapi.qweather.com/v7/grid-weather/now?location=" + location + "&key=" + key
|
||||
|
||||
response, err := gclient.New().ContentJson().ContentType("application/x-www-form-urlencoded").Get(gctx.New(), requestURL)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
var dataInfo = ""
|
||||
dataInfo = response.ReadAllString()
|
||||
|
||||
gp = new(GridPointRes)
|
||||
err = json.Unmarshal([]byte(dataInfo), &gp)
|
||||
|
||||
fmt.Println(gp.Now.Icon)
|
||||
return
|
||||
}
|
621
api/v1/common/coryCommon/zip.go
Normal file
621
api/v1/common/coryCommon/zip.go
Normal file
@ -0,0 +1,621 @@
|
||||
package coryCommon
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"github.com/gogf/gf/v2/os/gtime"
|
||||
"golang.org/x/text/encoding/simplifiedchinese"
|
||||
"golang.org/x/text/transform"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
//解压文件、文件夹复制
|
||||
|
||||
// FileZipFunc 对文件进行解压
|
||||
func FileZipFunc(relativePath string, filenPath string, template string) (fileName string, err error) {
|
||||
// 打开压缩文件
|
||||
zipFile, err := zip.OpenReader(relativePath)
|
||||
if err != nil {
|
||||
fmt.Println("无法打开压缩文件(只支持ZIP):", err)
|
||||
return
|
||||
}
|
||||
var i = 0
|
||||
var name = ""
|
||||
//进入的压缩文件,判断压缩文件中的第一层是否包含文件,如果有就在当前随机产生一个文件夹,返回就返回到随机文件夹的位置
|
||||
randomFolder := ""
|
||||
// 遍历压缩文件中的文件头信息
|
||||
for _, f := range zipFile.File {
|
||||
//path, _ := gbkDecode(f.Name)
|
||||
fmt.Println("头? ", f.Name)
|
||||
path, _ := IsGB18030(f.Name)
|
||||
// 判断是否为顶层文件夹
|
||||
if !hasSlash(path) {
|
||||
randomFolder = FileName("/randomFolder")
|
||||
template = template + randomFolder
|
||||
filenPath = filenPath + randomFolder
|
||||
break
|
||||
}
|
||||
}
|
||||
// 遍历压缩文件中的文件
|
||||
for _, file := range zipFile.File {
|
||||
fmt.Println("ti ? ", file.Name)
|
||||
// 解决文件名编码问题
|
||||
if i == 0 {
|
||||
gb18030, _ := IsGB18030(file.Name)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
name = gb18030
|
||||
}
|
||||
file.Name, err = IsGB18030(file.Name)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
//对所有文件的名称进行空格替换
|
||||
file.Name = strings.Replace(file.Name, " ", "", -1)
|
||||
if err != nil {
|
||||
return file.Name, err
|
||||
}
|
||||
// 获取文件的相对路径,根据原本路径来操作还是根据新路径来(filenPath)
|
||||
extractedFilePath := ""
|
||||
extractedFilePathTwo := ""
|
||||
if filenPath != "" {
|
||||
extractedFilePath = filepath.Join(FileToFunc(filenPath, 2), "/"+file.Name)
|
||||
split := strings.Split(filenPath, "/")
|
||||
extractedFilePathTwo = extractedFilePath + "/" + split[len(split)-1]
|
||||
} else {
|
||||
extractedFilePath = filepath.ToSlash(filepath.Join(GetCWD()+template+"/"+".", file.Name))
|
||||
extractedFilePathTwo = extractedFilePath + "/"
|
||||
}
|
||||
//判断文件夹是否存在,存在就退出(i==0 是为了只判断最外层那一文件夹路径)
|
||||
_, err := os.Stat(filepath.Dir(extractedFilePathTwo))
|
||||
if err == nil && i == 0 {
|
||||
zipFile.Close()
|
||||
// 删除压缩文件
|
||||
err = os.Remove(relativePath)
|
||||
err = errors.New("当前文件夹已经存在,导入无效!")
|
||||
return "", err
|
||||
}
|
||||
i = i + 1
|
||||
// 检查是否为文件
|
||||
if !file.FileInfo().IsDir() {
|
||||
// 创建文件的目录结构
|
||||
err = os.MkdirAll(filepath.Dir(extractedFilePath), os.ModePerm)
|
||||
if err != nil {
|
||||
fmt.Println("无法创建目录:", err)
|
||||
return "", err
|
||||
}
|
||||
|
||||
// 打开压缩文件中的文件
|
||||
zippedFile, err := file.Open()
|
||||
if err != nil {
|
||||
fmt.Println("无法打开压缩文件中的文件:", err)
|
||||
return "", err
|
||||
}
|
||||
defer zippedFile.Close()
|
||||
|
||||
// 创建目标文件
|
||||
extractedFile, err := os.Create(extractedFilePath)
|
||||
|
||||
if err != nil {
|
||||
fmt.Println("无法创建目标文件:", err)
|
||||
return "", err
|
||||
}
|
||||
defer extractedFile.Close()
|
||||
|
||||
// 将压缩文件中的内容复制到目标文件
|
||||
_, err = io.Copy(extractedFile, zippedFile)
|
||||
if err != nil {
|
||||
fmt.Println("无法解压缩文件:", err)
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
zipFile.Close()
|
||||
|
||||
// 删除压缩文件
|
||||
err = os.Remove(relativePath)
|
||||
if err != nil {
|
||||
fmt.Println("无法删除压缩文件:", err)
|
||||
return
|
||||
}
|
||||
fileName = strings.Split(name, "/")[0]
|
||||
if randomFolder != "" {
|
||||
fileName = ""
|
||||
} else {
|
||||
fileName = "/" + fileName
|
||||
}
|
||||
if filenPath != "" {
|
||||
return FileToFunc(filenPath, 2) + fileName, err
|
||||
} else {
|
||||
return GetCWD() + template + fileName, err
|
||||
}
|
||||
}
|
||||
|
||||
// IsGB18030 判断字符串是否是 GB18030 编码
|
||||
func IsGB18030(name string) (string, error) {
|
||||
// 创建 GB18030 解码器
|
||||
decoder := simplifiedchinese.GB18030.NewDecoder()
|
||||
// 使用 transform 解码数据
|
||||
_, err := io.ReadAll(transform.NewReader(bytes.NewReader([]byte(name)), decoder))
|
||||
if err == nil {
|
||||
return name, nil
|
||||
} else {
|
||||
fileName, errName := simplifiedchinese.GB18030.NewDecoder().String(name)
|
||||
return fileName, errName
|
||||
}
|
||||
}
|
||||
|
||||
// gbkDecode 解决文件名乱码
|
||||
func gbkDecode(s string) (string, error) {
|
||||
gbkDecoder := simplifiedchinese.GBK.NewDecoder()
|
||||
decodedName, _, err := transform.String(gbkDecoder, s)
|
||||
return decodedName, err
|
||||
}
|
||||
|
||||
type DocumentListPublicRes struct {
|
||||
Id int64 `json:"id"`
|
||||
IdStr string `json:"idStr"`
|
||||
Pid string `json:"pid"`
|
||||
Name string `json:"name"`
|
||||
FilenPath string `json:"filenPath"`
|
||||
FilenPathCoding string `json:"filenPathCoding"`
|
||||
Suffix string `json:"suffix"`
|
||||
Type string `json:"type"`
|
||||
CreateBy string `json:"createBy"`
|
||||
CreatedAt *gtime.Time `json:"createdAt"`
|
||||
IsDuplicate bool `json:"isDuplicate"`
|
||||
ProjectId int64 `json:"projectId"`
|
||||
}
|
||||
|
||||
// Traversal 遍历文件夹 ctx,绝对路径,上级文件夹(可有可无),表名,存储位置,项目id,模板1or资料2
|
||||
//
|
||||
// one, err := coryCommon.Traversal(ctx, path, req.Pid, dao.DocumentCompletion.Table(), dataFolder, req.ProjectId, "2") //遍历解压后的文件,插入数据
|
||||
// if err != nil {
|
||||
// liberr.ErrIsNil(ctx, err)
|
||||
// return
|
||||
// }
|
||||
// _, err = g.DB().Model(dao.DocumentCompletion.Table()).Ctx(ctx).Insert(one)
|
||||
// liberr.ErrIsNil(ctx, err, "新增失败!")
|
||||
func Traversal(ctx context.Context, root string, pidstr string, tableName string, templatePath string, projectId int64, num string) (dataOne []*DocumentListPublicRes, err error) {
|
||||
|
||||
template := strings.Replace(templatePath, "/resource/public", "/file", 1)
|
||||
err = filepath.Walk(root, func(path string, info os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// 获取相对路径
|
||||
relativePath, err := filepath.Rel(root, path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// 根目录下创建,还是指定文件夹下面创建?
|
||||
p := ""
|
||||
if pidstr != "" {
|
||||
value, _ := g.DB().Model(tableName).Ctx(ctx).Where("id_str", pidstr).Fields("filen_path").Value()
|
||||
split := strings.Split(root, "/")
|
||||
p = value.String() + "/" + split[len(split)-1] + "/" + relativePath
|
||||
} else {
|
||||
p = template + "/" + relativePath
|
||||
}
|
||||
p = strings.ReplaceAll(p, "\\", "/")
|
||||
// 获取当前项的深度
|
||||
depth := strings.Count(relativePath, string(filepath.Separator))
|
||||
// 判断父子关系并打印结果
|
||||
if depth == 0 && info.IsDir() {
|
||||
if relativePath == "." {
|
||||
split := strings.Split(root, "/")
|
||||
n := split[len(split)-1]
|
||||
// 根目录下创建,还是指定文件夹下面创建?
|
||||
p := ""
|
||||
if pidstr != "" {
|
||||
value, _ := g.DB().Model(tableName).Ctx(ctx).Where("id_str", pidstr).Fields("filen_path").Value()
|
||||
p = value.String() + "/" + n
|
||||
} else {
|
||||
p = template + "/" + n
|
||||
}
|
||||
p = strings.ReplaceAll(p, "\\", "/")
|
||||
template = template + "/" + n
|
||||
var dataTwo = new(DocumentListPublicRes)
|
||||
dataTwo.IdStr = SHA256(p)
|
||||
if pidstr != "" {
|
||||
dataTwo.Pid = pidstr
|
||||
} else {
|
||||
dataTwo.Pid = "0"
|
||||
}
|
||||
dataTwo.Name = n
|
||||
dataTwo.FilenPath = p
|
||||
////如果文件夹路径重复,就提示 解压文件夹的时候就已经判断了,这里就不需要了
|
||||
//err := IsFolderExist(ctx, p)
|
||||
//if err != nil {
|
||||
// return err
|
||||
//}
|
||||
dataTwo.Type = "2"
|
||||
dataOne = append(dataOne, dataTwo)
|
||||
} else {
|
||||
dir, n := filepath.Split(p)
|
||||
dir = strings.TrimSuffix(dir, "/")
|
||||
var dataTwo = new(DocumentListPublicRes)
|
||||
dataTwo.IdStr = SHA256(p)
|
||||
dataTwo.Pid = SHA256(dir)
|
||||
dataTwo.Name = n
|
||||
dataTwo.FilenPath = p
|
||||
////如果文件夹路径重复,就提示
|
||||
//err := IsFolderExist(ctx, p)
|
||||
//if err != nil {
|
||||
// return err
|
||||
//}
|
||||
dataTwo.Type = "2"
|
||||
dataOne = append(dataOne, dataTwo)
|
||||
}
|
||||
} else if info.IsDir() {
|
||||
// 子文件夹
|
||||
dir, n := filepath.Split(p)
|
||||
dir = strings.TrimSuffix(dir, "/")
|
||||
var dataTwo = new(DocumentListPublicRes)
|
||||
dataTwo.IdStr = SHA256(p)
|
||||
dataTwo.Pid = SHA256(dir)
|
||||
dataTwo.Name = n
|
||||
dataTwo.FilenPath = p
|
||||
dataTwo.Type = "2"
|
||||
dataOne = append(dataOne, dataTwo)
|
||||
} else {
|
||||
dir, n := filepath.Split(p)
|
||||
dir = strings.TrimSuffix(dir, "/")
|
||||
var dataTwo = new(DocumentListPublicRes)
|
||||
dataTwo.Pid = SHA256(dir)
|
||||
lastDotIndex := strings.LastIndex(n, ".")
|
||||
if lastDotIndex == -1 || lastDotIndex == 0 {
|
||||
dataTwo.Name = strings.Split(n, ".")[0]
|
||||
} else {
|
||||
dataTwo.Name = n[:lastDotIndex]
|
||||
}
|
||||
dataTwo.Suffix = n[lastDotIndex:]
|
||||
dataTwo.FilenPath = p
|
||||
dataTwo.Type = "1"
|
||||
//文件只能是这三种类型,其他类型进不来
|
||||
s := n[lastDotIndex:]
|
||||
if num == "1" { //资料有格式限制
|
||||
if strings.EqualFold(s, ".xls") || strings.EqualFold(s, ".xlsx") || strings.EqualFold(s, ".docx") || strings.EqualFold(s, ".doc") || strings.EqualFold(s, ".pptx") || strings.EqualFold(s, ".ppt") {
|
||||
dataOne = append(dataOne, dataTwo)
|
||||
}
|
||||
} else {
|
||||
dataOne = append(dataOne, dataTwo)
|
||||
}
|
||||
}
|
||||
return err
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
//fmt.Println("遍历文件夹时发生错误:", err)
|
||||
return nil, err
|
||||
}
|
||||
// 有项目id表示资料 无表模板
|
||||
if projectId > 0 {
|
||||
for i := range dataOne {
|
||||
dataOne[i].ProjectId = projectId
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func SHA256(str string) (hx string) {
|
||||
// 创建 SHA-256 哈希对象
|
||||
hash := sha256.New()
|
||||
// 将字符串转换为字节数组并进行哈希计算
|
||||
hash.Write([]byte(str))
|
||||
// 计算 SHA-256 哈希值
|
||||
hashedBytes := hash.Sum(nil)
|
||||
// 将哈希值转换为十六进制字符串
|
||||
hashStr := hex.EncodeToString(hashedBytes)
|
||||
return hashStr
|
||||
}
|
||||
|
||||
// CopyFile 文件复制
|
||||
func CopyFile(src, dst string) error {
|
||||
// 打开源文件
|
||||
inputFile, err := os.Open(src)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer inputFile.Close()
|
||||
|
||||
// 创建目标文件
|
||||
outputFile, err := os.Create(dst)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer outputFile.Close()
|
||||
|
||||
// 通过 Copy 函数实现拷贝功能
|
||||
_, err = io.Copy(outputFile, inputFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 确保文件内容被刷新到磁盘上
|
||||
err = outputFile.Sync()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// CopyDirectory 文件夹复制
|
||||
func CopyDirectory(src string, dest string) error {
|
||||
// 检查源文件夹是否存在
|
||||
_, err := os.Stat(src)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// 检查目标文件夹是否存在,不存在则创建
|
||||
err = os.MkdirAll(dest, 0755)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 遍历源文件夹
|
||||
files, err := os.ReadDir(src)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, file := range files {
|
||||
srcPath := src + "/" + file.Name()
|
||||
destPath := dest + "/" + file.Name()
|
||||
|
||||
// 判断文件类型
|
||||
if file.IsDir() {
|
||||
// 如果是文件夹,则递归调用 copyDirectory 函数复制文件夹及其子文件
|
||||
err = CopyDirectory(srcPath, destPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
// 如果是文件,则复制文件到目标文件夹
|
||||
inputFile, err := os.Open(srcPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer inputFile.Close()
|
||||
|
||||
outputFile, err := os.Create(destPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer outputFile.Close()
|
||||
|
||||
_, err = io.Copy(outputFile, inputFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// MoveFile 文件移动
|
||||
func MoveFile(source, destination string) (err error) {
|
||||
// 执行移动操作
|
||||
err = os.Rename(source, destination)
|
||||
if err != nil {
|
||||
fmt.Println("Error:", err)
|
||||
} else {
|
||||
fmt.Println("File moved successfully!")
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// MoveFolder 文件夹下面的文件及子文件全部移动到新文件夹下
|
||||
func MoveFolder(srcPath, destPath string) error {
|
||||
// 获取源文件夹下的所有文件和子文件夹
|
||||
fileList := []string{}
|
||||
err := filepath.Walk(srcPath, func(path string, info os.FileInfo, err error) error {
|
||||
fileList = append(fileList, path)
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 移动每个文件和子文件夹
|
||||
for _, file := range fileList {
|
||||
// 获取相对路径
|
||||
relPath, err := filepath.Rel(srcPath, file)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 构建目标路径
|
||||
destFile := filepath.Join(destPath, relPath)
|
||||
// 判断是文件还是文件夹
|
||||
if fileInfo, err := os.Stat(file); err == nil && fileInfo.IsDir() {
|
||||
// 如果是文件夹,创建目标文件夹
|
||||
err := os.MkdirAll(destFile, os.ModePerm)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
// 如果是文件,复制文件
|
||||
err := copyFile(file, destFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 移动完成后删除源文件夹
|
||||
return os.RemoveAll(srcPath)
|
||||
}
|
||||
|
||||
func copyFile(src, dest string) error {
|
||||
srcFile, err := os.Open(src)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer srcFile.Close()
|
||||
|
||||
destFile, err := os.Create(dest)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer destFile.Close()
|
||||
|
||||
_, err = io.Copy(destFile, srcFile)
|
||||
return err
|
||||
}
|
||||
|
||||
// FolderToZip 将给定文件夹压缩成压缩包存储到另外一个路径(参数:源数据、目标路径)
|
||||
func FolderToZip(folderToZip, zipFile string) (err error) {
|
||||
// 创建一个新的压缩包文件
|
||||
newZipFile, err := os.Create(zipFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer newZipFile.Close()
|
||||
// 创建一个 zip.Writer 来向压缩包中写入内容
|
||||
zipWriter := zip.NewWriter(newZipFile)
|
||||
defer zipWriter.Close()
|
||||
|
||||
// 递归遍历文件夹并将其中的文件和目录添加到压缩包中
|
||||
err = filepath.Walk(folderToZip, func(filePath string, info os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// 获取当前文件的相对路径
|
||||
relativePath, err := filepath.Rel(folderToZip, filePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// 如果是目录,则创建一个目录项
|
||||
if info.IsDir() {
|
||||
_, err = zipWriter.Create(relativePath + "/")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
// 如果是文件,则创建一个文件项并写入文件内容
|
||||
fileData, err := ioutil.ReadFile(filePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
file, err := zipWriter.Create(relativePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = file.Write(fileData)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// zipDirFiles 压缩文件
|
||||
func ZipDirFiles(src_dir string, zip_file_name string) error {
|
||||
// 检查并创建目标目录
|
||||
err := os.MkdirAll(filepath.Dir(zip_file_name), os.ModePerm)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 删除空的zip,而不是直接使用os.RemoveAll,以提高安全性
|
||||
err = os.Remove(zip_file_name)
|
||||
if err != nil && !os.IsNotExist(err) {
|
||||
return err
|
||||
}
|
||||
|
||||
// 创建新的zip文件
|
||||
zipfile, err := os.Create(zip_file_name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer zipfile.Close()
|
||||
|
||||
// 初始化zip写入器
|
||||
archive := zip.NewWriter(zipfile)
|
||||
defer archive.Close()
|
||||
|
||||
// 遍历源目录下的文件和子目录
|
||||
return filepath.Walk(src_dir, func(path string, info os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// 计算相对于源目录的路径
|
||||
relPath, _ := filepath.Rel(src_dir, path)
|
||||
if path == src_dir {
|
||||
// 如果是源目录本身,跳过
|
||||
return nil
|
||||
}
|
||||
|
||||
// 创建zip文件头
|
||||
header, err := zip.FileInfoHeader(info)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
header.Name = filepath.ToSlash(relPath)
|
||||
|
||||
// 标记目录
|
||||
if info.IsDir() {
|
||||
header.Name += "/"
|
||||
return nil
|
||||
}
|
||||
|
||||
// 处理文件
|
||||
file, err := os.Open(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
writer, err := archive.CreateHeader(header)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = io.Copy(writer, file)
|
||||
return err
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
// 压缩后删除源文件
|
||||
func DeleteFolderToZip(srcpath, destpathzip string) (err error) {
|
||||
//srcpath := filepath.ToSlash(`D:\GiteeProject\gsproject\zmkg-back\resource\public\temporary\del`)
|
||||
//destpathzip := filepath.ToSlash(`D:\GiteeProject\gsproject\zmkg-back\resource\public\temporary\yy.zip`)
|
||||
err = ZipDirFiles(srcpath, destpathzip) // 文件压缩 压缩文件目录 和压缩文件zip
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// 删除原文件
|
||||
if err := os.RemoveAll(srcpath); err != nil {
|
||||
if !os.IsNotExist(err) {
|
||||
fmt.Printf("Error removing existing file %s: %v\n", srcpath, err)
|
||||
return err
|
||||
}
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// 判断字符串中是否包含斜杠
|
||||
func hasSlash(s string) bool {
|
||||
for _, c := range s {
|
||||
if c == '/' || c == '\\' {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
278
api/v1/common/fileUpload/upload.go
Normal file
278
api/v1/common/fileUpload/upload.go
Normal file
@ -0,0 +1,278 @@
|
||||
package fileUpload
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"github.com/gogf/gf/v2/net/ghttp"
|
||||
"github.com/tiger1103/gfast/v3/api/v1/common/globe"
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
"path"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func InitUploadApi(group *ghttp.RouterGroup) {
|
||||
group.POST("/source/upload", SourceUploadFunc)
|
||||
//group.Bind(new(SourceUpload))
|
||||
}
|
||||
|
||||
type SourceUpload struct {
|
||||
}
|
||||
type SourceUploadReq struct {
|
||||
g.Meta `path:"source/upload" dc:"上传资源" method:"post" tags:"资源相关" `
|
||||
}
|
||||
type SourceUploadRes struct {
|
||||
}
|
||||
|
||||
/*func (SourceUpload) UploadFile(ctx context.Context, req *SourceUploadReq) (res *SourceUploadRes, err error) {
|
||||
err = startSaveFile(g.RequestFromCtx(ctx))
|
||||
return
|
||||
}*/
|
||||
|
||||
func SourceUploadFunc(request *ghttp.Request) {
|
||||
projectId := request.Get("projectId")
|
||||
fmt.Println("projectId", projectId)
|
||||
startSaveFile(request)
|
||||
}
|
||||
|
||||
func startSaveFile(request *ghttp.Request) error {
|
||||
err, filename := Upload(request, globe.SOURCE)
|
||||
fmt.Println("结束了")
|
||||
fmt.Println(err)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Println(filename)
|
||||
/* arr := strings.Split(filename, ".")
|
||||
arr = arr[:len(arr)-1]
|
||||
suffix := path.Ext(filename)
|
||||
var SourceType = ""
|
||||
switch suffix {
|
||||
case globe.CLT:
|
||||
SourceType = globe.TILESET
|
||||
break
|
||||
case globe.JCT:
|
||||
SourceType = globe.TILESET
|
||||
break
|
||||
case globe.MBTILES:
|
||||
SourceType = globe.LAYER
|
||||
break
|
||||
case globe.PAK:
|
||||
//此时需要判断是地形还是正射
|
||||
SourceType = globe.LAYER
|
||||
break
|
||||
}*/
|
||||
//source := database.SOURCE{
|
||||
// SourceID: tool.GetUuid(),
|
||||
// SourceName: strings.Join(arr, "."),
|
||||
// SourceType: SourceType,
|
||||
// SourcePath: filename,
|
||||
//}
|
||||
//database.GetORMDBInstance().Model(&database.SOURCE{}).Create(&source)
|
||||
return err
|
||||
}
|
||||
|
||||
func Upload(r *ghttp.Request, dir string) (error, string) {
|
||||
var contentLength int64
|
||||
contentLength = r.Request.ContentLength
|
||||
if contentLength <= 0 {
|
||||
return globe.GetErrors("content_length error"), ""
|
||||
}
|
||||
content_type_, has_key := r.Request.Header["Content-Type"]
|
||||
if !has_key {
|
||||
return globe.GetErrors("Content-Type error"), ""
|
||||
}
|
||||
if len(content_type_) != 1 {
|
||||
return globe.GetErrors("Content-Type count error"), ""
|
||||
}
|
||||
contentType := content_type_[0]
|
||||
const BOUNDARY string = "; boundary="
|
||||
loc := strings.Index(contentType, BOUNDARY)
|
||||
if -1 == loc {
|
||||
return globe.GetErrors("Content-Type error, no boundary"), ""
|
||||
}
|
||||
boundary := []byte(contentType[(loc + len(BOUNDARY)):])
|
||||
readData := make([]byte, 1024*12)
|
||||
var readTotal = 0
|
||||
var des = ""
|
||||
var filename = ""
|
||||
for {
|
||||
fileHeader, fileData, err := ParseFromHead(readData, readTotal, append(boundary, []byte("\r\n")...), r.Request.Body)
|
||||
if err != nil {
|
||||
return err, ""
|
||||
}
|
||||
filename = fileHeader.FileName
|
||||
des = path.Join(dir, filename)
|
||||
f, err := os.Create(des)
|
||||
if err != nil {
|
||||
return err, ""
|
||||
}
|
||||
f.Write(fileData)
|
||||
fileData = nil
|
||||
//需要反复搜索boundary
|
||||
tempData, reachEnd, err := ReadToBoundary(boundary, r.Request.Body, f)
|
||||
f.Close()
|
||||
if err != nil {
|
||||
return err, ""
|
||||
}
|
||||
if reachEnd {
|
||||
break
|
||||
} else {
|
||||
copy(readData[0:], tempData)
|
||||
readTotal = len(tempData)
|
||||
continue
|
||||
}
|
||||
}
|
||||
return nil, filename
|
||||
}
|
||||
|
||||
// / 解析多个文件上传中,每个具体的文件的信息
|
||||
type FileHeader struct {
|
||||
ContentDisposition string
|
||||
Name string
|
||||
FileName string ///< 文件名
|
||||
ContentType string
|
||||
ContentLength int64
|
||||
}
|
||||
|
||||
// / 解析描述文件信息的头部
|
||||
// / @return FileHeader 文件名等信息的结构体
|
||||
// / @return bool 解析成功还是失败
|
||||
func ParseFileHeader(h []byte) (FileHeader, bool) {
|
||||
arr := bytes.Split(h, []byte("\r\n"))
|
||||
var out_header FileHeader
|
||||
out_header.ContentLength = -1
|
||||
const (
|
||||
CONTENT_DISPOSITION = "Content-Disposition: "
|
||||
NAME = "name=\""
|
||||
FILENAME = "filename=\""
|
||||
CONTENT_TYPE = "Content-Type: "
|
||||
CONTENT_LENGTH = "Content-Length: "
|
||||
)
|
||||
for _, item := range arr {
|
||||
if bytes.HasPrefix(item, []byte(CONTENT_DISPOSITION)) {
|
||||
l := len(CONTENT_DISPOSITION)
|
||||
arr1 := bytes.Split(item[l:], []byte("; "))
|
||||
out_header.ContentDisposition = string(arr1[0])
|
||||
if bytes.HasPrefix(arr1[1], []byte(NAME)) {
|
||||
out_header.Name = string(arr1[1][len(NAME) : len(arr1[1])-1])
|
||||
}
|
||||
fmt.Println(arr1)
|
||||
l = len(arr1[2])
|
||||
if bytes.HasPrefix(arr1[2], []byte(FILENAME)) && arr1[2][l-1] == 0x22 {
|
||||
out_header.FileName = string(arr1[2][len(FILENAME) : l-1])
|
||||
}
|
||||
} else if bytes.HasPrefix(item, []byte(CONTENT_TYPE)) {
|
||||
l := len(CONTENT_TYPE)
|
||||
out_header.ContentType = string(item[l:])
|
||||
} else if bytes.HasPrefix(item, []byte(CONTENT_LENGTH)) {
|
||||
l := len(CONTENT_LENGTH)
|
||||
s := string(item[l:])
|
||||
content_length, err := strconv.ParseInt(s, 10, 64)
|
||||
if err != nil {
|
||||
log.Printf("content length error:%s", string(item))
|
||||
return out_header, false
|
||||
} else {
|
||||
out_header.ContentLength = content_length
|
||||
}
|
||||
} else {
|
||||
log.Printf("unknown:%s\n", string(item))
|
||||
}
|
||||
}
|
||||
if len(out_header.FileName) == 0 {
|
||||
return out_header, false
|
||||
}
|
||||
return out_header, true
|
||||
}
|
||||
|
||||
// / 从流中一直读到文件的末位
|
||||
// / @return []byte 没有写到文件且又属于下一个文件的数据
|
||||
// / @return bool 是否已经读到流的末位了
|
||||
// / @return error 是否发生错误
|
||||
func ReadToBoundary(boundary []byte, stream io.ReadCloser, target io.WriteCloser) ([]byte, bool, error) {
|
||||
read_data := make([]byte, 1024*8)
|
||||
read_data_len := 0
|
||||
buf := make([]byte, 1024*4)
|
||||
b_len := len(boundary)
|
||||
reach_end := false
|
||||
for !reach_end {
|
||||
read_len, err := stream.Read(buf)
|
||||
if err != nil {
|
||||
if err != io.EOF && read_len <= 0 {
|
||||
return nil, true, err
|
||||
}
|
||||
reach_end = true
|
||||
}
|
||||
//todo: 下面这一句很蠢,值得优化
|
||||
copy(read_data[read_data_len:], buf[:read_len]) //追加到另一块buffer,仅仅只是为了搜索方便
|
||||
read_data_len += read_len
|
||||
if read_data_len < b_len+4 {
|
||||
continue
|
||||
}
|
||||
loc := bytes.Index(read_data[:read_data_len], boundary)
|
||||
if loc >= 0 {
|
||||
//找到了结束位置
|
||||
target.Write(read_data[:loc-4])
|
||||
return read_data[loc:read_data_len], reach_end, nil
|
||||
}
|
||||
|
||||
target.Write(read_data[:read_data_len-b_len-4])
|
||||
copy(read_data[0:], read_data[read_data_len-b_len-4:])
|
||||
read_data_len = b_len + 4
|
||||
}
|
||||
target.Write(read_data[:read_data_len])
|
||||
return nil, reach_end, nil
|
||||
}
|
||||
|
||||
// / 解析表单的头部
|
||||
// / @param read_data 已经从流中读到的数据
|
||||
// / @param read_total 已经从流中读到的数据长度
|
||||
// / @param boundary 表单的分割字符串
|
||||
// / @param stream 输入流
|
||||
// / @return FileHeader 文件名等信息头
|
||||
// / []byte 已经从流中读到的部分
|
||||
// / error 是否发生错误
|
||||
func ParseFromHead(read_data []byte, readTotal int, boundary []byte, stream io.ReadCloser) (FileHeader, []byte, error) {
|
||||
buf := make([]byte, 1024*4)
|
||||
foundBoundary := false
|
||||
boundaryLoc := -1
|
||||
var file_header FileHeader
|
||||
for {
|
||||
read_len, err := stream.Read(buf)
|
||||
fmt.Println("read_len", read_len)
|
||||
if err != nil {
|
||||
if err != io.EOF {
|
||||
return file_header, nil, err
|
||||
}
|
||||
break
|
||||
}
|
||||
if readTotal+read_len > cap(read_data) {
|
||||
return file_header, nil, fmt.Errorf("not found boundary")
|
||||
}
|
||||
copy(read_data[readTotal:], buf[:read_len])
|
||||
readTotal += read_len
|
||||
if !foundBoundary {
|
||||
boundaryLoc = bytes.Index(read_data[:readTotal], boundary)
|
||||
if -1 == boundaryLoc {
|
||||
continue
|
||||
}
|
||||
foundBoundary = true
|
||||
}
|
||||
start_loc := boundaryLoc + len(boundary)
|
||||
file_head_loc := bytes.Index(read_data[start_loc:readTotal], []byte("\r\n\r\n"))
|
||||
if -1 == file_head_loc {
|
||||
continue
|
||||
}
|
||||
file_head_loc += start_loc
|
||||
ret := false
|
||||
file_header, ret = ParseFileHeader(read_data[start_loc:file_head_loc])
|
||||
if !ret {
|
||||
return file_header, nil, fmt.Errorf("ParseFileHeader fail:%s", string(read_data[start_loc:file_head_loc]))
|
||||
}
|
||||
return file_header, read_data[file_head_loc+4 : readTotal], nil
|
||||
}
|
||||
return file_header, nil, fmt.Errorf("reach to stream EOF")
|
||||
}
|
96
api/v1/common/globe/globe.go
Normal file
96
api/v1/common/globe/globe.go
Normal file
@ -0,0 +1,96 @@
|
||||
package globe
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"github.com/gogf/gf/v2/net/ghttp"
|
||||
"gorm.io/gorm"
|
||||
"net/http"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
const (
|
||||
ALL = -1 //所有
|
||||
ENABLE = 1
|
||||
DISABLE = 0
|
||||
DESC = "desc"
|
||||
ASC = "asc"
|
||||
PAGE = 1
|
||||
PAGESIZE = 10
|
||||
ONLINE = 1
|
||||
OFFLINE = 0
|
||||
PREFFIX = "yjearth4.0"
|
||||
)
|
||||
|
||||
var IS_OFFLINE_VERSION = true //是否为单机版本
|
||||
const SOURCE = "resource/public/clt/"
|
||||
|
||||
const (
|
||||
TILESET = "tileset"
|
||||
BIM = "bim"
|
||||
LAYER = "layer"
|
||||
TERRAIN = "terrain"
|
||||
POINT = "point"
|
||||
LINE = "line"
|
||||
AREA = "area"
|
||||
MODEL = "model"
|
||||
KML = "kml"
|
||||
GEOJSON = "geojson"
|
||||
DIRECTORY = "directory"
|
||||
SHP = "shp"
|
||||
)
|
||||
|
||||
const (
|
||||
PAK = ".pak"
|
||||
MBTILES = ".mbtiles"
|
||||
CLT = ".clt"
|
||||
JCT = ".jct"
|
||||
DOTGEOJSON = ".geojson"
|
||||
DOTSHP = ".shp"
|
||||
)
|
||||
|
||||
var (
|
||||
PORT = "80"
|
||||
HOST = ""
|
||||
PROTOCOL = ""
|
||||
KEY = ""
|
||||
CRT = ""
|
||||
)
|
||||
|
||||
const (
|
||||
HTTP = "http"
|
||||
HTTPS = "https"
|
||||
)
|
||||
|
||||
func GetErrors(msg string) error {
|
||||
return errors.New(msg)
|
||||
}
|
||||
|
||||
func GetAddr() string {
|
||||
//单机版本时 无代理,需要补全地址
|
||||
//if IS_OFFLINE_VERSION {
|
||||
// return PROTOCOL + "://" + HOST + ":" + PORT + "/" + PREFFIX
|
||||
//}
|
||||
//网络版时 有代理 不需要补全地址
|
||||
return PREFFIX
|
||||
}
|
||||
|
||||
/*clt数据包*/
|
||||
type Tile struct {
|
||||
MD5 string `json:"md5"`
|
||||
PATH string `json:"path"`
|
||||
Tile []byte `json:"tile"`
|
||||
Type string `json:"type"`
|
||||
}
|
||||
|
||||
func RenderData(request *ghttp.Request, data []byte) {
|
||||
request.Response.Header().Set("Cache-Control", "private,max-age="+strconv.Itoa(60*60))
|
||||
request.Response.WriteHeader(http.StatusOK)
|
||||
request.Response.Writer.Write(data)
|
||||
}
|
||||
func CloseDB(db *gorm.DB) {
|
||||
s, err := db.DB()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
s.Close()
|
||||
}
|
25
api/v1/common/req.go
Normal file
25
api/v1/common/req.go
Normal file
@ -0,0 +1,25 @@
|
||||
/*
|
||||
* @desc:公共接口相关
|
||||
* @company:云南奇讯科技有限公司
|
||||
* @Author: yixiaohu<yxh669@qq.com>
|
||||
* @Date: 2022/3/30 9:28
|
||||
*/
|
||||
|
||||
package common
|
||||
|
||||
// PageReq 公共请求参数
|
||||
type PageReq struct {
|
||||
DateRange []string `p:"dateRange"` //日期范围
|
||||
PageNum int `p:"pageNum"` //当前页码
|
||||
PageSize int `p:"pageSize"` //每页数
|
||||
OrderBy string //排序方式
|
||||
NotInPlan bool `p:"notInPlan"` //是否过滤周计划中的id
|
||||
}
|
||||
|
||||
type Author struct {
|
||||
Authorization string `p:"Authorization" in:"header" dc:"Bearer {{token}}"`
|
||||
}
|
||||
|
||||
type Paging struct {
|
||||
IsPaging string `json:"isPaging" dc:"是否开启分页功能 YES开启 NO不开启(空字符串也不开启分页;默认)"` //是否开启分页功能 YES开启 NO不开启(空字符串也不开启分页;默认)
|
||||
}
|
21
api/v1/common/res.go
Normal file
21
api/v1/common/res.go
Normal file
@ -0,0 +1,21 @@
|
||||
/*
|
||||
* @desc:返回响应公共参数
|
||||
* @company:云南奇讯科技有限公司
|
||||
* @Author: yixiaohu<yxh669@qq.com>
|
||||
* @Date: 2022/10/27 16:30
|
||||
*/
|
||||
|
||||
package common
|
||||
|
||||
import "github.com/gogf/gf/v2/frame/g"
|
||||
|
||||
// EmptyRes 不响应任何数据
|
||||
type EmptyRes struct {
|
||||
g.Meta `mime:"application/json"`
|
||||
}
|
||||
|
||||
// ListRes 列表公共返回
|
||||
type ListRes struct {
|
||||
CurrentPage int `json:"currentPage"`
|
||||
Total interface{} `json:"total"`
|
||||
}
|
254
api/v1/common/shp/shp.go
Normal file
254
api/v1/common/shp/shp.go
Normal file
@ -0,0 +1,254 @@
|
||||
package shp
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/tomchavakis/turf-go"
|
||||
|
||||
"github.com/tiger1103/gfast/v3/api/v1/common/globe"
|
||||
"github.com/tiger1103/gfast/v3/api/v1/common/tool"
|
||||
"github.com/tiger1103/gfast/v3/api/v1/common/tool/shp"
|
||||
"github.com/tomchavakis/geojson/geometry"
|
||||
)
|
||||
|
||||
const (
|
||||
DefaultColor = "#12f6f6"
|
||||
DefaultWidth = "2"
|
||||
)
|
||||
|
||||
type Point struct {
|
||||
Lng float64 `json:"lng"`
|
||||
Lat float64 `json:"lat"`
|
||||
Alt float64 `json:"alt"` // 裝點更新 只更新這一個,更新立柱的高程時 這個字段不動
|
||||
Width float64 `json:"width"`
|
||||
Property
|
||||
}
|
||||
|
||||
type Polyline struct {
|
||||
Positions []Point `json:"positions"`
|
||||
Width string `json:"width"`
|
||||
Color string `json:"color"`
|
||||
Alpha string `json:"alpha"`
|
||||
Degree string `json:"degree"`
|
||||
// Name string `json:"name"` // text
|
||||
// Property string `json:"property"`
|
||||
Range Box `json:"range"`
|
||||
Property
|
||||
}
|
||||
|
||||
type Polygon struct {
|
||||
Positions []Point `json:"positions"`
|
||||
Color string `json:"color"`
|
||||
Range Box `json:"range"`
|
||||
}
|
||||
|
||||
type Box struct {
|
||||
MinX float64 `json:"min_x"`
|
||||
MinY float64 `json:"min_y"`
|
||||
MaxX float64 `json:"max_x"`
|
||||
MaxY float64 `json:"max_y"`
|
||||
}
|
||||
type ShpObj struct {
|
||||
Points []Point `json:"points"`
|
||||
Polylines []Polyline `json:"polylines"`
|
||||
Polygons []Polygon `json:"polygons"`
|
||||
}
|
||||
|
||||
type Detail struct {
|
||||
// Rotation []interfac e{} `json:"rotation"`
|
||||
Position Point `json:"position"`
|
||||
}
|
||||
|
||||
type Degree struct {
|
||||
Position PointDegree `json:"position"`
|
||||
}
|
||||
|
||||
type PointDegree struct {
|
||||
Lng float64 `json:"lng"`
|
||||
Lat float64 `json:"lat"`
|
||||
Alt float64 `json:"alt"` // 裝點更新 只更新這一個,更新立柱的高程時 這個字段不動
|
||||
Degree string `json:"degree"`
|
||||
}
|
||||
|
||||
type Property struct {
|
||||
Name string `json:"name"`
|
||||
Beizhu string `json:"beizhu"`
|
||||
Tishi string `json:"tishi"`
|
||||
Height float64 `json:"height"` // 更新立柱的時 更新這個字段
|
||||
Difference float64 `json:"difference"` // height - alt
|
||||
SourceId string `json:"sourceId"`
|
||||
}
|
||||
|
||||
/*读取shp数据*/
|
||||
func ReadShp(file string) (error, *ShpObj) {
|
||||
//if !globe.IS_OFFLINE_VERSION {
|
||||
// file = globe.SOURCE + file
|
||||
//}
|
||||
if !tool.PathExists(file) {
|
||||
return globe.GetErrors("资源不存在," + file), nil
|
||||
}
|
||||
shape, err := shp.Open(file)
|
||||
if err != nil {
|
||||
return err, nil
|
||||
}
|
||||
defer shape.Close()
|
||||
|
||||
obj := ShpObj{
|
||||
Polygons: []Polygon{},
|
||||
Polylines: []Polyline{},
|
||||
Points: []Point{},
|
||||
}
|
||||
|
||||
fields := shape.Fields()
|
||||
for shape.Next() {
|
||||
n, p := shape.Shape()
|
||||
|
||||
name := ""
|
||||
beizhu := ""
|
||||
tishi := ""
|
||||
|
||||
var O_LClr, O_LWidth, O_LAlpha /*, O_LType, O_SType, O_TType, O_Name, O_Comment*/ string
|
||||
O_LClr = DefaultColor
|
||||
O_LWidth = DefaultWidth
|
||||
// Text := ""
|
||||
for k, f := range fields {
|
||||
val := shape.ReadAttribute(n, k)
|
||||
|
||||
bb := f.String()
|
||||
|
||||
// // 记录本次判断开始前的名字
|
||||
// temp := name
|
||||
|
||||
switch bb {
|
||||
// case "名称": // 方阵的名称
|
||||
// if len(name) == 0 {
|
||||
// name = val
|
||||
// }
|
||||
// case "TxtMemo": // 方阵的名称
|
||||
// if len(name) == 0 {
|
||||
// name = val
|
||||
// }
|
||||
case "name": // 方阵的名称
|
||||
if len(name) == 0 {
|
||||
name = val
|
||||
}
|
||||
|
||||
// case "O_Name": // 方阵的名称
|
||||
// if len(name) == 0 {
|
||||
// name = val
|
||||
// }
|
||||
case "Text": // 方阵的名称
|
||||
if len(name) == 0 {
|
||||
name = val
|
||||
}
|
||||
case "备注": // 方阵的名称
|
||||
beizhu = val
|
||||
case "提示": // 方阵的名称
|
||||
tishi = val
|
||||
}
|
||||
|
||||
// 如果本次循环后名字被清空,则替换为原本的名字
|
||||
// if name == "" {
|
||||
// name = temp
|
||||
// }
|
||||
|
||||
// fmt.Printf("\t%v: %v\n", f, val)
|
||||
}
|
||||
|
||||
// fmt.Println(O_Name, O_Comment, O_LClr, O_LWidth, O_LAlpha, O_LType, O_SType, O_TType, Shape_Leng, Text, TxtMemo)
|
||||
// fmt.Println("Text", Text)
|
||||
// fmt.Println("O_Name", O_Name)
|
||||
// fmt.Println("O_Comment", O_Comment)
|
||||
// fmt.Println("O_LType", O_LType)
|
||||
// fmt.Println("O_SType", O_SType)
|
||||
// fmt.Println("O_TType", O_TType)
|
||||
if p2, ok := p.(*shp.PolyLine); ok {
|
||||
polyline := Polyline{}
|
||||
polyline.Alpha = O_LAlpha
|
||||
polyline.Color = O_LClr
|
||||
polyline.Width = O_LWidth
|
||||
polyline.Name = name
|
||||
polyline.Range.MinX = p.BBox().MinX
|
||||
polyline.Range.MinY = p.BBox().MinY
|
||||
polyline.Range.MaxX = p.BBox().MaxX
|
||||
polyline.Range.MaxY = p.BBox().MaxX
|
||||
for _, po := range p2.Points {
|
||||
point := Point{Lng: po.X, Lat: po.Y}
|
||||
polyline.Positions = append(polyline.Positions, point)
|
||||
}
|
||||
obj.Polylines = append(obj.Polylines, polyline)
|
||||
} else if p3, ok2 := p.(*shp.Polygon); ok2 {
|
||||
polyline := Polyline{}
|
||||
polyline.Alpha = O_LAlpha
|
||||
polyline.Color = O_LClr
|
||||
polyline.Width = O_LWidth
|
||||
polyline.Name = name
|
||||
polyline.Beizhu = beizhu
|
||||
polyline.Tishi = tishi
|
||||
// polyline.Property = Property
|
||||
polyline.Range.MinX = p.BBox().MinX
|
||||
polyline.Range.MinY = p.BBox().MinY
|
||||
polyline.Range.MaxX = p.BBox().MaxX
|
||||
polyline.Range.MaxY = p.BBox().MaxX
|
||||
for _, po := range p3.Points {
|
||||
point := Point{Lng: po.X, Lat: po.Y}
|
||||
polyline.Positions = append(polyline.Positions, point)
|
||||
}
|
||||
obj.Polylines = append(obj.Polylines, polyline)
|
||||
// fmt.Println(p3.Points)
|
||||
} else if p3, ok3 := p.(*shp.Point); ok3 {
|
||||
point := Point{Lng: p3.X, Lat: p3.Y}
|
||||
point.Name = name
|
||||
point.Tishi = tishi
|
||||
point.Beizhu = beizhu
|
||||
obj.Points = append(obj.Points, point)
|
||||
} else if p3, ok2 := p.(*shp.PolygonZ); ok2 {
|
||||
polyline := Polyline{}
|
||||
polyline.Alpha = O_LAlpha
|
||||
polyline.Color = O_LClr
|
||||
polyline.Width = O_LWidth
|
||||
polyline.Name = name
|
||||
polyline.Beizhu = beizhu
|
||||
polyline.Tishi = tishi
|
||||
// polyline.Property = Property
|
||||
polyline.Range.MinX = p.BBox().MinX
|
||||
polyline.Range.MinY = p.BBox().MinY
|
||||
polyline.Range.MaxX = p.BBox().MaxX
|
||||
polyline.Range.MaxY = p.BBox().MaxX
|
||||
for _, po := range p3.Points {
|
||||
|
||||
point := Point{Lng: po.X, Lat: po.Y}
|
||||
polyline.Positions = append(polyline.Positions, point)
|
||||
}
|
||||
obj.Polylines = append(obj.Polylines, polyline)
|
||||
// fmt.Println(p3.Points)
|
||||
} else if p3, ok3 := p.(*shp.PointZ); ok3 {
|
||||
point := Point{Lng: p3.X, Lat: p3.Y}
|
||||
point.Name = name
|
||||
point.Tishi = tishi
|
||||
point.Beizhu = beizhu
|
||||
obj.Points = append(obj.Points, point)
|
||||
} else {
|
||||
fmt.Println("其他类型")
|
||||
}
|
||||
}
|
||||
return nil, &obj
|
||||
}
|
||||
|
||||
/*判断点是否被区域包含*/
|
||||
func PointInPolygon(point Point, positions []Point) (bool, error) {
|
||||
if len(positions) < 3 {
|
||||
return false, globe.GetErrors("坐标点数量不能小于3")
|
||||
}
|
||||
polygon := geometry.Polygon{}
|
||||
var pts []geometry.Point
|
||||
for _, position := range positions {
|
||||
pts = append(pts, geometry.Point{Lat: position.Lat, Lng: point.Lng})
|
||||
}
|
||||
// pts = append(pts, pts[len(pts)-1])
|
||||
|
||||
LineString := geometry.LineString{}
|
||||
LineString.Coordinates = pts
|
||||
polygon.Coordinates = []geometry.LineString{LineString}
|
||||
|
||||
return turf.PointInPolygon(geometry.Point{Lat: point.Lat, Lng: point.Lng}, polygon)
|
||||
}
|
133
api/v1/common/source/clt/clt.go
Normal file
133
api/v1/common/source/clt/clt.go
Normal file
@ -0,0 +1,133 @@
|
||||
package clt
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"compress/gzip"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/gogf/gf/v2/net/ghttp"
|
||||
"github.com/tiger1103/gfast/v3/api/v1/common/globe"
|
||||
"github.com/tiger1103/gfast/v3/api/v1/common/tool"
|
||||
"github.com/tiger1103/gfast/v3/database"
|
||||
"github.com/tiger1103/gfast/v3/database/sqlite"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"path"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func InitCltData(group *ghttp.RouterGroup) {
|
||||
group.GET("/data/tileset/{source_id}/*.action", cltCallback)
|
||||
group.GET("/data/bim/{source_id}/*.action", cltCallback)
|
||||
}
|
||||
|
||||
func GetTile(sourceid, p string) []byte {
|
||||
md5 := tool.Md5V(p)
|
||||
tile := globe.Tile{}
|
||||
database.GetSourceDB(sourceid).DB.Select("tile").Where("md5=?", md5).First(&tile)
|
||||
|
||||
// 创建一个字节缓冲区,并将压缩数据写入其中
|
||||
buf := bytes.NewBuffer(tile.Tile)
|
||||
// 创建一个gzip.Reader对象,用于解压缩数据
|
||||
reader, _ := gzip.NewReader(buf)
|
||||
defer reader.Close()
|
||||
// 读取解压缩后的数据
|
||||
decompressedData, _ := io.ReadAll(reader)
|
||||
return decompressedData
|
||||
}
|
||||
func cltCallback(request *ghttp.Request) {
|
||||
sourceId := request.Get("source_id").String()
|
||||
cltObj := database.GetSourceDB(sourceId)
|
||||
if cltObj.DB == nil {
|
||||
request.Response.WriteStatus(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
argcs := strings.Split(request.RequestURI, "/")
|
||||
argcs = argcs[7:]
|
||||
md5 := tool.Md5V(strings.Join(argcs, "/"))
|
||||
tile := globe.Tile{}
|
||||
RowsAffected := cltObj.DB.Select("tile").Where("md5=?", md5).Find(&tile).RowsAffected
|
||||
if RowsAffected == 0 {
|
||||
request.Response.WriteStatus(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
suffix := path.Ext(request.RequestURI)
|
||||
if suffix == ".json" {
|
||||
request.Response.Header().Set("content-type", "application/json")
|
||||
} else {
|
||||
request.Response.Header().Set("content-type", "application/octet-stream")
|
||||
}
|
||||
if cltObj.Gzip {
|
||||
request.Response.Header().Set("Content-Encoding", "gzip")
|
||||
}
|
||||
globe.RenderData(request, tile.Tile)
|
||||
}
|
||||
|
||||
type Info struct {
|
||||
Params string `json:"params"`
|
||||
}
|
||||
|
||||
type parseIsZip struct {
|
||||
Zip bool `json:"zip"`
|
||||
}
|
||||
type IsJct struct {
|
||||
Jct bool `json:"jct"`
|
||||
}
|
||||
|
||||
func OpenClt(cltPath string, sourceID string) (error, *database.SourceObj) {
|
||||
//if !globe.IS_OFFLINE_VERSION {
|
||||
// //网络版事 需要拼接数据地址,方便服务器迁移
|
||||
|
||||
if !tool.PathExists(cltPath) {
|
||||
getwd, err := os.Getwd()
|
||||
if err != nil {
|
||||
return err, nil
|
||||
}
|
||||
cltPath = path.Join(getwd, cltPath)
|
||||
}
|
||||
|
||||
//}
|
||||
fmt.Println(cltPath)
|
||||
if tool.PathExists(cltPath) {
|
||||
db, err := sqlite.OpenDB(cltPath)
|
||||
if err != nil {
|
||||
return err, nil
|
||||
}
|
||||
var obj database.SourceObj
|
||||
obj.DB = db
|
||||
var info []Info
|
||||
db.Model(&Info{}).Find(&info)
|
||||
p := parseIsZip{}
|
||||
errs := json.Unmarshal([]byte(info[0].Params), &p)
|
||||
if errs == nil {
|
||||
obj.Gzip = p.Zip
|
||||
}
|
||||
suffix := path.Ext(cltPath)
|
||||
if suffix == globe.CLT {
|
||||
obj.Type = globe.TILESET
|
||||
obj.Url = "/zm/api/v1/data/tileset/" + sourceID + "/tileset.json"
|
||||
|
||||
} else {
|
||||
obj.Type = globe.BIM
|
||||
obj.Url = "/zm/api/v1/data/bim/" + sourceID + "/tileset.json"
|
||||
if len(info) < 2 {
|
||||
//非jct资源
|
||||
globe.CloseDB(db)
|
||||
return globe.GetErrors("非jct资源"), nil
|
||||
}
|
||||
isjct := IsJct{}
|
||||
errs := json.Unmarshal([]byte(info[1].Params), &isjct)
|
||||
if errs != nil {
|
||||
globe.CloseDB(db)
|
||||
return globe.GetErrors("jct资源检测失败"), nil
|
||||
}
|
||||
}
|
||||
|
||||
database.SetSourceDB(sourceID, obj)
|
||||
return err, &obj
|
||||
}
|
||||
fmt.Println("资源不存在:" + cltPath)
|
||||
return globe.GetErrors("资源不存在:" + cltPath), nil
|
||||
|
||||
}
|
123
api/v1/common/source/mbt/mbt.go
Normal file
123
api/v1/common/source/mbt/mbt.go
Normal file
@ -0,0 +1,123 @@
|
||||
package mbt
|
||||
|
||||
import (
|
||||
"github.com/gogf/gf/v2/net/ghttp"
|
||||
"github.com/tiger1103/gfast/v3/api/v1/common/globe"
|
||||
"github.com/tiger1103/gfast/v3/api/v1/common/tool"
|
||||
"github.com/tiger1103/gfast/v3/database"
|
||||
"github.com/tiger1103/gfast/v3/database/sqlite"
|
||||
"gorm.io/gorm"
|
||||
"math"
|
||||
"net/http"
|
||||
"os"
|
||||
"path"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func InitMbtData(group *ghttp.RouterGroup) {
|
||||
group.GET("/data/mbt/{source_id}/{z}/{x}/{y}.*", mbtCallback)
|
||||
}
|
||||
|
||||
func mbtCallback(request *ghttp.Request) {
|
||||
sourceId := request.Get("source_id").String()
|
||||
mbtobj := database.GetSourceDB(sourceId)
|
||||
if mbtobj.DB == nil {
|
||||
request.Response.WriteStatus(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
z := request.Get("z").Int()
|
||||
x := request.Get("x").Int()
|
||||
y := request.Get("y").Int()
|
||||
y = int(math.Pow(2, float64(z))) - 1 - y
|
||||
tile := Tile{}
|
||||
RowsAffected := mbtobj.DB.Model(&Tile{}).
|
||||
Select("tile_data").
|
||||
Where(&Tile{ZoomLevel: z, TileColumn: x, TileRow: y}).First(&tile).RowsAffected
|
||||
if RowsAffected > 0 {
|
||||
request.Response.Header().Set("content-type", mbtobj.ContentType)
|
||||
globe.RenderData(request, tile.TileData)
|
||||
return
|
||||
} else {
|
||||
request.Response.WriteStatus(http.StatusNotFound)
|
||||
}
|
||||
}
|
||||
|
||||
type Tile struct {
|
||||
TileData []byte `json:"tile_data"`
|
||||
ZoomLevel int `json:"zoom_level"`
|
||||
TileColumn int `json:"tile_column"`
|
||||
TileRow int `json:"tile_row"`
|
||||
}
|
||||
|
||||
type Metadata struct {
|
||||
Name string
|
||||
Value string
|
||||
}
|
||||
|
||||
func OpenMbt(mbtPath string, sourceID string) (error, *database.SourceObj) {
|
||||
//if !globe.IS_OFFLINE_VERSION {
|
||||
// //网络版事 需要拼接数据地址,方便服务器迁移
|
||||
// mbtPath = path.Join(globe.SOURCE, mbtPath)
|
||||
//}
|
||||
getwd, err := os.Getwd()
|
||||
if err != nil {
|
||||
return err, nil
|
||||
}
|
||||
mbtPath = path.Join(getwd, mbtPath)
|
||||
if !tool.PathExists(mbtPath) {
|
||||
return globe.GetErrors("资源不存在," + mbtPath), nil
|
||||
}
|
||||
db, err := sqlite.OpenDB(mbtPath)
|
||||
if err != nil {
|
||||
return err, nil
|
||||
}
|
||||
var obj database.SourceObj
|
||||
obj.DB = db
|
||||
obj.Type = globe.LAYER
|
||||
var meta []Metadata
|
||||
db.Model(&Metadata{}).Find(&meta)
|
||||
obj.Info.MinLevel, obj.Info.MaxLevel = startQueryLevel(db)
|
||||
var format = "png"
|
||||
for _, v := range meta {
|
||||
if v.Name == "format" {
|
||||
format = v.Value
|
||||
}
|
||||
if v.Name == "bounds" {
|
||||
arr := strings.Split(v.Value, ",")
|
||||
obj.Info.West = arr[0]
|
||||
obj.Info.South = arr[1]
|
||||
obj.Info.East = arr[2]
|
||||
obj.Info.North = arr[3]
|
||||
}
|
||||
if v.Name == "profile" {
|
||||
obj.Info.ProFile = v.Value
|
||||
}
|
||||
if v.Name == "description" {
|
||||
//lsv下载的 自带投影,此时不需要加
|
||||
if strings.Contains(v.Value, "LSV") {
|
||||
obj.Info.TilingScheme = 0
|
||||
} else {
|
||||
obj.Info.TilingScheme = 1
|
||||
}
|
||||
}
|
||||
}
|
||||
obj.ContentType = "image/" + format
|
||||
obj.Url = "/zm/api/v1/data/mbt/" + sourceID + "/{z}/{x}/{y}." + format
|
||||
database.SetSourceDB(sourceID, obj)
|
||||
return err, &obj
|
||||
}
|
||||
func startQueryLevel(db *gorm.DB) (min, max int) {
|
||||
zoom_level := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22}
|
||||
var existsLevels []int
|
||||
for i := 0; i < len(zoom_level); i++ {
|
||||
RowsAffected := db.Model(&Tile{}).Select("zoom_level").Where(&Tile{ZoomLevel: i}).Find(&Tile{}).RowsAffected
|
||||
if RowsAffected > 0 {
|
||||
existsLevels = append(existsLevels, i)
|
||||
}
|
||||
}
|
||||
if len(existsLevels) > 0 {
|
||||
min = existsLevels[0]
|
||||
max = existsLevels[len(existsLevels)-1]
|
||||
}
|
||||
return
|
||||
}
|
148
api/v1/common/source/pak/pak.go
Normal file
148
api/v1/common/source/pak/pak.go
Normal file
@ -0,0 +1,148 @@
|
||||
package pak
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/gogf/gf/v2/net/ghttp"
|
||||
"github.com/tiger1103/gfast/v3/api/v1/common/globe"
|
||||
"github.com/tiger1103/gfast/v3/api/v1/common/tool"
|
||||
"github.com/tiger1103/gfast/v3/database"
|
||||
"github.com/tiger1103/gfast/v3/database/sqlite"
|
||||
"math"
|
||||
"net/http"
|
||||
"os"
|
||||
"path"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
image = "image"
|
||||
terrain = "terrain"
|
||||
)
|
||||
|
||||
func InitPakData(group *ghttp.RouterGroup) {
|
||||
//group.GET("/data/pak/{source_id}/{z}/{x}/{y}.*", pakCallback)
|
||||
//group.GET("/data/pak/{source_id}/layer.json", pakCallback)
|
||||
group.GET("/data/pak/{source_id}/*.action", pakCallback)
|
||||
}
|
||||
|
||||
type Json struct {
|
||||
Layerjson []byte `json:"layerjson"`
|
||||
}
|
||||
|
||||
// 获取pak文件中的表名
|
||||
func gettablename(x int, y int, z int) string {
|
||||
if z < 10 {
|
||||
return "blocks"
|
||||
} else {
|
||||
tx := math.Ceil(float64(x / 512))
|
||||
ty := math.Ceil(float64(y / 512))
|
||||
return "blocks_" + strconv.Itoa(z) + "_" + strconv.Itoa(int(tx)) + "_" + strconv.Itoa(int(ty))
|
||||
}
|
||||
}
|
||||
|
||||
type Tile struct {
|
||||
Tile []byte `json:"tile"`
|
||||
Z int `json:"z"`
|
||||
X int `json:"x"`
|
||||
Y int `json:"y"`
|
||||
}
|
||||
|
||||
func pakCallback(request *ghttp.Request) {
|
||||
sourceId := request.Get("source_id").String()
|
||||
pakobj := database.GetSourceDB(sourceId)
|
||||
if pakobj.DB == nil {
|
||||
request.Response.WriteStatus(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
suffix := path.Ext(request.RequestURI)
|
||||
if suffix == ".json" {
|
||||
json := Json{}
|
||||
pakobj.DB.Model(&Info{}).First(&json)
|
||||
request.Response.Header().Set("content-type", "application/json")
|
||||
globe.RenderData(request, json.Layerjson)
|
||||
return
|
||||
} else {
|
||||
uri := request.RequestURI
|
||||
arr := strings.Split(uri, "/")
|
||||
//z := request.Get("z").Int()
|
||||
//x := request.Get("x").Int()
|
||||
//y := request.Get("y").Int()
|
||||
z, _ := strconv.Atoi(arr[7])
|
||||
x, _ := strconv.Atoi(arr[8])
|
||||
y, _ := strconv.Atoi(strings.Split(arr[9], ".")[0])
|
||||
//y = int(math.Pow(2, float64(z))) - 1 - y
|
||||
tile := Tile{}
|
||||
RowsAffected := pakobj.DB.Table(gettablename(x, y, z)).Select("tile").Where(&Tile{Z: z, X: x, Y: y}).First(&tile).RowsAffected
|
||||
if RowsAffected > 0 {
|
||||
request.Response.Header().Set("content-type", pakobj.ContentType)
|
||||
if pakobj.Gzip {
|
||||
request.Response.Header().Set("Content-Encoding", "gzip")
|
||||
}
|
||||
globe.RenderData(request, tile.Tile)
|
||||
return
|
||||
} else {
|
||||
request.Response.WriteStatus(http.StatusNotFound)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type Info struct {
|
||||
Minx float64 `json:"minx"`
|
||||
Miny float64 `json:"miny"`
|
||||
Maxx float64 `json:"maxx"`
|
||||
Maxy float64 `json:"maxy"`
|
||||
Minlevel int `json:"minlevel"`
|
||||
Maxlevel int `json:"maxlevel"`
|
||||
Type string `json:"type"`
|
||||
Zip int `json:"zip"`
|
||||
//Layerjson []byte `json:"layerjson"`
|
||||
Contenttype string `json:"contenttype"`
|
||||
}
|
||||
|
||||
func OpenPak(pakPath string, sourceID string) (error, *database.SourceObj) {
|
||||
//if !globe.IS_OFFLINE_VERSION {
|
||||
// //网络版事 需要拼接数据地址,方便服务器迁移
|
||||
// pakPath = path.Join(globe.SOURCE, pakPath)
|
||||
//}
|
||||
getwd, err := os.Getwd()
|
||||
if err != nil {
|
||||
return err, nil
|
||||
}
|
||||
pakPath = path.Join(getwd, pakPath)
|
||||
|
||||
if !tool.PathExists(pakPath) {
|
||||
return globe.GetErrors("资源不存在," + pakPath), nil
|
||||
}
|
||||
fmt.Println("资源存在")
|
||||
db, err := sqlite.OpenDB(pakPath)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return err, nil
|
||||
}
|
||||
var obj database.SourceObj
|
||||
obj.DB = db
|
||||
info := Info{}
|
||||
db.Model(&Info{}).First(&info)
|
||||
if info.Type == image {
|
||||
obj.Type = globe.LAYER
|
||||
obj.ContentType = info.Contenttype
|
||||
obj.Url = "/zm/api/v1/data/pak/" + sourceID + "/{z}/{x}/{y}." + strings.Split(obj.ContentType, "/")[1]
|
||||
}
|
||||
if info.Type == terrain {
|
||||
obj.Type = globe.TERRAIN
|
||||
obj.ContentType = "application/octet-stream"
|
||||
obj.Url = "/zm/api/v1/data/pak/" + sourceID + "/"
|
||||
}
|
||||
if info.Zip > 0 {
|
||||
obj.Gzip = true
|
||||
}
|
||||
obj.Info.MaxLevel = info.Maxlevel
|
||||
obj.Info.MinLevel = info.Minlevel
|
||||
obj.Info.West = strconv.FormatFloat(info.Minx, 'f', -1, 64)
|
||||
obj.Info.South = strconv.FormatFloat(info.Miny, 'f', -1, 64)
|
||||
obj.Info.East = strconv.FormatFloat(info.Maxx, 'f', -1, 64)
|
||||
obj.Info.North = strconv.FormatFloat(info.Maxy, 'f', -1, 64)
|
||||
database.SetSourceDB(sourceID, obj)
|
||||
return err, &obj
|
||||
}
|
295
api/v1/common/source/shp/shp.go
Normal file
295
api/v1/common/source/shp/shp.go
Normal file
@ -0,0 +1,295 @@
|
||||
package shp
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"github.com/gogf/gf/v2/net/ghttp"
|
||||
"github.com/tidwall/gjson"
|
||||
shp2 "github.com/tiger1103/gfast/v3/api/v1/common/shp"
|
||||
"io"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const MaxNeighborsLen = 8 //最大邻居个数
|
||||
const SOURCE = "static/source/"
|
||||
const Gisfile = "gisfile/"
|
||||
|
||||
// 84的投影文件
|
||||
const WGS84_PRJ = "GEOGCS[\"GCS_WGS_1984\",DATUM[\"D_WGS_1984\",SPHEROID[\"WGS_1984\",6378137.0,298.257223563]],PRIMEM[\"Greenwich\",0.0],UNIT[\"Degree\",0.0174532925199433]]"
|
||||
|
||||
func InitShp(group *ghttp.RouterGroup) {
|
||||
group.Group("/shp", func(group *ghttp.RouterGroup) {
|
||||
group.Bind(new(SHP))
|
||||
})
|
||||
}
|
||||
|
||||
type SHP struct {
|
||||
}
|
||||
|
||||
type SHPLoadReq struct {
|
||||
g.Meta `path:"load" summary:"cesium加载shp" method:"get" tags:"shp相关" `
|
||||
//SourceID string `json:"source_id" dc:"资源id" v:"required"`
|
||||
Path string `json:"path" dc:"路径" v:"required"`
|
||||
}
|
||||
|
||||
type SHPLoadRes struct {
|
||||
shp2.ShpObj
|
||||
}
|
||||
|
||||
//func (receiver SHP) LoadSHP(ctx context.Context, req *SHPLoadReq) (res *SHPLoadRes, err error) {
|
||||
// err, obj := shp2.ReadShp(req.Path)
|
||||
// if err != nil {
|
||||
// return nil, err
|
||||
// }
|
||||
// res = &SHPLoadRes{}
|
||||
// res.Points = obj.Points
|
||||
// res.Polylines = obj.Polylines
|
||||
// res.Polygons = obj.Polygons
|
||||
// return res, err
|
||||
//}
|
||||
|
||||
type Range struct {
|
||||
MinX float64 `json:"min_x"`
|
||||
MinY float64 `json:"min_y"`
|
||||
MaxX float64 `json:"max_x"`
|
||||
MaxY float64 `json:"max_y"`
|
||||
}
|
||||
|
||||
type Position struct {
|
||||
X float64 `json:"x"`
|
||||
Y float64 `json:"y"`
|
||||
Z float64 `json:"z"`
|
||||
Attr map[string]interface{} `json:"attr"`
|
||||
}
|
||||
|
||||
type Text struct {
|
||||
X float64 `json:"x"`
|
||||
Y float64 `json:"y"`
|
||||
Text string `json:"text"`
|
||||
}
|
||||
|
||||
type Circle struct {
|
||||
X float64 `json:"x"`
|
||||
Y float64 `json:"y"`
|
||||
Z float64 `json:"z"`
|
||||
Radius float64 `json:"radius"`
|
||||
}
|
||||
|
||||
type Polyline struct {
|
||||
Positions []Position `json:"positions"`
|
||||
Attr map[string]interface{} `json:"attr"`
|
||||
Range Range `json:"range"`
|
||||
}
|
||||
|
||||
type Polygon struct {
|
||||
Positions []Position `json:"positions"`
|
||||
Attr map[string]interface{} `json:"attr"`
|
||||
Range Range `json:"range"`
|
||||
}
|
||||
|
||||
type MultiPolygon struct {
|
||||
Polygons []Polygon `json:"polygons"`
|
||||
Attr map[string]interface{} `json:"attr"`
|
||||
Range Range `json:"range"`
|
||||
}
|
||||
|
||||
type MultiPolyline struct {
|
||||
Polylines []Polyline `json:"polylines"`
|
||||
Attr map[string]interface{} `json:"attr"`
|
||||
Range Range `json:"range"`
|
||||
}
|
||||
|
||||
type LayerData struct {
|
||||
LayerName string `json:"layer_name"`
|
||||
Proj4 string `json:"proj4"`
|
||||
Texts []Text `json:"texts"`
|
||||
Circles []Circle `json:"circles"`
|
||||
Points []Position `json:"points"`
|
||||
Polylines []Polyline `json:"polylines"`
|
||||
Polygons []Polygon `json:"polygons"`
|
||||
MultiPolygons []MultiPolygon `json:"multi_polygons"`
|
||||
MultiPolylines []MultiPolyline `json:"multi_polylines"`
|
||||
}
|
||||
|
||||
type Directory struct {
|
||||
Name string `json:"name"`
|
||||
IsDir bool `json:"is_dir"`
|
||||
Data []LayerData `json:"data"`
|
||||
Children []Directory `json:"children"`
|
||||
}
|
||||
|
||||
type Response struct {
|
||||
Code int `json:"code"`
|
||||
Data Directory `json:"data"`
|
||||
}
|
||||
|
||||
// JoinLoadSHP 原本的LoadSHP有问题,直接调用远程的接口
|
||||
func (receiver SHP) JoinLoadSHP(ctx context.Context, req *SHPLoadReq) (res *SHPLoadRes, err error) {
|
||||
res = new(SHPLoadRes)
|
||||
if req.Path[0] == '/' {
|
||||
req.Path = req.Path[1:]
|
||||
}
|
||||
|
||||
url := "http://192.168.1.177:8921/yjearth5/api/v1/vector/load?path=/project/zmkg/" + req.Path
|
||||
|
||||
reqs, err := http.Get(url)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer reqs.Body.Close()
|
||||
|
||||
body, err := io.ReadAll(reqs.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if gjson.Get(string(body), "message").String() == "资源不存在" {
|
||||
return nil, fmt.Errorf("资源不存在")
|
||||
}
|
||||
|
||||
var list shp2.ShpObj
|
||||
processShapes(body, "data.children.0.data.0.polylines", &list)
|
||||
processShapes(body, "data.children.0.data.0.polygons", &list)
|
||||
processShapes(body, "data.children.0.data", &list)
|
||||
|
||||
if list.Polylines == nil {
|
||||
list.Polylines = []shp2.Polyline{}
|
||||
}
|
||||
|
||||
if list.Polygons == nil {
|
||||
list.Polygons = []shp2.Polygon{}
|
||||
}
|
||||
|
||||
if list.Points == nil {
|
||||
list.Points = []shp2.Point{}
|
||||
}
|
||||
|
||||
res.Polygons = list.Polygons
|
||||
res.Polylines = list.Polylines
|
||||
res.Points = list.Points
|
||||
|
||||
return res, err
|
||||
}
|
||||
|
||||
func Adasda(path string) (res *SHPLoadRes) {
|
||||
fmt.Println("加载shp文件", path)
|
||||
res = new(SHPLoadRes)
|
||||
if path[0] == '/' {
|
||||
path = path[1:]
|
||||
}
|
||||
|
||||
url := "http://192.168.1.177:8921/yjearth5/api/v1/vector/load?path=/project/zmkg/" + path
|
||||
|
||||
reqs, err := http.Get(url)
|
||||
if err != nil {
|
||||
fmt.Println("请求数据失败")
|
||||
}
|
||||
defer reqs.Body.Close()
|
||||
|
||||
body, err := io.ReadAll(reqs.Body)
|
||||
if err != nil {
|
||||
fmt.Errorf("读取资源失败")
|
||||
}
|
||||
|
||||
if gjson.Get(string(body), "message").String() == "资源不存在" {
|
||||
fmt.Errorf("资源不存在")
|
||||
}
|
||||
|
||||
var list = shp2.ShpObj{
|
||||
Polylines: []shp2.Polyline{},
|
||||
Polygons: []shp2.Polygon{},
|
||||
Points: []shp2.Point{},
|
||||
}
|
||||
processShapes(body, "data.children.0.data.0.polylines", &list)
|
||||
processShapes(body, "data.children.0.data.0.polygons", &list)
|
||||
processShapes(body, "data.children.0.data", &list)
|
||||
|
||||
res.Polygons = list.Polygons
|
||||
res.Polylines = list.Polylines
|
||||
res.Points = list.Points
|
||||
return
|
||||
}
|
||||
|
||||
func processShapes(data []byte, path string, shapes *shp2.ShpObj) {
|
||||
gjson.GetBytes(data, path).ForEach(func(key, shape gjson.Result) bool {
|
||||
// 面
|
||||
var pointsList []shp2.Point
|
||||
psGet := shape.Get("positions")
|
||||
if psGet.Exists() {
|
||||
psGet.ForEach(func(posKey, value gjson.Result) bool {
|
||||
longitude := value.Get("x").Float()
|
||||
latitude := value.Get("y").Float()
|
||||
altitude := value.Get("z").Float()
|
||||
|
||||
pointsList = append(pointsList, shp2.Point{
|
||||
Lng: longitude,
|
||||
Lat: latitude,
|
||||
Alt: altitude,
|
||||
})
|
||||
return true
|
||||
})
|
||||
if len(pointsList) > 0 {
|
||||
shapes.Polylines = append(shapes.Polylines, shp2.Polyline{
|
||||
Positions: pointsList,
|
||||
Width: "5",
|
||||
Color: "#f00",
|
||||
})
|
||||
}
|
||||
if shape.Get("attr").Exists() {
|
||||
aName := shape.Get("attr.NAME").String()
|
||||
fmt.Println("!!! ", aName)
|
||||
shapes.Polylines[len(shapes.Polylines)-1].Property = shp2.Property{
|
||||
Name: aName,
|
||||
}
|
||||
}
|
||||
if shape.Get("range").Exists() {
|
||||
minX := shape.Get("range.min_x").Float()
|
||||
minY := shape.Get("range.min_y").Float()
|
||||
maxX := shape.Get("range.max_x").Float()
|
||||
maxY := shape.Get("range.max_y").Float()
|
||||
shapes.Polylines[len(shapes.Polylines)-1].Range = shp2.Box{
|
||||
MinX: minX,
|
||||
MinY: minY,
|
||||
MaxX: maxX,
|
||||
MaxY: maxY,
|
||||
}
|
||||
}
|
||||
} else {
|
||||
//fmt.Println("!!! ", shape.Get("points"))
|
||||
// 点
|
||||
var point []shp2.Point
|
||||
shape.Get("points").ForEach(func(posKey, value gjson.Result) bool {
|
||||
aName := value.Get("attr.NAME")
|
||||
if value.Get("attr.NAME").Exists() {
|
||||
//排除nc 和 空
|
||||
isPureNumber, _ := regexp.MatchString(`^\d+$`, aName.String())
|
||||
if strings.Contains(aName.String(), "NC") || strings.TrimSpace(aName.String()) == "" || isPureNumber {
|
||||
return true
|
||||
}
|
||||
longitude := value.Get("x").Float()
|
||||
latitude := value.Get("y").Float()
|
||||
altitude := value.Get("z").Float()
|
||||
|
||||
point = append(point, shp2.Point{
|
||||
Lng: longitude,
|
||||
Lat: latitude,
|
||||
Alt: altitude,
|
||||
Property: shp2.Property{
|
||||
Name: aName.String(),
|
||||
},
|
||||
})
|
||||
|
||||
}
|
||||
return true
|
||||
})
|
||||
if len(point) > 0 {
|
||||
shapes.Points = append(shapes.Points, point...)
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
})
|
||||
}
|
85
api/v1/common/source/source.go
Normal file
85
api/v1/common/source/source.go
Normal file
@ -0,0 +1,85 @@
|
||||
package source
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"github.com/gogf/gf/v2/net/ghttp"
|
||||
"github.com/gogf/gf/v2/os/gctx"
|
||||
"github.com/tiger1103/gfast/v3/api/v1/common/globe"
|
||||
"github.com/tiger1103/gfast/v3/api/v1/common/source/clt"
|
||||
"github.com/tiger1103/gfast/v3/api/v1/common/source/pak"
|
||||
"github.com/tiger1103/gfast/v3/database"
|
||||
"github.com/tiger1103/gfast/v3/internal/app/system/dao"
|
||||
"path"
|
||||
)
|
||||
|
||||
func InitSource(group *ghttp.RouterGroup) {
|
||||
ReadAllSourceFromDB()
|
||||
group.Group("/data/service", func(group *ghttp.RouterGroup) {
|
||||
group.Bind(new(LoadSource))
|
||||
})
|
||||
}
|
||||
|
||||
type LoadSource struct {
|
||||
}
|
||||
type LoadSourceReq struct {
|
||||
g.Meta `path:"load-compact-service" summary:"引擎加载资源" method:"post" tags:"资源相关" `
|
||||
SourceID string `json:"source_id" v:"required" dc:"资源id"`
|
||||
}
|
||||
type LoadSourceRes struct {
|
||||
Type string `json:"type"`
|
||||
Url string `json:"url"`
|
||||
database.SourceInfo
|
||||
}
|
||||
|
||||
func (LoadSource) LoadCompactService(ctx context.Context, req *LoadSourceReq) (res *LoadSourceRes, err error) {
|
||||
obj := database.GetSourceDB(req.SourceID)
|
||||
res = &LoadSourceRes{
|
||||
Url: obj.Url,
|
||||
Type: obj.Type,
|
||||
}
|
||||
res.North = obj.Info.North
|
||||
res.West = obj.Info.West
|
||||
res.East = obj.Info.East
|
||||
res.South = obj.Info.South
|
||||
res.ProFile = obj.Info.ProFile
|
||||
res.TilingScheme = obj.Info.TilingScheme
|
||||
res.MaxLevel = obj.Info.MaxLevel
|
||||
res.MinLevel = obj.Info.MinLevel
|
||||
return
|
||||
}
|
||||
|
||||
func ReadAllSourceFromDB() {
|
||||
var sources []database.SOURCE
|
||||
var gfb []database.SOURCE
|
||||
g.DB().Model(&database.SOURCE{})
|
||||
ctx := gctx.New()
|
||||
//模型
|
||||
dao.QianqiMoxing.Ctx(ctx).Scan(&sources)
|
||||
//光伏板
|
||||
dao.QianqiGuangfuban.Ctx(ctx).Scan(&gfb)
|
||||
sources = append(sources, gfb...)
|
||||
for _, v := range sources {
|
||||
suffix := path.Ext(v.SourcePath)
|
||||
switch suffix {
|
||||
case globe.CLT:
|
||||
err, obj := clt.OpenClt(v.SourcePath, v.SourceID)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
}
|
||||
marshal, _ := json.Marshal(obj)
|
||||
fmt.Println(string(marshal), v.SourceID)
|
||||
break
|
||||
case globe.PAK:
|
||||
err, obj := pak.OpenPak(v.SourcePath, v.SourceID)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
}
|
||||
marshal, _ := json.Marshal(obj)
|
||||
fmt.Println(string(marshal), v.SourceID)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
49
api/v1/common/tool/excel/excel.go
Normal file
49
api/v1/common/tool/excel/excel.go
Normal file
@ -0,0 +1,49 @@
|
||||
package excel
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/tiger1103/gfast/v3/api/v1/common/tool"
|
||||
"github.com/xuri/excelize/v2"
|
||||
)
|
||||
|
||||
type Sheet struct {
|
||||
Name string `json:"name"`
|
||||
Rows [][]string `json:"rows"`
|
||||
}
|
||||
|
||||
func ReadXlsx(xlsx string) (err error, sheet []Sheet) {
|
||||
if !tool.PathExists(xlsx) {
|
||||
return errors.New("文件不存在:" + xlsx), sheet
|
||||
}
|
||||
f, err := excelize.OpenFile(xlsx)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return err, sheet
|
||||
}
|
||||
defer func() {
|
||||
// 关闭工作簿
|
||||
if err := f.Close(); err != nil {
|
||||
fmt.Println(err)
|
||||
}
|
||||
}()
|
||||
list := f.GetSheetList()
|
||||
// 获取 Sheet1 上所有单元格
|
||||
for _, sheetName := range list {
|
||||
result, err := f.GetRows(sheetName)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
continue
|
||||
}
|
||||
sheet = append(sheet, Sheet{sheetName, result})
|
||||
//rows = append(rows, result...)
|
||||
}
|
||||
|
||||
//for _, row := range rows {
|
||||
// for _, colCell := range row {
|
||||
// fmt.Print(colCell, "\t")
|
||||
// }
|
||||
// fmt.Println()
|
||||
//}
|
||||
return nil, sheet
|
||||
}
|
25
api/v1/common/tool/proj/proj.go
Normal file
25
api/v1/common/tool/proj/proj.go
Normal file
@ -0,0 +1,25 @@
|
||||
package proj
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
"github.com/dop251/goja"
|
||||
)
|
||||
|
||||
//go:embed proj4.js
|
||||
var proj4 string
|
||||
|
||||
var CGCS2000_to_WGS84 func(degrees int, cscs2000 [][]string) string
|
||||
var WGS84_to_CGCS2000 func(degrees int, wgs84 [][]string) string
|
||||
|
||||
func InitProj() {
|
||||
vm := goja.New()
|
||||
vm.RunString(proj4)
|
||||
vm.ExportTo(vm.Get("CGCS2000_to_WGS84"), &CGCS2000_to_WGS84)
|
||||
vm.ExportTo(vm.Get("WGS84_to_CGCS2000"), &WGS84_to_CGCS2000)
|
||||
//var ss [][]string
|
||||
//ss = append(ss, []string{
|
||||
// "106.545463204423", "23.467020901621", "805.6832",
|
||||
//})
|
||||
//s := WGS84_to_CGCS2000(108, ss)
|
||||
//fmt.Println(s)
|
||||
}
|
1994
api/v1/common/tool/proj/proj4.js
Normal file
1994
api/v1/common/tool/proj/proj4.js
Normal file
File diff suppressed because it is too large
Load Diff
2
api/v1/common/tool/shp/.hound.yml
Normal file
2
api/v1/common/tool/shp/.hound.yml
Normal file
@ -0,0 +1,2 @@
|
||||
go:
|
||||
enabled: true
|
19
api/v1/common/tool/shp/.travis.yml
Normal file
19
api/v1/common/tool/shp/.travis.yml
Normal file
@ -0,0 +1,19 @@
|
||||
language: go
|
||||
sudo: false
|
||||
|
||||
go:
|
||||
- 1.8.x
|
||||
- 1.9.x
|
||||
- master
|
||||
|
||||
os:
|
||||
- linux
|
||||
|
||||
before_install:
|
||||
- go get -t -v ./...
|
||||
|
||||
script:
|
||||
- go test -race -coverprofile=coverage.txt -covermode=atomic
|
||||
|
||||
after_success:
|
||||
- bash <(curl -s https://codecov.io/bash)
|
21
api/v1/common/tool/shp/LICENSE
Normal file
21
api/v1/common/tool/shp/LICENSE
Normal file
@ -0,0 +1,21 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2014 Jonas Palm
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
87
api/v1/common/tool/shp/README.md
Normal file
87
api/v1/common/tool/shp/README.md
Normal file
@ -0,0 +1,87 @@
|
||||
go-shp
|
||||
======
|
||||
|
||||
[](https://travis-ci.org/jonas-p/go-shp)
|
||||
[](https://ci.appveyor.com/project/fawick/go-shp)
|
||||
[](https://goreportcard.com/report/github.com/jonas-p/go-shp)
|
||||
[](https://codecov.io/gh/jonas-p/go-shp)
|
||||
|
||||
Go library for reading and writing ESRI Shapefiles. This is a pure Golang implementation based on the ESRI Shapefile technical description.
|
||||
|
||||
### Usage
|
||||
#### Installation
|
||||
|
||||
go get github.com/jonas-p/go-shp
|
||||
|
||||
#### Importing
|
||||
|
||||
```go
|
||||
import "github.com/jonas-p/go-shp"
|
||||
```
|
||||
|
||||
### Examples
|
||||
#### Reading a shapefile
|
||||
|
||||
```go
|
||||
// open a shapefile for reading
|
||||
shape, err := shp.Open("points.shp")
|
||||
if err != nil { log.Fatal(err) }
|
||||
defer shape.Close()
|
||||
|
||||
// fields from the attribute table (DBF)
|
||||
fields := shape.Fields()
|
||||
|
||||
// loop through all features in the shapefile
|
||||
for shape.Next() {
|
||||
n, p := shape.Shape()
|
||||
|
||||
// print feature
|
||||
fmt.Println(reflect.TypeOf(p).Elem(), p.BBox())
|
||||
|
||||
// print attributes
|
||||
for k, f := range fields {
|
||||
val := shape.ReadAttribute(n, k)
|
||||
fmt.Printf("\t%v: %v\n", f, val)
|
||||
}
|
||||
fmt.Println()
|
||||
}
|
||||
```
|
||||
|
||||
#### Creating a shapefile
|
||||
|
||||
```go
|
||||
// points to write
|
||||
points := []shp.Point{
|
||||
shp.Point{10.0, 10.0},
|
||||
shp.Point{10.0, 15.0},
|
||||
shp.Point{15.0, 15.0},
|
||||
shp.Point{15.0, 10.0},
|
||||
}
|
||||
|
||||
// fields to write
|
||||
fields := []shp.Field{
|
||||
// String attribute field with length 25
|
||||
shp.StringField("NAME", 25),
|
||||
}
|
||||
|
||||
// create and open a shapefile for writing points
|
||||
shape, err := shp.Create("points.shp", shp.POINT)
|
||||
if err != nil { log.Fatal(err) }
|
||||
defer shape.Close()
|
||||
|
||||
// setup fields for attributes
|
||||
shape.SetFields(fields)
|
||||
|
||||
// write points and attributes
|
||||
for n, point := range points {
|
||||
shape.Write(&point)
|
||||
|
||||
// write attribute for object n for field 0 (NAME)
|
||||
shape.WriteAttribute(n, 0, "Point " + strconv.Itoa(n + 1))
|
||||
}
|
||||
```
|
||||
|
||||
### Resources
|
||||
|
||||
- [Documentation on godoc.org](http://godoc.org/github.com/jonas-p/go-shp)
|
||||
- [ESRI Shapefile Technical Description](http://www.esri.com/library/whitepapers/pdfs/shapefile.pdf)
|
26
api/v1/common/tool/shp/appveyor.yml
Normal file
26
api/v1/common/tool/shp/appveyor.yml
Normal file
@ -0,0 +1,26 @@
|
||||
clone_folder: c:\go-shp
|
||||
|
||||
environment:
|
||||
GOPATH: c:\gopath
|
||||
|
||||
branches:
|
||||
only:
|
||||
- master
|
||||
|
||||
init:
|
||||
- ps: >-
|
||||
$app = Get-WmiObject -Class Win32_Product -Filter "Vendor = 'http://golang.org'"
|
||||
|
||||
if ($app) {
|
||||
$app.Uninstall()
|
||||
}
|
||||
|
||||
install:
|
||||
- rmdir c:\go /s /q
|
||||
- appveyor DownloadFile https://storage.googleapis.com/golang/go1.9.windows-amd64.msi
|
||||
- msiexec /i go1.9.windows-amd64.msi /q
|
||||
- go version
|
||||
- go env
|
||||
|
||||
build_script:
|
||||
- go test ./...
|
27
api/v1/common/tool/shp/errreader.go
Normal file
27
api/v1/common/tool/shp/errreader.go
Normal file
@ -0,0 +1,27 @@
|
||||
package shp
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
)
|
||||
|
||||
// errReader is a helper to perform multiple successive read from another reader
|
||||
// and do the error checking only once afterwards. It will not perform any new
|
||||
// reads in case there was an error encountered earlier.
|
||||
type errReader struct {
|
||||
io.Reader
|
||||
e error
|
||||
n int64
|
||||
}
|
||||
|
||||
func (er *errReader) Read(p []byte) (n int, err error) {
|
||||
if er.e != nil {
|
||||
return 0, fmt.Errorf("unable to read after previous error: %v", er.e)
|
||||
}
|
||||
n, err = er.Reader.Read(p)
|
||||
if n < len(p) && err != nil {
|
||||
er.e = err
|
||||
}
|
||||
er.n += int64(n)
|
||||
return n, er.e
|
||||
}
|
253
api/v1/common/tool/shp/reader.go
Normal file
253
api/v1/common/tool/shp/reader.go
Normal file
@ -0,0 +1,253 @@
|
||||
package shp
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"io"
|
||||
"math"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Reader provides a interface for reading Shapefiles. Calls
|
||||
// to the Next method will iterate through the objects in the
|
||||
// Shapefile. After a call to Next the object will be available
|
||||
// through the Shape method.
|
||||
type Reader struct {
|
||||
GeometryType ShapeType
|
||||
bbox Box
|
||||
err error
|
||||
|
||||
shp readSeekCloser
|
||||
shape Shape
|
||||
num int32
|
||||
filename string
|
||||
filelength int64
|
||||
|
||||
dbf readSeekCloser
|
||||
dbfFields []Field
|
||||
dbfNumRecords int32
|
||||
dbfHeaderLength int16
|
||||
dbfRecordLength int16
|
||||
}
|
||||
|
||||
type readSeekCloser interface {
|
||||
io.Reader
|
||||
io.Seeker
|
||||
io.Closer
|
||||
}
|
||||
|
||||
// Open opens a Shapefile for reading.
|
||||
func Open(filename string) (*Reader, error) {
|
||||
ext := filepath.Ext(filename)
|
||||
if strings.ToLower(ext) != ".shp" {
|
||||
return nil, fmt.Errorf("Invalid file extension: %s", filename)
|
||||
}
|
||||
shp, err := os.Open(filename)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
s := &Reader{filename: strings.TrimSuffix(filename, ext), shp: shp}
|
||||
return s, s.readHeaders()
|
||||
}
|
||||
|
||||
// BBox returns the bounding box of the shapefile.
|
||||
func (r *Reader) BBox() Box {
|
||||
return r.bbox
|
||||
}
|
||||
|
||||
// Read and parse headers in the Shapefile. This will
|
||||
// fill out GeometryType, filelength and bbox.
|
||||
func (r *Reader) readHeaders() error {
|
||||
er := &errReader{Reader: r.shp}
|
||||
// don't trust the the filelength in the header
|
||||
r.filelength, _ = r.shp.Seek(0, io.SeekEnd)
|
||||
|
||||
var filelength int32
|
||||
r.shp.Seek(24, 0)
|
||||
// file length
|
||||
binary.Read(er, binary.BigEndian, &filelength)
|
||||
r.shp.Seek(32, 0)
|
||||
binary.Read(er, binary.LittleEndian, &r.GeometryType)
|
||||
r.bbox.MinX = readFloat64(er)
|
||||
r.bbox.MinY = readFloat64(er)
|
||||
r.bbox.MaxX = readFloat64(er)
|
||||
r.bbox.MaxY = readFloat64(er)
|
||||
r.shp.Seek(100, 0)
|
||||
return er.e
|
||||
}
|
||||
|
||||
func readFloat64(r io.Reader) float64 {
|
||||
var bits uint64
|
||||
binary.Read(r, binary.LittleEndian, &bits)
|
||||
return math.Float64frombits(bits)
|
||||
}
|
||||
|
||||
// Close closes the Shapefile.
|
||||
func (r *Reader) Close() error {
|
||||
if r.err == nil {
|
||||
r.err = r.shp.Close()
|
||||
if r.dbf != nil {
|
||||
r.dbf.Close()
|
||||
}
|
||||
}
|
||||
return r.err
|
||||
}
|
||||
|
||||
// Shape returns the most recent feature that was read by
|
||||
// a call to Next. It returns two values, the int is the
|
||||
// object index starting from zero in the shapefile which
|
||||
// can be used as row in ReadAttribute, and the Shape is the object.
|
||||
func (r *Reader) Shape() (int, Shape) {
|
||||
return int(r.num) - 1, r.shape
|
||||
}
|
||||
|
||||
// Attribute returns value of the n-th attribute of the most recent feature
|
||||
// that was read by a call to Next.
|
||||
func (r *Reader) Attribute(n int) string {
|
||||
return r.ReadAttribute(int(r.num)-1, n)
|
||||
}
|
||||
|
||||
// newShape creates a new shape with a given type.
|
||||
func newShape(shapetype ShapeType) (Shape, error) {
|
||||
switch shapetype {
|
||||
case NULL:
|
||||
return new(Null), nil
|
||||
case POINT:
|
||||
return new(Point), nil
|
||||
case POLYLINE:
|
||||
return new(PolyLine), nil
|
||||
case POLYGON:
|
||||
return new(Polygon), nil
|
||||
case MULTIPOINT:
|
||||
return new(MultiPoint), nil
|
||||
case POINTZ:
|
||||
return new(PointZ), nil
|
||||
case POLYLINEZ:
|
||||
return new(PolyLineZ), nil
|
||||
case POLYGONZ:
|
||||
return new(PolygonZ), nil
|
||||
case MULTIPOINTZ:
|
||||
return new(MultiPointZ), nil
|
||||
case POINTM:
|
||||
return new(PointM), nil
|
||||
case POLYLINEM:
|
||||
return new(PolyLineM), nil
|
||||
case POLYGONM:
|
||||
return new(PolygonM), nil
|
||||
case MULTIPOINTM:
|
||||
return new(MultiPointM), nil
|
||||
case MULTIPATCH:
|
||||
return new(MultiPatch), nil
|
||||
default:
|
||||
return nil, fmt.Errorf("Unsupported shape type: %v", shapetype)
|
||||
}
|
||||
}
|
||||
|
||||
// Next reads in the next Shape in the Shapefile, which
|
||||
// will then be available through the Shape method. It
|
||||
// returns false when the reader has reached the end of the
|
||||
// file or encounters an error.
|
||||
func (r *Reader) Next() bool {
|
||||
cur, _ := r.shp.Seek(0, io.SeekCurrent)
|
||||
if cur >= r.filelength {
|
||||
return false
|
||||
}
|
||||
|
||||
var size int32
|
||||
var shapetype ShapeType
|
||||
er := &errReader{Reader: r.shp}
|
||||
binary.Read(er, binary.BigEndian, &r.num)
|
||||
binary.Read(er, binary.BigEndian, &size)
|
||||
binary.Read(er, binary.LittleEndian, &shapetype)
|
||||
if er.e != nil {
|
||||
if er.e != io.EOF {
|
||||
r.err = fmt.Errorf("Error when reading metadata of next shape: %v", er.e)
|
||||
} else {
|
||||
r.err = io.EOF
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
var err error
|
||||
r.shape, err = newShape(shapetype)
|
||||
if err != nil {
|
||||
r.err = fmt.Errorf("Error decoding shape type: %v", err)
|
||||
return false
|
||||
}
|
||||
r.shape.read(er)
|
||||
if er.e != nil {
|
||||
r.err = fmt.Errorf("Error while reading next shape: %v", er.e)
|
||||
return false
|
||||
}
|
||||
|
||||
// move to next object
|
||||
r.shp.Seek(int64(size)*2+cur+8, 0)
|
||||
return true
|
||||
}
|
||||
|
||||
// Opens DBF file using r.filename + "dbf". This method
|
||||
// will parse the header and fill out all dbf* values int
|
||||
// the f object.
|
||||
func (r *Reader) openDbf() (err error) {
|
||||
if r.dbf != nil {
|
||||
return
|
||||
}
|
||||
|
||||
r.dbf, err = os.Open(r.filename + ".dbf")
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// read header
|
||||
r.dbf.Seek(4, io.SeekStart)
|
||||
binary.Read(r.dbf, binary.LittleEndian, &r.dbfNumRecords)
|
||||
binary.Read(r.dbf, binary.LittleEndian, &r.dbfHeaderLength)
|
||||
binary.Read(r.dbf, binary.LittleEndian, &r.dbfRecordLength)
|
||||
|
||||
r.dbf.Seek(20, io.SeekCurrent) // skip padding
|
||||
numFields := int(math.Floor(float64(r.dbfHeaderLength-33) / 32.0))
|
||||
r.dbfFields = make([]Field, numFields)
|
||||
binary.Read(r.dbf, binary.LittleEndian, &r.dbfFields)
|
||||
return
|
||||
}
|
||||
|
||||
// Fields returns a slice of Fields that are present in the
|
||||
// DBF table.
|
||||
func (r *Reader) Fields() []Field {
|
||||
err := r.openDbf()
|
||||
fmt.Println(err)
|
||||
if err != nil {
|
||||
return nil
|
||||
} // make sure we have dbf file to read from
|
||||
return r.dbfFields
|
||||
}
|
||||
|
||||
// Err returns the last non-EOF error encountered.
|
||||
func (r *Reader) Err() error {
|
||||
if r.err == io.EOF {
|
||||
return nil
|
||||
}
|
||||
return r.err
|
||||
}
|
||||
|
||||
// AttributeCount returns number of records in the DBF table.
|
||||
func (r *Reader) AttributeCount() int {
|
||||
r.openDbf() // make sure we have a dbf file to read from
|
||||
return int(r.dbfNumRecords)
|
||||
}
|
||||
|
||||
// ReadAttribute returns the attribute value at row for field in
|
||||
// the DBF table as a string. Both values starts at 0.
|
||||
func (r *Reader) ReadAttribute(row int, field int) string {
|
||||
r.openDbf() // make sure we have a dbf file to read from
|
||||
seekTo := 1 + int64(r.dbfHeaderLength) + (int64(row) * int64(r.dbfRecordLength))
|
||||
for n := 0; n < field; n++ {
|
||||
seekTo += int64(r.dbfFields[n].Size)
|
||||
}
|
||||
r.dbf.Seek(seekTo, io.SeekStart)
|
||||
buf := make([]byte, r.dbfFields[field].Size)
|
||||
r.dbf.Read(buf)
|
||||
return strings.Trim(string(buf[:]), " ")
|
||||
}
|
527
api/v1/common/tool/shp/reader_test.go
Normal file
527
api/v1/common/tool/shp/reader_test.go
Normal file
@ -0,0 +1,527 @@
|
||||
package shp
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func pointsEqual(a, b []float64) bool {
|
||||
if len(a) != len(b) {
|
||||
return false
|
||||
}
|
||||
for k, v := range a {
|
||||
if v != b[k] {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func getShapesFromFile(prefix string, t *testing.T) (shapes []Shape) {
|
||||
filename := prefix + ".shp"
|
||||
file, err := Open(filename)
|
||||
if err != nil {
|
||||
t.Fatal("Failed to open shapefile: " + filename + " (" + err.Error() + ")")
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
for file.Next() {
|
||||
_, shape := file.Shape()
|
||||
shapes = append(shapes, shape)
|
||||
}
|
||||
if file.Err() != nil {
|
||||
t.Errorf("Error while getting shapes for %s: %v", prefix, file.Err())
|
||||
}
|
||||
|
||||
return shapes
|
||||
}
|
||||
|
||||
type shapeGetterFunc func(string, *testing.T) []Shape
|
||||
|
||||
type identityTestFunc func(*testing.T, [][]float64, []Shape)
|
||||
|
||||
func testPoint(t *testing.T, points [][]float64, shapes []Shape) {
|
||||
for n, s := range shapes {
|
||||
p, ok := s.(*Point)
|
||||
if !ok {
|
||||
t.Fatal("Failed to type assert.")
|
||||
}
|
||||
if !pointsEqual([]float64{p.X, p.Y}, points[n]) {
|
||||
t.Error("Points did not match.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func testPolyLine(t *testing.T, points [][]float64, shapes []Shape) {
|
||||
for n, s := range shapes {
|
||||
p, ok := s.(*PolyLine)
|
||||
if !ok {
|
||||
t.Fatal("Failed to type assert.")
|
||||
}
|
||||
for k, point := range p.Points {
|
||||
if !pointsEqual(points[n*3+k], []float64{point.X, point.Y}) {
|
||||
t.Error("Points did not match.")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func testPolygon(t *testing.T, points [][]float64, shapes []Shape) {
|
||||
for n, s := range shapes {
|
||||
p, ok := s.(*Polygon)
|
||||
if !ok {
|
||||
t.Fatal("Failed to type assert.")
|
||||
}
|
||||
for k, point := range p.Points {
|
||||
if !pointsEqual(points[n*3+k], []float64{point.X, point.Y}) {
|
||||
t.Error("Points did not match.")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func testMultiPoint(t *testing.T, points [][]float64, shapes []Shape) {
|
||||
for n, s := range shapes {
|
||||
p, ok := s.(*MultiPoint)
|
||||
if !ok {
|
||||
t.Fatal("Failed to type assert.")
|
||||
}
|
||||
for k, point := range p.Points {
|
||||
if !pointsEqual(points[n*3+k], []float64{point.X, point.Y}) {
|
||||
t.Error("Points did not match.")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func testPointZ(t *testing.T, points [][]float64, shapes []Shape) {
|
||||
for n, s := range shapes {
|
||||
p, ok := s.(*PointZ)
|
||||
if !ok {
|
||||
t.Fatal("Failed to type assert.")
|
||||
}
|
||||
if !pointsEqual([]float64{p.X, p.Y, p.Z}, points[n]) {
|
||||
t.Error("Points did not match.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func testPolyLineZ(t *testing.T, points [][]float64, shapes []Shape) {
|
||||
for n, s := range shapes {
|
||||
p, ok := s.(*PolyLineZ)
|
||||
if !ok {
|
||||
t.Fatal("Failed to type assert.")
|
||||
}
|
||||
for k, point := range p.Points {
|
||||
if !pointsEqual(points[n*3+k], []float64{point.X, point.Y, p.ZArray[k]}) {
|
||||
t.Error("Points did not match.")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func testPolygonZ(t *testing.T, points [][]float64, shapes []Shape) {
|
||||
for n, s := range shapes {
|
||||
p, ok := s.(*PolygonZ)
|
||||
if !ok {
|
||||
t.Fatal("Failed to type assert.")
|
||||
}
|
||||
for k, point := range p.Points {
|
||||
if !pointsEqual(points[n*3+k], []float64{point.X, point.Y, p.ZArray[k]}) {
|
||||
t.Error("Points did not match.")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func testMultiPointZ(t *testing.T, points [][]float64, shapes []Shape) {
|
||||
for n, s := range shapes {
|
||||
p, ok := s.(*MultiPointZ)
|
||||
if !ok {
|
||||
t.Fatal("Failed to type assert.")
|
||||
}
|
||||
for k, point := range p.Points {
|
||||
if !pointsEqual(points[n*3+k], []float64{point.X, point.Y, p.ZArray[k]}) {
|
||||
t.Error("Points did not match.")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func testPointM(t *testing.T, points [][]float64, shapes []Shape) {
|
||||
for n, s := range shapes {
|
||||
p, ok := s.(*PointM)
|
||||
if !ok {
|
||||
t.Fatal("Failed to type assert.")
|
||||
}
|
||||
if !pointsEqual([]float64{p.X, p.Y, p.M}, points[n]) {
|
||||
t.Error("Points did not match.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func testPolyLineM(t *testing.T, points [][]float64, shapes []Shape) {
|
||||
for n, s := range shapes {
|
||||
p, ok := s.(*PolyLineM)
|
||||
if !ok {
|
||||
t.Fatal("Failed to type assert.")
|
||||
}
|
||||
for k, point := range p.Points {
|
||||
if !pointsEqual(points[n*3+k], []float64{point.X, point.Y, p.MArray[k]}) {
|
||||
t.Error("Points did not match.")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func testPolygonM(t *testing.T, points [][]float64, shapes []Shape) {
|
||||
for n, s := range shapes {
|
||||
p, ok := s.(*PolygonM)
|
||||
if !ok {
|
||||
t.Fatal("Failed to type assert.")
|
||||
}
|
||||
for k, point := range p.Points {
|
||||
if !pointsEqual(points[n*3+k], []float64{point.X, point.Y, p.MArray[k]}) {
|
||||
t.Error("Points did not match.")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func testMultiPointM(t *testing.T, points [][]float64, shapes []Shape) {
|
||||
for n, s := range shapes {
|
||||
p, ok := s.(*MultiPointM)
|
||||
if !ok {
|
||||
t.Fatal("Failed to type assert.")
|
||||
}
|
||||
for k, point := range p.Points {
|
||||
if !pointsEqual(points[n*3+k], []float64{point.X, point.Y, p.MArray[k]}) {
|
||||
t.Error("Points did not match.")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func testMultiPatch(t *testing.T, points [][]float64, shapes []Shape) {
|
||||
for n, s := range shapes {
|
||||
p, ok := s.(*MultiPatch)
|
||||
if !ok {
|
||||
t.Fatal("Failed to type assert.")
|
||||
}
|
||||
for k, point := range p.Points {
|
||||
if !pointsEqual(points[n*3+k], []float64{point.X, point.Y, p.ZArray[k]}) {
|
||||
t.Error("Points did not match.")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func testshapeIdentity(t *testing.T, prefix string, getter shapeGetterFunc) {
|
||||
shapes := getter(prefix, t)
|
||||
d := dataForReadTests[prefix]
|
||||
if len(shapes) != d.count {
|
||||
t.Errorf("Number of shapes for %s read was wrong. Wanted %d, got %d.", prefix, d.count, len(shapes))
|
||||
}
|
||||
d.tester(t, d.points, shapes)
|
||||
}
|
||||
|
||||
func TestReadBBox(t *testing.T) {
|
||||
tests := []struct {
|
||||
filename string
|
||||
want Box
|
||||
}{
|
||||
{"test_files/multipatch.shp", Box{0, 0, 10, 10}},
|
||||
{"test_files/multipoint.shp", Box{0, 5, 10, 10}},
|
||||
{"test_files/multipointm.shp", Box{0, 5, 10, 10}},
|
||||
{"test_files/multipointz.shp", Box{0, 5, 10, 10}},
|
||||
{"test_files/point.shp", Box{0, 5, 10, 10}},
|
||||
{"test_files/pointm.shp", Box{0, 5, 10, 10}},
|
||||
{"test_files/pointz.shp", Box{0, 5, 10, 10}},
|
||||
{"test_files/polygon.shp", Box{0, 0, 5, 5}},
|
||||
{"test_files/polygonm.shp", Box{0, 0, 5, 5}},
|
||||
{"test_files/polygonz.shp", Box{0, 0, 5, 5}},
|
||||
{"test_files/polyline.shp", Box{0, 0, 25, 25}},
|
||||
{"test_files/polylinem.shp", Box{0, 0, 25, 25}},
|
||||
{"test_files/polylinez.shp", Box{0, 0, 25, 25}},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
r, err := Open(tt.filename)
|
||||
if err != nil {
|
||||
t.Fatalf("%v", err)
|
||||
}
|
||||
if got := r.BBox().MinX; got != tt.want.MinX {
|
||||
t.Errorf("got MinX = %v, want %v", got, tt.want.MinX)
|
||||
}
|
||||
if got := r.BBox().MinY; got != tt.want.MinY {
|
||||
t.Errorf("got MinY = %v, want %v", got, tt.want.MinY)
|
||||
}
|
||||
if got := r.BBox().MaxX; got != tt.want.MaxX {
|
||||
t.Errorf("got MaxX = %v, want %v", got, tt.want.MaxX)
|
||||
}
|
||||
if got := r.BBox().MaxY; got != tt.want.MaxY {
|
||||
t.Errorf("got MaxY = %v, want %v", got, tt.want.MaxY)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type testCaseData struct {
|
||||
points [][]float64
|
||||
tester identityTestFunc
|
||||
count int
|
||||
}
|
||||
|
||||
var dataForReadTests = map[string]testCaseData{
|
||||
"test_files/polygonm": {
|
||||
points: [][]float64{
|
||||
{0, 0, 0},
|
||||
{0, 5, 5},
|
||||
{5, 5, 10},
|
||||
{5, 0, 15},
|
||||
{0, 0, 0},
|
||||
},
|
||||
tester: testPolygonM,
|
||||
count: 1,
|
||||
},
|
||||
"test_files/multipointm": {
|
||||
points: [][]float64{
|
||||
{10, 10, 100},
|
||||
{5, 5, 50},
|
||||
{0, 10, 75},
|
||||
},
|
||||
tester: testMultiPointM,
|
||||
count: 1,
|
||||
},
|
||||
"test_files/multipatch": {
|
||||
points: [][]float64{
|
||||
{0, 0, 0},
|
||||
{10, 0, 0},
|
||||
{10, 10, 0},
|
||||
{0, 10, 0},
|
||||
{0, 0, 0},
|
||||
{0, 10, 0},
|
||||
{0, 10, 10},
|
||||
{0, 0, 10},
|
||||
{0, 0, 0},
|
||||
{0, 10, 0},
|
||||
{10, 0, 0},
|
||||
{10, 0, 10},
|
||||
{10, 10, 10},
|
||||
{10, 10, 0},
|
||||
{10, 0, 0},
|
||||
{0, 0, 0},
|
||||
{0, 0, 10},
|
||||
{10, 0, 10},
|
||||
{10, 0, 0},
|
||||
{0, 0, 0},
|
||||
{10, 10, 0},
|
||||
{10, 10, 10},
|
||||
{0, 10, 10},
|
||||
{0, 10, 0},
|
||||
{10, 10, 0},
|
||||
{0, 0, 10},
|
||||
{0, 10, 10},
|
||||
{10, 10, 10},
|
||||
{10, 0, 10},
|
||||
{0, 0, 10},
|
||||
},
|
||||
tester: testMultiPatch,
|
||||
count: 1,
|
||||
},
|
||||
"test_files/point": {
|
||||
points: [][]float64{
|
||||
{10, 10},
|
||||
{5, 5},
|
||||
{0, 10},
|
||||
},
|
||||
tester: testPoint,
|
||||
count: 3,
|
||||
},
|
||||
"test_files/polyline": {
|
||||
points: [][]float64{
|
||||
{0, 0},
|
||||
{5, 5},
|
||||
{10, 10},
|
||||
{15, 15},
|
||||
{20, 20},
|
||||
{25, 25},
|
||||
},
|
||||
tester: testPolyLine,
|
||||
count: 2,
|
||||
},
|
||||
"test_files/polygon": {
|
||||
points: [][]float64{
|
||||
{0, 0},
|
||||
{0, 5},
|
||||
{5, 5},
|
||||
{5, 0},
|
||||
{0, 0},
|
||||
},
|
||||
tester: testPolygon,
|
||||
count: 1,
|
||||
},
|
||||
"test_files/multipoint": {
|
||||
points: [][]float64{
|
||||
{10, 10},
|
||||
{5, 5},
|
||||
{0, 10},
|
||||
},
|
||||
tester: testMultiPoint,
|
||||
count: 1,
|
||||
},
|
||||
"test_files/pointz": {
|
||||
points: [][]float64{
|
||||
{10, 10, 100},
|
||||
{5, 5, 50},
|
||||
{0, 10, 75},
|
||||
},
|
||||
tester: testPointZ,
|
||||
count: 3,
|
||||
},
|
||||
"test_files/polylinez": {
|
||||
points: [][]float64{
|
||||
{0, 0, 0},
|
||||
{5, 5, 5},
|
||||
{10, 10, 10},
|
||||
{15, 15, 15},
|
||||
{20, 20, 20},
|
||||
{25, 25, 25},
|
||||
},
|
||||
tester: testPolyLineZ,
|
||||
count: 2,
|
||||
},
|
||||
"test_files/polygonz": {
|
||||
points: [][]float64{
|
||||
{0, 0, 0},
|
||||
{0, 5, 5},
|
||||
{5, 5, 10},
|
||||
{5, 0, 15},
|
||||
{0, 0, 0},
|
||||
},
|
||||
tester: testPolygonZ,
|
||||
count: 1,
|
||||
},
|
||||
"test_files/multipointz": {
|
||||
points: [][]float64{
|
||||
{10, 10, 100},
|
||||
{5, 5, 50},
|
||||
{0, 10, 75},
|
||||
},
|
||||
tester: testMultiPointZ,
|
||||
count: 1,
|
||||
},
|
||||
"test_files/pointm": {
|
||||
points: [][]float64{
|
||||
{10, 10, 100},
|
||||
{5, 5, 50},
|
||||
{0, 10, 75},
|
||||
},
|
||||
tester: testPointM,
|
||||
count: 3,
|
||||
},
|
||||
"test_files/polylinem": {
|
||||
points: [][]float64{
|
||||
{0, 0, 0},
|
||||
{5, 5, 5},
|
||||
{10, 10, 10},
|
||||
{15, 15, 15},
|
||||
{20, 20, 20},
|
||||
{25, 25, 25},
|
||||
},
|
||||
tester: testPolyLineM,
|
||||
count: 2,
|
||||
},
|
||||
}
|
||||
|
||||
func TestReadPoint(t *testing.T) {
|
||||
testshapeIdentity(t, "test_files/point", getShapesFromFile)
|
||||
}
|
||||
|
||||
func TestReadPolyLine(t *testing.T) {
|
||||
testshapeIdentity(t, "test_files/polyline", getShapesFromFile)
|
||||
}
|
||||
|
||||
func TestReadPolygon(t *testing.T) {
|
||||
testshapeIdentity(t, "test_files/polygon", getShapesFromFile)
|
||||
}
|
||||
|
||||
func TestReadMultiPoint(t *testing.T) {
|
||||
testshapeIdentity(t, "test_files/multipoint", getShapesFromFile)
|
||||
}
|
||||
|
||||
func TestReadPointZ(t *testing.T) {
|
||||
testshapeIdentity(t, "test_files/pointz", getShapesFromFile)
|
||||
}
|
||||
|
||||
func TestReadPolyLineZ(t *testing.T) {
|
||||
testshapeIdentity(t, "test_files/polylinez", getShapesFromFile)
|
||||
}
|
||||
|
||||
func TestReadPolygonZ(t *testing.T) {
|
||||
testshapeIdentity(t, "test_files/polygonz", getShapesFromFile)
|
||||
}
|
||||
|
||||
func TestReadMultiPointZ(t *testing.T) {
|
||||
testshapeIdentity(t, "test_files/multipointz", getShapesFromFile)
|
||||
}
|
||||
|
||||
func TestReadPointM(t *testing.T) {
|
||||
testshapeIdentity(t, "test_files/pointm", getShapesFromFile)
|
||||
}
|
||||
|
||||
func TestReadPolyLineM(t *testing.T) {
|
||||
testshapeIdentity(t, "test_files/polylinem", getShapesFromFile)
|
||||
}
|
||||
|
||||
func TestReadPolygonM(t *testing.T) {
|
||||
testshapeIdentity(t, "test_files/polygonm", getShapesFromFile)
|
||||
}
|
||||
|
||||
func TestReadMultiPointM(t *testing.T) {
|
||||
testshapeIdentity(t, "test_files/multipointm", getShapesFromFile)
|
||||
}
|
||||
|
||||
func TestReadMultiPatch(t *testing.T) {
|
||||
testshapeIdentity(t, "test_files/multipatch", getShapesFromFile)
|
||||
}
|
||||
|
||||
func newReadSeekCloser(b []byte) readSeekCloser {
|
||||
return struct {
|
||||
io.Closer
|
||||
io.ReadSeeker
|
||||
}{
|
||||
ioutil.NopCloser(nil),
|
||||
bytes.NewReader(b),
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadInvalidShapeType(t *testing.T) {
|
||||
record := []byte{
|
||||
0, 0, 0, 0,
|
||||
0, 0, 0, 0,
|
||||
255, 255, 255, 255, // shape type
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
r interface {
|
||||
Next() bool
|
||||
Err() error
|
||||
}
|
||||
name string
|
||||
}{
|
||||
{&Reader{shp: newReadSeekCloser(record), filelength: int64(len(record))}, "reader"},
|
||||
{&seqReader{shp: newReadSeekCloser(record), filelength: int64(len(record))}, "seqReader"},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
if test.r.Next() {
|
||||
t.Fatal("read unsupported shape type without stopping")
|
||||
}
|
||||
if test.r.Err() == nil {
|
||||
t.Fatal("read unsupported shape type without error")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
235
api/v1/common/tool/shp/sequentialreader.go
Normal file
235
api/v1/common/tool/shp/sequentialreader.go
Normal file
@ -0,0 +1,235 @@
|
||||
package shp
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"math"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// SequentialReader is the interface that allows reading shapes and attributes one after another. It also embeds io.Closer.
|
||||
type SequentialReader interface {
|
||||
// Close() frees the resources allocated by the SequentialReader.
|
||||
io.Closer
|
||||
|
||||
// Next() tries to advance the reading by one shape and one attribute row
|
||||
// and returns true if the read operation could be performed without any
|
||||
// error.
|
||||
Next() bool
|
||||
|
||||
// Shape returns the index and the last read shape. If the SequentialReader
|
||||
// encountered any errors, nil is returned for the Shape.
|
||||
Shape() (int, Shape)
|
||||
|
||||
// Attribute returns the value of the n-th attribute in the current row. If
|
||||
// the SequentialReader encountered any errors, the empty string is
|
||||
// returned.
|
||||
Attribute(n int) string
|
||||
|
||||
// Fields returns the fields of the database. If the SequentialReader
|
||||
// encountered any errors, nil is returned.
|
||||
Fields() []Field
|
||||
|
||||
// Err returns the last non-EOF error encountered.
|
||||
Err() error
|
||||
}
|
||||
|
||||
// Attributes returns all attributes of the shape that sr was last advanced to.
|
||||
func Attributes(sr SequentialReader) []string {
|
||||
if sr.Err() != nil {
|
||||
return nil
|
||||
}
|
||||
s := make([]string, len(sr.Fields()))
|
||||
for i := range s {
|
||||
s[i] = sr.Attribute(i)
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// AttributeCount returns the number of fields of the database.
|
||||
func AttributeCount(sr SequentialReader) int {
|
||||
return len(sr.Fields())
|
||||
}
|
||||
|
||||
// seqReader implements SequentialReader based on external io.ReadCloser
|
||||
// instances
|
||||
type seqReader struct {
|
||||
shp, dbf io.ReadCloser
|
||||
err error
|
||||
|
||||
geometryType ShapeType
|
||||
bbox Box
|
||||
|
||||
shape Shape
|
||||
num int32
|
||||
filelength int64
|
||||
|
||||
dbfFields []Field
|
||||
dbfNumRecords int32
|
||||
dbfHeaderLength int16
|
||||
dbfRecordLength int16
|
||||
dbfRow []byte
|
||||
}
|
||||
|
||||
// Read and parse headers in the Shapefile. This will fill out GeometryType,
|
||||
// filelength and bbox.
|
||||
func (sr *seqReader) readHeaders() {
|
||||
// contrary to Reader.readHeaders we cannot seek with the ReadCloser, so we
|
||||
// need to trust the filelength in the header
|
||||
|
||||
er := &errReader{Reader: sr.shp}
|
||||
// shp headers
|
||||
io.CopyN(ioutil.Discard, er, 24)
|
||||
var l int32
|
||||
binary.Read(er, binary.BigEndian, &l)
|
||||
sr.filelength = int64(l) * 2
|
||||
io.CopyN(ioutil.Discard, er, 4)
|
||||
binary.Read(er, binary.LittleEndian, &sr.geometryType)
|
||||
sr.bbox.MinX = readFloat64(er)
|
||||
sr.bbox.MinY = readFloat64(er)
|
||||
sr.bbox.MaxX = readFloat64(er)
|
||||
sr.bbox.MaxY = readFloat64(er)
|
||||
io.CopyN(ioutil.Discard, er, 32) // skip four float64: Zmin, Zmax, Mmin, Max
|
||||
if er.e != nil {
|
||||
sr.err = fmt.Errorf("Error when reading SHP header: %v", er.e)
|
||||
return
|
||||
}
|
||||
|
||||
// dbf header
|
||||
er = &errReader{Reader: sr.dbf}
|
||||
if sr.dbf == nil {
|
||||
return
|
||||
}
|
||||
io.CopyN(ioutil.Discard, er, 4)
|
||||
binary.Read(er, binary.LittleEndian, &sr.dbfNumRecords)
|
||||
binary.Read(er, binary.LittleEndian, &sr.dbfHeaderLength)
|
||||
binary.Read(er, binary.LittleEndian, &sr.dbfRecordLength)
|
||||
io.CopyN(ioutil.Discard, er, 20) // skip padding
|
||||
numFields := int(math.Floor(float64(sr.dbfHeaderLength-33) / 32.0))
|
||||
sr.dbfFields = make([]Field, numFields)
|
||||
binary.Read(er, binary.LittleEndian, &sr.dbfFields)
|
||||
buf := make([]byte, 1)
|
||||
er.Read(buf[:])
|
||||
if er.e != nil {
|
||||
sr.err = fmt.Errorf("Error when reading DBF header: %v", er.e)
|
||||
return
|
||||
}
|
||||
if buf[0] != 0x0d {
|
||||
sr.err = fmt.Errorf("Field descriptor array terminator not found")
|
||||
return
|
||||
}
|
||||
sr.dbfRow = make([]byte, sr.dbfRecordLength)
|
||||
}
|
||||
|
||||
// Next implements a method of interface SequentialReader for seqReader.
|
||||
func (sr *seqReader) Next() bool {
|
||||
if sr.err != nil {
|
||||
return false
|
||||
}
|
||||
var num, size int32
|
||||
var shapetype ShapeType
|
||||
|
||||
// read shape
|
||||
er := &errReader{Reader: sr.shp}
|
||||
binary.Read(er, binary.BigEndian, &num)
|
||||
binary.Read(er, binary.BigEndian, &size)
|
||||
binary.Read(er, binary.LittleEndian, &shapetype)
|
||||
|
||||
if er.e != nil {
|
||||
if er.e != io.EOF {
|
||||
sr.err = fmt.Errorf("Error when reading shapefile header: %v", er.e)
|
||||
} else {
|
||||
sr.err = io.EOF
|
||||
}
|
||||
return false
|
||||
}
|
||||
sr.num = num
|
||||
var err error
|
||||
sr.shape, err = newShape(shapetype)
|
||||
if err != nil {
|
||||
sr.err = fmt.Errorf("Error decoding shape type: %v", err)
|
||||
return false
|
||||
}
|
||||
sr.shape.read(er)
|
||||
switch {
|
||||
case er.e == io.EOF:
|
||||
// io.EOF means end-of-file was reached gracefully after all
|
||||
// shape-internal reads succeeded, so it's not a reason stop
|
||||
// iterating over all shapes.
|
||||
er.e = nil
|
||||
case er.e != nil:
|
||||
sr.err = fmt.Errorf("Error while reading next shape: %v", er.e)
|
||||
return false
|
||||
}
|
||||
skipBytes := int64(size)*2 + 8 - er.n
|
||||
_, ce := io.CopyN(ioutil.Discard, er, skipBytes)
|
||||
if er.e != nil {
|
||||
sr.err = er.e
|
||||
return false
|
||||
}
|
||||
if ce != nil {
|
||||
sr.err = fmt.Errorf("Error when discarding bytes on sequential read: %v", ce)
|
||||
return false
|
||||
}
|
||||
if _, err := io.ReadFull(sr.dbf, sr.dbfRow); err != nil {
|
||||
sr.err = fmt.Errorf("Error when reading DBF row: %v", err)
|
||||
return false
|
||||
}
|
||||
if sr.dbfRow[0] != 0x20 && sr.dbfRow[0] != 0x2a {
|
||||
sr.err = fmt.Errorf("Attribute row %d starts with incorrect deletion indicator", num)
|
||||
}
|
||||
return sr.err == nil
|
||||
}
|
||||
|
||||
// Shape implements a method of interface SequentialReader for seqReader.
|
||||
func (sr *seqReader) Shape() (int, Shape) {
|
||||
return int(sr.num) - 1, sr.shape
|
||||
}
|
||||
|
||||
// Attribute implements a method of interface SequentialReader for seqReader.
|
||||
func (sr *seqReader) Attribute(n int) string {
|
||||
if sr.err != nil {
|
||||
return ""
|
||||
}
|
||||
start := 1
|
||||
f := 0
|
||||
for ; f < n; f++ {
|
||||
start += int(sr.dbfFields[f].Size)
|
||||
}
|
||||
s := string(sr.dbfRow[start : start+int(sr.dbfFields[f].Size)])
|
||||
return strings.Trim(s, " ")
|
||||
}
|
||||
|
||||
// Err returns the first non-EOF error that was encountered.
|
||||
func (sr *seqReader) Err() error {
|
||||
if sr.err == io.EOF {
|
||||
return nil
|
||||
}
|
||||
return sr.err
|
||||
}
|
||||
|
||||
// Close closes the seqReader and free all the allocated resources.
|
||||
func (sr *seqReader) Close() error {
|
||||
if err := sr.shp.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := sr.dbf.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Fields returns a slice of the fields that are present in the DBF table.
|
||||
func (sr *seqReader) Fields() []Field {
|
||||
return sr.dbfFields
|
||||
}
|
||||
|
||||
// SequentialReaderFromExt returns a new SequentialReader that interprets shp
|
||||
// as a source of shapes whose attributes can be retrieved from dbf.
|
||||
func SequentialReaderFromExt(shp, dbf io.ReadCloser) SequentialReader {
|
||||
sr := &seqReader{shp: shp, dbf: dbf}
|
||||
sr.readHeaders()
|
||||
return sr
|
||||
}
|
43
api/v1/common/tool/shp/sequentialreader_test.go
Normal file
43
api/v1/common/tool/shp/sequentialreader_test.go
Normal file
@ -0,0 +1,43 @@
|
||||
package shp
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func openFile(name string, t *testing.T) *os.File {
|
||||
f, err := os.Open(name)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to open %s: %v", name, err)
|
||||
}
|
||||
return f
|
||||
}
|
||||
|
||||
func getShapesSequentially(prefix string, t *testing.T) (shapes []Shape) {
|
||||
shp := openFile(prefix+".shp", t)
|
||||
dbf := openFile(prefix+".dbf", t)
|
||||
|
||||
sr := SequentialReaderFromExt(shp, dbf)
|
||||
if err := sr.Err(); err != nil {
|
||||
t.Fatalf("Error when iterating over the shapefile header: %v", err)
|
||||
}
|
||||
for sr.Next() {
|
||||
_, shape := sr.Shape()
|
||||
shapes = append(shapes, shape)
|
||||
}
|
||||
if err := sr.Err(); err != nil {
|
||||
t.Errorf("Error when iterating over the shapes: %v", err)
|
||||
}
|
||||
|
||||
if err := sr.Close(); err != nil {
|
||||
t.Errorf("Could not close sequential reader: %v", err)
|
||||
}
|
||||
return shapes
|
||||
}
|
||||
|
||||
func TestSequentialReader(t *testing.T) {
|
||||
for prefix := range dataForReadTests {
|
||||
t.Logf("Testing sequential read for %s", prefix)
|
||||
testshapeIdentity(t, prefix, getShapesSequentially)
|
||||
}
|
||||
}
|
612
api/v1/common/tool/shp/shapefile.go
Normal file
612
api/v1/common/tool/shp/shapefile.go
Normal file
@ -0,0 +1,612 @@
|
||||
package shp
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"io"
|
||||
"strings"
|
||||
)
|
||||
|
||||
//go:generate stringer -type=ShapeType
|
||||
|
||||
// ShapeType is a identifier for the the type of shapes.
|
||||
type ShapeType int32
|
||||
|
||||
// These are the possible shape types.
|
||||
const (
|
||||
NULL ShapeType = 0
|
||||
POINT ShapeType = 1
|
||||
POLYLINE ShapeType = 3
|
||||
POLYGON ShapeType = 5
|
||||
MULTIPOINT ShapeType = 8
|
||||
POINTZ ShapeType = 11
|
||||
POLYLINEZ ShapeType = 13
|
||||
POLYGONZ ShapeType = 15
|
||||
MULTIPOINTZ ShapeType = 18
|
||||
POINTM ShapeType = 21
|
||||
POLYLINEM ShapeType = 23
|
||||
POLYGONM ShapeType = 25
|
||||
MULTIPOINTM ShapeType = 28
|
||||
MULTIPATCH ShapeType = 31
|
||||
)
|
||||
|
||||
// Box structure made up from four coordinates. This type
|
||||
// is used to represent bounding boxes
|
||||
type Box struct {
|
||||
MinX, MinY, MaxX, MaxY float64
|
||||
}
|
||||
|
||||
// Extend extends the box with coordinates from the provided
|
||||
// box. This method calls Box.ExtendWithPoint twice with
|
||||
// {MinX, MinY} and {MaxX, MaxY}
|
||||
func (b *Box) Extend(box Box) {
|
||||
b.ExtendWithPoint(Point{box.MinX, box.MinY})
|
||||
b.ExtendWithPoint(Point{box.MaxX, box.MaxY})
|
||||
}
|
||||
|
||||
// ExtendWithPoint extends box with coordinates from point
|
||||
// if they are outside the range of the current box.
|
||||
func (b *Box) ExtendWithPoint(p Point) {
|
||||
if p.X < b.MinX {
|
||||
b.MinX = p.X
|
||||
}
|
||||
if p.Y < b.MinY {
|
||||
b.MinY = p.Y
|
||||
}
|
||||
if p.X > b.MaxX {
|
||||
b.MaxX = p.X
|
||||
}
|
||||
if p.Y > b.MaxY {
|
||||
b.MaxY = p.Y
|
||||
}
|
||||
}
|
||||
|
||||
// BBoxFromPoints returns the bounding box calculated
|
||||
// from points.
|
||||
func BBoxFromPoints(points []Point) (box Box) {
|
||||
for k, p := range points {
|
||||
if k == 0 {
|
||||
box = Box{p.X, p.Y, p.X, p.Y}
|
||||
} else {
|
||||
box.ExtendWithPoint(p)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Shape interface
|
||||
type Shape interface {
|
||||
BBox() Box
|
||||
|
||||
read(io.Reader)
|
||||
write(io.Writer)
|
||||
}
|
||||
|
||||
// Null is an empty shape.
|
||||
type Null struct {
|
||||
}
|
||||
|
||||
// BBox Returns an empty BBox at the geometry origin.
|
||||
func (n Null) BBox() Box {
|
||||
return Box{0.0, 0.0, 0.0, 0.0}
|
||||
}
|
||||
|
||||
func (n *Null) read(file io.Reader) {
|
||||
binary.Read(file, binary.LittleEndian, n)
|
||||
}
|
||||
|
||||
func (n *Null) write(file io.Writer) {
|
||||
binary.Write(file, binary.LittleEndian, n)
|
||||
}
|
||||
|
||||
// Point is the shape that consists of single a geometry point.
|
||||
type Point struct {
|
||||
X, Y float64
|
||||
}
|
||||
|
||||
// BBox returns the bounding box of the Point feature, i.e. an empty area at
|
||||
// the point location itself.
|
||||
func (p Point) BBox() Box {
|
||||
return Box{p.X, p.Y, p.X, p.Y}
|
||||
}
|
||||
|
||||
func (p *Point) read(file io.Reader) {
|
||||
binary.Read(file, binary.LittleEndian, p)
|
||||
}
|
||||
|
||||
func (p *Point) write(file io.Writer) {
|
||||
binary.Write(file, binary.LittleEndian, p)
|
||||
}
|
||||
|
||||
func flatten(points [][]Point) []Point {
|
||||
n, i := 0, 0
|
||||
for _, v := range points {
|
||||
n += len(v)
|
||||
}
|
||||
r := make([]Point, n)
|
||||
for _, v := range points {
|
||||
for _, p := range v {
|
||||
r[i] = p
|
||||
i++
|
||||
}
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
// PolyLine is a shape type that consists of an ordered set of vertices that
|
||||
// consists of one or more parts. A part is a connected sequence of two ore
|
||||
// more points. Parts may or may not be connected to another and may or may not
|
||||
// intersect each other.
|
||||
type PolyLine struct {
|
||||
Box
|
||||
NumParts int32
|
||||
NumPoints int32
|
||||
Parts []int32
|
||||
Points []Point
|
||||
}
|
||||
|
||||
// NewPolyLine returns a pointer a new PolyLine created
|
||||
// with the provided points. The inner slice should be
|
||||
// the points that the parent part consists of.
|
||||
func NewPolyLine(parts [][]Point) *PolyLine {
|
||||
points := flatten(parts)
|
||||
|
||||
p := &PolyLine{}
|
||||
p.NumParts = int32(len(parts))
|
||||
p.NumPoints = int32(len(points))
|
||||
p.Parts = make([]int32, len(parts))
|
||||
var marker int32
|
||||
for i, part := range parts {
|
||||
p.Parts[i] = marker
|
||||
marker += int32(len(part))
|
||||
}
|
||||
p.Points = points
|
||||
p.Box = p.BBox()
|
||||
|
||||
return p
|
||||
}
|
||||
|
||||
// BBox returns the bounding box of the PolyLine feature
|
||||
func (p PolyLine) BBox() Box {
|
||||
return BBoxFromPoints(p.Points)
|
||||
}
|
||||
|
||||
func (p *PolyLine) read(file io.Reader) {
|
||||
binary.Read(file, binary.LittleEndian, &p.Box)
|
||||
binary.Read(file, binary.LittleEndian, &p.NumParts)
|
||||
binary.Read(file, binary.LittleEndian, &p.NumPoints)
|
||||
p.Parts = make([]int32, p.NumParts)
|
||||
p.Points = make([]Point, p.NumPoints)
|
||||
binary.Read(file, binary.LittleEndian, &p.Parts)
|
||||
binary.Read(file, binary.LittleEndian, &p.Points)
|
||||
}
|
||||
|
||||
func (p *PolyLine) write(file io.Writer) {
|
||||
binary.Write(file, binary.LittleEndian, p.Box)
|
||||
binary.Write(file, binary.LittleEndian, p.NumParts)
|
||||
binary.Write(file, binary.LittleEndian, p.NumPoints)
|
||||
binary.Write(file, binary.LittleEndian, p.Parts)
|
||||
binary.Write(file, binary.LittleEndian, p.Points)
|
||||
}
|
||||
|
||||
// Polygon is identical to the PolyLine struct. However the parts must form
|
||||
// rings that may not intersect.
|
||||
type Polygon PolyLine
|
||||
|
||||
// BBox returns the bounding box of the Polygon feature
|
||||
func (p Polygon) BBox() Box {
|
||||
return BBoxFromPoints(p.Points)
|
||||
}
|
||||
|
||||
func (p *Polygon) read(file io.Reader) {
|
||||
binary.Read(file, binary.LittleEndian, &p.Box)
|
||||
binary.Read(file, binary.LittleEndian, &p.NumParts)
|
||||
binary.Read(file, binary.LittleEndian, &p.NumPoints)
|
||||
p.Parts = make([]int32, p.NumParts)
|
||||
p.Points = make([]Point, p.NumPoints)
|
||||
binary.Read(file, binary.LittleEndian, &p.Parts)
|
||||
binary.Read(file, binary.LittleEndian, &p.Points)
|
||||
}
|
||||
|
||||
func (p *Polygon) write(file io.Writer) {
|
||||
binary.Write(file, binary.LittleEndian, p.Box)
|
||||
binary.Write(file, binary.LittleEndian, p.NumParts)
|
||||
binary.Write(file, binary.LittleEndian, p.NumPoints)
|
||||
binary.Write(file, binary.LittleEndian, p.Parts)
|
||||
binary.Write(file, binary.LittleEndian, p.Points)
|
||||
}
|
||||
|
||||
// MultiPoint is the shape that consists of multiple points.
|
||||
type MultiPoint struct {
|
||||
Box Box
|
||||
NumPoints int32
|
||||
Points []Point
|
||||
}
|
||||
|
||||
// BBox returns the bounding box of the MultiPoint feature
|
||||
func (p MultiPoint) BBox() Box {
|
||||
return BBoxFromPoints(p.Points)
|
||||
}
|
||||
|
||||
func (p *MultiPoint) read(file io.Reader) {
|
||||
binary.Read(file, binary.LittleEndian, &p.Box)
|
||||
binary.Read(file, binary.LittleEndian, &p.NumPoints)
|
||||
p.Points = make([]Point, p.NumPoints)
|
||||
binary.Read(file, binary.LittleEndian, &p.Points)
|
||||
}
|
||||
|
||||
func (p *MultiPoint) write(file io.Writer) {
|
||||
binary.Write(file, binary.LittleEndian, p.Box)
|
||||
binary.Write(file, binary.LittleEndian, p.NumPoints)
|
||||
binary.Write(file, binary.LittleEndian, p.Points)
|
||||
}
|
||||
|
||||
// PointZ is a triplet of double precision coordinates plus a measure.
|
||||
type PointZ struct {
|
||||
X float64
|
||||
Y float64
|
||||
Z float64
|
||||
M float64
|
||||
}
|
||||
|
||||
// BBox eturns the bounding box of the PointZ feature which is an zero-sized area
|
||||
// at the X and Y coordinates of the feature.
|
||||
func (p PointZ) BBox() Box {
|
||||
return Box{p.X, p.Y, p.X, p.Y}
|
||||
}
|
||||
|
||||
func (p *PointZ) read(file io.Reader) {
|
||||
binary.Read(file, binary.LittleEndian, p)
|
||||
}
|
||||
|
||||
func (p *PointZ) write(file io.Writer) {
|
||||
binary.Write(file, binary.LittleEndian, p)
|
||||
}
|
||||
|
||||
// PolyLineZ is a shape which consists of one or more parts. A part is a
|
||||
// connected sequence of two or more points. Parts may or may not be connected
|
||||
// and may or may not intersect one another.
|
||||
type PolyLineZ struct {
|
||||
Box Box
|
||||
NumParts int32
|
||||
NumPoints int32
|
||||
Parts []int32
|
||||
Points []Point
|
||||
ZRange [2]float64
|
||||
ZArray []float64
|
||||
MRange [2]float64
|
||||
MArray []float64
|
||||
}
|
||||
|
||||
// BBox eturns the bounding box of the PolyLineZ feature.
|
||||
func (p PolyLineZ) BBox() Box {
|
||||
return BBoxFromPoints(p.Points)
|
||||
}
|
||||
|
||||
func (p *PolyLineZ) read(file io.Reader) {
|
||||
binary.Read(file, binary.LittleEndian, &p.Box)
|
||||
binary.Read(file, binary.LittleEndian, &p.NumParts)
|
||||
binary.Read(file, binary.LittleEndian, &p.NumPoints)
|
||||
p.Parts = make([]int32, p.NumParts)
|
||||
p.Points = make([]Point, p.NumPoints)
|
||||
p.ZArray = make([]float64, p.NumPoints)
|
||||
p.MArray = make([]float64, p.NumPoints)
|
||||
binary.Read(file, binary.LittleEndian, &p.Parts)
|
||||
binary.Read(file, binary.LittleEndian, &p.Points)
|
||||
binary.Read(file, binary.LittleEndian, &p.ZRange)
|
||||
binary.Read(file, binary.LittleEndian, &p.ZArray)
|
||||
binary.Read(file, binary.LittleEndian, &p.MRange)
|
||||
binary.Read(file, binary.LittleEndian, &p.MArray)
|
||||
}
|
||||
|
||||
func (p *PolyLineZ) write(file io.Writer) {
|
||||
binary.Write(file, binary.LittleEndian, p.Box)
|
||||
binary.Write(file, binary.LittleEndian, p.NumParts)
|
||||
binary.Write(file, binary.LittleEndian, p.NumPoints)
|
||||
binary.Write(file, binary.LittleEndian, p.Parts)
|
||||
binary.Write(file, binary.LittleEndian, p.Points)
|
||||
binary.Write(file, binary.LittleEndian, p.ZRange)
|
||||
binary.Write(file, binary.LittleEndian, p.ZArray)
|
||||
binary.Write(file, binary.LittleEndian, p.MRange)
|
||||
binary.Write(file, binary.LittleEndian, p.MArray)
|
||||
}
|
||||
|
||||
// PolygonZ structure is identical to the PolyLineZ structure.
|
||||
type PolygonZ PolyLineZ
|
||||
|
||||
// BBox returns the bounding box of the PolygonZ feature
|
||||
func (p PolygonZ) BBox() Box {
|
||||
return BBoxFromPoints(p.Points)
|
||||
}
|
||||
|
||||
func (p *PolygonZ) read(file io.Reader) {
|
||||
binary.Read(file, binary.LittleEndian, &p.Box)
|
||||
binary.Read(file, binary.LittleEndian, &p.NumParts)
|
||||
binary.Read(file, binary.LittleEndian, &p.NumPoints)
|
||||
p.Parts = make([]int32, p.NumParts)
|
||||
p.Points = make([]Point, p.NumPoints)
|
||||
p.ZArray = make([]float64, p.NumPoints)
|
||||
p.MArray = make([]float64, p.NumPoints)
|
||||
binary.Read(file, binary.LittleEndian, &p.Parts)
|
||||
binary.Read(file, binary.LittleEndian, &p.Points)
|
||||
binary.Read(file, binary.LittleEndian, &p.ZRange)
|
||||
binary.Read(file, binary.LittleEndian, &p.ZArray)
|
||||
binary.Read(file, binary.LittleEndian, &p.MRange)
|
||||
binary.Read(file, binary.LittleEndian, &p.MArray)
|
||||
}
|
||||
|
||||
func (p *PolygonZ) write(file io.Writer) {
|
||||
binary.Write(file, binary.LittleEndian, p.Box)
|
||||
binary.Write(file, binary.LittleEndian, p.NumParts)
|
||||
binary.Write(file, binary.LittleEndian, p.NumPoints)
|
||||
binary.Write(file, binary.LittleEndian, p.Parts)
|
||||
binary.Write(file, binary.LittleEndian, p.Points)
|
||||
binary.Write(file, binary.LittleEndian, p.ZRange)
|
||||
binary.Write(file, binary.LittleEndian, p.ZArray)
|
||||
binary.Write(file, binary.LittleEndian, p.MRange)
|
||||
binary.Write(file, binary.LittleEndian, p.MArray)
|
||||
}
|
||||
|
||||
// MultiPointZ consists of one ore more PointZ.
|
||||
type MultiPointZ struct {
|
||||
Box Box
|
||||
NumPoints int32
|
||||
Points []Point
|
||||
ZRange [2]float64
|
||||
ZArray []float64
|
||||
MRange [2]float64
|
||||
MArray []float64
|
||||
}
|
||||
|
||||
// BBox eturns the bounding box of the MultiPointZ feature.
|
||||
func (p MultiPointZ) BBox() Box {
|
||||
return BBoxFromPoints(p.Points)
|
||||
}
|
||||
|
||||
func (p *MultiPointZ) read(file io.Reader) {
|
||||
binary.Read(file, binary.LittleEndian, &p.Box)
|
||||
binary.Read(file, binary.LittleEndian, &p.NumPoints)
|
||||
p.Points = make([]Point, p.NumPoints)
|
||||
p.ZArray = make([]float64, p.NumPoints)
|
||||
p.MArray = make([]float64, p.NumPoints)
|
||||
binary.Read(file, binary.LittleEndian, &p.Points)
|
||||
binary.Read(file, binary.LittleEndian, &p.ZRange)
|
||||
binary.Read(file, binary.LittleEndian, &p.ZArray)
|
||||
binary.Read(file, binary.LittleEndian, &p.MRange)
|
||||
binary.Read(file, binary.LittleEndian, &p.MArray)
|
||||
}
|
||||
|
||||
func (p *MultiPointZ) write(file io.Writer) {
|
||||
binary.Write(file, binary.LittleEndian, p.Box)
|
||||
binary.Write(file, binary.LittleEndian, p.NumPoints)
|
||||
binary.Write(file, binary.LittleEndian, p.Points)
|
||||
binary.Write(file, binary.LittleEndian, p.ZRange)
|
||||
binary.Write(file, binary.LittleEndian, p.ZArray)
|
||||
binary.Write(file, binary.LittleEndian, p.MRange)
|
||||
binary.Write(file, binary.LittleEndian, p.MArray)
|
||||
}
|
||||
|
||||
// PointM is a point with a measure.
|
||||
type PointM struct {
|
||||
X float64
|
||||
Y float64
|
||||
M float64
|
||||
}
|
||||
|
||||
// BBox returns the bounding box of the PointM feature which is a zero-sized
|
||||
// area at the X- and Y-coordinates of the point.
|
||||
func (p PointM) BBox() Box {
|
||||
return Box{p.X, p.Y, p.X, p.Y}
|
||||
}
|
||||
|
||||
func (p *PointM) read(file io.Reader) {
|
||||
binary.Read(file, binary.LittleEndian, p)
|
||||
}
|
||||
|
||||
func (p *PointM) write(file io.Writer) {
|
||||
binary.Write(file, binary.LittleEndian, p)
|
||||
}
|
||||
|
||||
// PolyLineM is the polyline in which each point also has a measure.
|
||||
type PolyLineM struct {
|
||||
Box Box
|
||||
NumParts int32
|
||||
NumPoints int32
|
||||
Parts []int32
|
||||
Points []Point
|
||||
MRange [2]float64
|
||||
MArray []float64
|
||||
}
|
||||
|
||||
// BBox returns the bounding box of the PolyLineM feature.
|
||||
func (p PolyLineM) BBox() Box {
|
||||
return BBoxFromPoints(p.Points)
|
||||
}
|
||||
|
||||
func (p *PolyLineM) read(file io.Reader) {
|
||||
binary.Read(file, binary.LittleEndian, &p.Box)
|
||||
binary.Read(file, binary.LittleEndian, &p.NumParts)
|
||||
binary.Read(file, binary.LittleEndian, &p.NumPoints)
|
||||
p.Parts = make([]int32, p.NumParts)
|
||||
p.Points = make([]Point, p.NumPoints)
|
||||
p.MArray = make([]float64, p.NumPoints)
|
||||
binary.Read(file, binary.LittleEndian, &p.Parts)
|
||||
binary.Read(file, binary.LittleEndian, &p.Points)
|
||||
binary.Read(file, binary.LittleEndian, &p.MRange)
|
||||
binary.Read(file, binary.LittleEndian, &p.MArray)
|
||||
}
|
||||
|
||||
func (p *PolyLineM) write(file io.Writer) {
|
||||
binary.Write(file, binary.LittleEndian, p.Box)
|
||||
binary.Write(file, binary.LittleEndian, p.NumParts)
|
||||
binary.Write(file, binary.LittleEndian, p.NumPoints)
|
||||
binary.Write(file, binary.LittleEndian, p.Parts)
|
||||
binary.Write(file, binary.LittleEndian, p.Points)
|
||||
binary.Write(file, binary.LittleEndian, p.MRange)
|
||||
binary.Write(file, binary.LittleEndian, p.MArray)
|
||||
}
|
||||
|
||||
// PolygonM structure is identical to the PolyLineZ structure.
|
||||
type PolygonM PolyLineZ
|
||||
|
||||
// BBox returns the bounding box of the PolygonM feature.
|
||||
func (p PolygonM) BBox() Box {
|
||||
return BBoxFromPoints(p.Points)
|
||||
}
|
||||
|
||||
func (p *PolygonM) read(file io.Reader) {
|
||||
binary.Read(file, binary.LittleEndian, &p.Box)
|
||||
binary.Read(file, binary.LittleEndian, &p.NumParts)
|
||||
binary.Read(file, binary.LittleEndian, &p.NumPoints)
|
||||
p.Parts = make([]int32, p.NumParts)
|
||||
p.Points = make([]Point, p.NumPoints)
|
||||
p.MArray = make([]float64, p.NumPoints)
|
||||
binary.Read(file, binary.LittleEndian, &p.Parts)
|
||||
binary.Read(file, binary.LittleEndian, &p.Points)
|
||||
binary.Read(file, binary.LittleEndian, &p.MRange)
|
||||
binary.Read(file, binary.LittleEndian, &p.MArray)
|
||||
}
|
||||
|
||||
func (p *PolygonM) write(file io.Writer) {
|
||||
binary.Write(file, binary.LittleEndian, p.Box)
|
||||
binary.Write(file, binary.LittleEndian, p.NumParts)
|
||||
binary.Write(file, binary.LittleEndian, p.NumPoints)
|
||||
binary.Write(file, binary.LittleEndian, p.Parts)
|
||||
binary.Write(file, binary.LittleEndian, p.Points)
|
||||
binary.Write(file, binary.LittleEndian, p.MRange)
|
||||
binary.Write(file, binary.LittleEndian, p.MArray)
|
||||
}
|
||||
|
||||
// MultiPointM is the collection of multiple points with measures.
|
||||
type MultiPointM struct {
|
||||
Box Box
|
||||
NumPoints int32
|
||||
Points []Point
|
||||
MRange [2]float64
|
||||
MArray []float64
|
||||
}
|
||||
|
||||
// BBox eturns the bounding box of the MultiPointM feature
|
||||
func (p MultiPointM) BBox() Box {
|
||||
return BBoxFromPoints(p.Points)
|
||||
}
|
||||
|
||||
func (p *MultiPointM) read(file io.Reader) {
|
||||
binary.Read(file, binary.LittleEndian, &p.Box)
|
||||
binary.Read(file, binary.LittleEndian, &p.NumPoints)
|
||||
p.Points = make([]Point, p.NumPoints)
|
||||
p.MArray = make([]float64, p.NumPoints)
|
||||
binary.Read(file, binary.LittleEndian, &p.Points)
|
||||
binary.Read(file, binary.LittleEndian, &p.MRange)
|
||||
binary.Read(file, binary.LittleEndian, &p.MArray)
|
||||
}
|
||||
|
||||
func (p *MultiPointM) write(file io.Writer) {
|
||||
binary.Write(file, binary.LittleEndian, p.Box)
|
||||
binary.Write(file, binary.LittleEndian, p.NumPoints)
|
||||
binary.Write(file, binary.LittleEndian, p.Points)
|
||||
binary.Write(file, binary.LittleEndian, p.MRange)
|
||||
binary.Write(file, binary.LittleEndian, p.MArray)
|
||||
}
|
||||
|
||||
// MultiPatch consists of a number of surfaces patches. Each surface path
|
||||
// descries a surface. The surface patches of a MultiPatch are referred to as
|
||||
// its parts, and the type of part controls how the order of vertices of an
|
||||
// MultiPatch part is interpreted.
|
||||
type MultiPatch struct {
|
||||
Box Box
|
||||
NumParts int32
|
||||
NumPoints int32
|
||||
Parts []int32
|
||||
PartTypes []int32
|
||||
Points []Point
|
||||
ZRange [2]float64
|
||||
ZArray []float64
|
||||
MRange [2]float64
|
||||
MArray []float64
|
||||
}
|
||||
|
||||
// BBox returns the bounding box of the MultiPatch feature
|
||||
func (p MultiPatch) BBox() Box {
|
||||
return BBoxFromPoints(p.Points)
|
||||
}
|
||||
|
||||
func (p *MultiPatch) read(file io.Reader) {
|
||||
binary.Read(file, binary.LittleEndian, &p.Box)
|
||||
binary.Read(file, binary.LittleEndian, &p.NumParts)
|
||||
binary.Read(file, binary.LittleEndian, &p.NumPoints)
|
||||
p.Parts = make([]int32, p.NumParts)
|
||||
p.PartTypes = make([]int32, p.NumParts)
|
||||
p.Points = make([]Point, p.NumPoints)
|
||||
p.ZArray = make([]float64, p.NumPoints)
|
||||
p.MArray = make([]float64, p.NumPoints)
|
||||
binary.Read(file, binary.LittleEndian, &p.Parts)
|
||||
binary.Read(file, binary.LittleEndian, &p.PartTypes)
|
||||
binary.Read(file, binary.LittleEndian, &p.Points)
|
||||
binary.Read(file, binary.LittleEndian, &p.ZRange)
|
||||
binary.Read(file, binary.LittleEndian, &p.ZArray)
|
||||
binary.Read(file, binary.LittleEndian, &p.MRange)
|
||||
binary.Read(file, binary.LittleEndian, &p.MArray)
|
||||
}
|
||||
|
||||
func (p *MultiPatch) write(file io.Writer) {
|
||||
binary.Write(file, binary.LittleEndian, p.Box)
|
||||
binary.Write(file, binary.LittleEndian, p.NumParts)
|
||||
binary.Write(file, binary.LittleEndian, p.NumPoints)
|
||||
binary.Write(file, binary.LittleEndian, p.Parts)
|
||||
binary.Write(file, binary.LittleEndian, p.PartTypes)
|
||||
binary.Write(file, binary.LittleEndian, p.Points)
|
||||
binary.Write(file, binary.LittleEndian, p.ZRange)
|
||||
binary.Write(file, binary.LittleEndian, p.ZArray)
|
||||
binary.Write(file, binary.LittleEndian, p.MRange)
|
||||
binary.Write(file, binary.LittleEndian, p.MArray)
|
||||
}
|
||||
|
||||
// Field representation of a field object in the DBF file
|
||||
type Field struct {
|
||||
Name [11]byte
|
||||
Fieldtype byte
|
||||
Addr [4]byte // not used
|
||||
Size uint8
|
||||
Precision uint8
|
||||
Padding [14]byte
|
||||
}
|
||||
|
||||
// Returns a string representation of the Field. Currently
|
||||
// this only returns field name.
|
||||
func (f Field) String() string {
|
||||
return strings.TrimRight(string(f.Name[:]), "\x00")
|
||||
}
|
||||
|
||||
// StringField returns a Field that can be used in SetFields to initialize the
|
||||
// DBF file.
|
||||
func StringField(name string, length uint8) Field {
|
||||
// TODO: Error checking
|
||||
field := Field{Fieldtype: 'C', Size: length}
|
||||
copy(field.Name[:], []byte(name))
|
||||
return field
|
||||
}
|
||||
|
||||
// NumberField returns a Field that can be used in SetFields to initialize the
|
||||
// DBF file.
|
||||
func NumberField(name string, length uint8) Field {
|
||||
field := Field{Fieldtype: 'N', Size: length}
|
||||
copy(field.Name[:], []byte(name))
|
||||
return field
|
||||
}
|
||||
|
||||
// FloatField returns a Field that can be used in SetFields to initialize the
|
||||
// DBF file. Used to store floating points with precision in the DBF.
|
||||
func FloatField(name string, length uint8, precision uint8) Field {
|
||||
field := Field{Fieldtype: 'F', Size: length, Precision: precision}
|
||||
copy(field.Name[:], []byte(name))
|
||||
return field
|
||||
}
|
||||
|
||||
// DateField feturns a Field that can be used in SetFields to initialize the
|
||||
// DBF file. Used to store Date strings formatted as YYYYMMDD. Data wise this
|
||||
// is the same as a StringField with length 8.
|
||||
func DateField(name string) Field {
|
||||
field := Field{Fieldtype: 'D', Size: 8}
|
||||
copy(field.Name[:], []byte(name))
|
||||
return field
|
||||
}
|
22
api/v1/common/tool/shp/shapefile_test.go
Normal file
22
api/v1/common/tool/shp/shapefile_test.go
Normal file
@ -0,0 +1,22 @@
|
||||
package shp
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestBoxExtend(t *testing.T) {
|
||||
a := Box{-124.763068, 45.543541, -116.915989, 49.002494}
|
||||
b := Box{-92.888114, 42.49192, -86.805415, 47.080621}
|
||||
a.Extend(b)
|
||||
c := Box{-124.763068, 42.49192, -86.805415, 49.002494}
|
||||
if a.MinX != c.MinX {
|
||||
t.Errorf("a.MinX = %v, want %v", a.MinX, c.MinX)
|
||||
}
|
||||
if a.MinY != c.MinY {
|
||||
t.Errorf("a.MinY = %v, want %v", a.MinY, c.MinY)
|
||||
}
|
||||
if a.MaxX != c.MaxX {
|
||||
t.Errorf("a.MaxX = %v, want %v", a.MaxX, c.MaxX)
|
||||
}
|
||||
if a.MaxY != c.MaxY {
|
||||
t.Errorf("a.MaxY = %v, want %v", a.MaxY, c.MaxY)
|
||||
}
|
||||
}
|
31
api/v1/common/tool/shp/shapetype_string.go
Normal file
31
api/v1/common/tool/shp/shapetype_string.go
Normal file
@ -0,0 +1,31 @@
|
||||
// Code generated by "stringer -type=ShapeType"; DO NOT EDIT.
|
||||
|
||||
package shp
|
||||
|
||||
import "strconv"
|
||||
|
||||
const _ShapeType_name = "NULLPOINTPOLYLINEPOLYGONMULTIPOINTPOINTZPOLYLINEZPOLYGONZMULTIPOINTZPOINTMPOLYLINEMPOLYGONMMULTIPOINTMMULTIPATCH"
|
||||
|
||||
var _ShapeType_map = map[ShapeType]string{
|
||||
0: _ShapeType_name[0:4],
|
||||
1: _ShapeType_name[4:9],
|
||||
3: _ShapeType_name[9:17],
|
||||
5: _ShapeType_name[17:24],
|
||||
8: _ShapeType_name[24:34],
|
||||
11: _ShapeType_name[34:40],
|
||||
13: _ShapeType_name[40:49],
|
||||
15: _ShapeType_name[49:57],
|
||||
18: _ShapeType_name[57:68],
|
||||
21: _ShapeType_name[68:74],
|
||||
23: _ShapeType_name[74:83],
|
||||
25: _ShapeType_name[83:91],
|
||||
28: _ShapeType_name[91:102],
|
||||
31: _ShapeType_name[102:112],
|
||||
}
|
||||
|
||||
func (i ShapeType) String() string {
|
||||
if str, ok := _ShapeType_map[i]; ok {
|
||||
return str
|
||||
}
|
||||
return "ShapeType(" + strconv.FormatInt(int64(i), 10) + ")"
|
||||
}
|
BIN
api/v1/common/tool/shp/test_files/multipatch.dbf
Normal file
BIN
api/v1/common/tool/shp/test_files/multipatch.dbf
Normal file
Binary file not shown.
BIN
api/v1/common/tool/shp/test_files/multipatch.shp
Normal file
BIN
api/v1/common/tool/shp/test_files/multipatch.shp
Normal file
Binary file not shown.
BIN
api/v1/common/tool/shp/test_files/multipatch.shx
Normal file
BIN
api/v1/common/tool/shp/test_files/multipatch.shx
Normal file
Binary file not shown.
BIN
api/v1/common/tool/shp/test_files/multipoint.dbf
Normal file
BIN
api/v1/common/tool/shp/test_files/multipoint.dbf
Normal file
Binary file not shown.
BIN
api/v1/common/tool/shp/test_files/multipoint.shp
Normal file
BIN
api/v1/common/tool/shp/test_files/multipoint.shp
Normal file
Binary file not shown.
BIN
api/v1/common/tool/shp/test_files/multipoint.shx
Normal file
BIN
api/v1/common/tool/shp/test_files/multipoint.shx
Normal file
Binary file not shown.
BIN
api/v1/common/tool/shp/test_files/multipointm.dbf
Normal file
BIN
api/v1/common/tool/shp/test_files/multipointm.dbf
Normal file
Binary file not shown.
BIN
api/v1/common/tool/shp/test_files/multipointm.shp
Normal file
BIN
api/v1/common/tool/shp/test_files/multipointm.shp
Normal file
Binary file not shown.
BIN
api/v1/common/tool/shp/test_files/multipointm.shx
Normal file
BIN
api/v1/common/tool/shp/test_files/multipointm.shx
Normal file
Binary file not shown.
BIN
api/v1/common/tool/shp/test_files/multipointz.dbf
Normal file
BIN
api/v1/common/tool/shp/test_files/multipointz.dbf
Normal file
Binary file not shown.
BIN
api/v1/common/tool/shp/test_files/multipointz.shp
Normal file
BIN
api/v1/common/tool/shp/test_files/multipointz.shp
Normal file
Binary file not shown.
BIN
api/v1/common/tool/shp/test_files/multipointz.shx
Normal file
BIN
api/v1/common/tool/shp/test_files/multipointz.shx
Normal file
Binary file not shown.
BIN
api/v1/common/tool/shp/test_files/point.dbf
Normal file
BIN
api/v1/common/tool/shp/test_files/point.dbf
Normal file
Binary file not shown.
BIN
api/v1/common/tool/shp/test_files/point.shp
Normal file
BIN
api/v1/common/tool/shp/test_files/point.shp
Normal file
Binary file not shown.
BIN
api/v1/common/tool/shp/test_files/point.shx
Normal file
BIN
api/v1/common/tool/shp/test_files/point.shx
Normal file
Binary file not shown.
BIN
api/v1/common/tool/shp/test_files/pointm.dbf
Normal file
BIN
api/v1/common/tool/shp/test_files/pointm.dbf
Normal file
Binary file not shown.
BIN
api/v1/common/tool/shp/test_files/pointm.shp
Normal file
BIN
api/v1/common/tool/shp/test_files/pointm.shp
Normal file
Binary file not shown.
BIN
api/v1/common/tool/shp/test_files/pointm.shx
Normal file
BIN
api/v1/common/tool/shp/test_files/pointm.shx
Normal file
Binary file not shown.
BIN
api/v1/common/tool/shp/test_files/pointz.dbf
Normal file
BIN
api/v1/common/tool/shp/test_files/pointz.dbf
Normal file
Binary file not shown.
BIN
api/v1/common/tool/shp/test_files/pointz.shp
Normal file
BIN
api/v1/common/tool/shp/test_files/pointz.shp
Normal file
Binary file not shown.
BIN
api/v1/common/tool/shp/test_files/pointz.shx
Normal file
BIN
api/v1/common/tool/shp/test_files/pointz.shx
Normal file
Binary file not shown.
BIN
api/v1/common/tool/shp/test_files/polygon.dbf
Normal file
BIN
api/v1/common/tool/shp/test_files/polygon.dbf
Normal file
Binary file not shown.
BIN
api/v1/common/tool/shp/test_files/polygon.shp
Normal file
BIN
api/v1/common/tool/shp/test_files/polygon.shp
Normal file
Binary file not shown.
BIN
api/v1/common/tool/shp/test_files/polygon.shx
Normal file
BIN
api/v1/common/tool/shp/test_files/polygon.shx
Normal file
Binary file not shown.
BIN
api/v1/common/tool/shp/test_files/polygonm.dbf
Normal file
BIN
api/v1/common/tool/shp/test_files/polygonm.dbf
Normal file
Binary file not shown.
BIN
api/v1/common/tool/shp/test_files/polygonm.shp
Normal file
BIN
api/v1/common/tool/shp/test_files/polygonm.shp
Normal file
Binary file not shown.
BIN
api/v1/common/tool/shp/test_files/polygonm.shx
Normal file
BIN
api/v1/common/tool/shp/test_files/polygonm.shx
Normal file
Binary file not shown.
BIN
api/v1/common/tool/shp/test_files/polygonz.dbf
Normal file
BIN
api/v1/common/tool/shp/test_files/polygonz.dbf
Normal file
Binary file not shown.
BIN
api/v1/common/tool/shp/test_files/polygonz.shp
Normal file
BIN
api/v1/common/tool/shp/test_files/polygonz.shp
Normal file
Binary file not shown.
BIN
api/v1/common/tool/shp/test_files/polygonz.shx
Normal file
BIN
api/v1/common/tool/shp/test_files/polygonz.shx
Normal file
Binary file not shown.
BIN
api/v1/common/tool/shp/test_files/polyline.dbf
Normal file
BIN
api/v1/common/tool/shp/test_files/polyline.dbf
Normal file
Binary file not shown.
BIN
api/v1/common/tool/shp/test_files/polyline.shp
Normal file
BIN
api/v1/common/tool/shp/test_files/polyline.shp
Normal file
Binary file not shown.
BIN
api/v1/common/tool/shp/test_files/polyline.shx
Normal file
BIN
api/v1/common/tool/shp/test_files/polyline.shx
Normal file
Binary file not shown.
BIN
api/v1/common/tool/shp/test_files/polylinem.dbf
Normal file
BIN
api/v1/common/tool/shp/test_files/polylinem.dbf
Normal file
Binary file not shown.
BIN
api/v1/common/tool/shp/test_files/polylinem.shp
Normal file
BIN
api/v1/common/tool/shp/test_files/polylinem.shp
Normal file
Binary file not shown.
BIN
api/v1/common/tool/shp/test_files/polylinem.shx
Normal file
BIN
api/v1/common/tool/shp/test_files/polylinem.shx
Normal file
Binary file not shown.
BIN
api/v1/common/tool/shp/test_files/polylinez.dbf
Normal file
BIN
api/v1/common/tool/shp/test_files/polylinez.dbf
Normal file
Binary file not shown.
BIN
api/v1/common/tool/shp/test_files/polylinez.shp
Normal file
BIN
api/v1/common/tool/shp/test_files/polylinez.shp
Normal file
Binary file not shown.
BIN
api/v1/common/tool/shp/test_files/polylinez.shx
Normal file
BIN
api/v1/common/tool/shp/test_files/polylinez.shx
Normal file
Binary file not shown.
345
api/v1/common/tool/shp/writer.go
Normal file
345
api/v1/common/tool/shp/writer.go
Normal file
@ -0,0 +1,345 @@
|
||||
package shp
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"math"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Writer is the type that is used to write a new shapefile.
|
||||
type Writer struct {
|
||||
filename string
|
||||
shp writeSeekCloser
|
||||
shx writeSeekCloser
|
||||
GeometryType ShapeType
|
||||
num int32
|
||||
bbox Box
|
||||
|
||||
dbf writeSeekCloser
|
||||
dbfFields []Field
|
||||
dbfHeaderLength int16
|
||||
dbfRecordLength int16
|
||||
}
|
||||
|
||||
type writeSeekCloser interface {
|
||||
io.Writer
|
||||
io.Seeker
|
||||
io.Closer
|
||||
}
|
||||
|
||||
// Create returns a point to new Writer and the first error that was
|
||||
// encountered. In case an error occurred the returned Writer point will be nil
|
||||
// This also creates a corresponding SHX file. It is important to use Close()
|
||||
// when done because that method writes all the headers for each file (SHP, SHX
|
||||
// and DBF).
|
||||
// If filename does not end on ".shp" already, it will be treated as the basename
|
||||
// for the file and the ".shp" extension will be appended to that name.
|
||||
func Create(filename string, t ShapeType) (*Writer, error) {
|
||||
if strings.HasSuffix(strings.ToLower(filename), ".shp") {
|
||||
filename = filename[0 : len(filename)-4]
|
||||
}
|
||||
shp, err := os.Create(filename + ".shp")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
shx, err := os.Create(filename + ".shx")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
shp.Seek(100, io.SeekStart)
|
||||
shx.Seek(100, io.SeekStart)
|
||||
w := &Writer{
|
||||
filename: filename,
|
||||
shp: shp,
|
||||
shx: shx,
|
||||
GeometryType: t,
|
||||
}
|
||||
return w, nil
|
||||
}
|
||||
|
||||
// Append returns a Writer pointer that will append to the given shapefile and
|
||||
// the first error that was encounted during creation of that Writer. The
|
||||
// shapefile must have a valid index file.
|
||||
func Append(filename string) (*Writer, error) {
|
||||
shp, err := os.OpenFile(filename, os.O_RDWR, 0666)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ext := filepath.Ext(filename)
|
||||
basename := filename[:len(filename)-len(ext)]
|
||||
w := &Writer{
|
||||
filename: basename,
|
||||
shp: shp,
|
||||
}
|
||||
_, err = shp.Seek(32, io.SeekStart)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot seek to SHP geometry type: %v", err)
|
||||
}
|
||||
err = binary.Read(shp, binary.LittleEndian, &w.GeometryType)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot read geometry type: %v", err)
|
||||
}
|
||||
er := &errReader{Reader: shp}
|
||||
w.bbox.MinX = readFloat64(er)
|
||||
w.bbox.MinY = readFloat64(er)
|
||||
w.bbox.MaxX = readFloat64(er)
|
||||
w.bbox.MaxY = readFloat64(er)
|
||||
if er.e != nil {
|
||||
return nil, fmt.Errorf("cannot read bounding box: %v", er.e)
|
||||
}
|
||||
|
||||
shx, err := os.OpenFile(basename+".shx", os.O_RDWR, 0666)
|
||||
if os.IsNotExist(err) {
|
||||
// TODO allow index file to not exist, in that case just
|
||||
// read through all the shapes and create it on the fly
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot open shapefile index: %v", err)
|
||||
}
|
||||
_, err = shx.Seek(-8, io.SeekEnd)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot seek to last shape index: %v", err)
|
||||
}
|
||||
var offset int32
|
||||
err = binary.Read(shx, binary.BigEndian, &offset)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot read last shape index: %v", err)
|
||||
}
|
||||
offset = offset * 2
|
||||
_, err = shp.Seek(int64(offset), io.SeekStart)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot seek to last shape: %v", err)
|
||||
}
|
||||
err = binary.Read(shp, binary.BigEndian, &w.num)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot read number of last shape: %v", err)
|
||||
}
|
||||
_, err = shp.Seek(0, io.SeekEnd)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot seek to SHP end: %v", err)
|
||||
}
|
||||
_, err = shx.Seek(0, io.SeekEnd)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot seek to SHX end: %v", err)
|
||||
}
|
||||
w.shx = shx
|
||||
|
||||
dbf, err := os.Open(basename + ".dbf")
|
||||
if os.IsNotExist(err) {
|
||||
return w, nil // it's okay if the DBF does not exist
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot open DBF: %v", err)
|
||||
}
|
||||
|
||||
_, err = dbf.Seek(8, io.SeekStart)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot seek in DBF: %v", err)
|
||||
}
|
||||
err = binary.Read(dbf, binary.LittleEndian, &w.dbfHeaderLength)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot read header length from DBF: %v", err)
|
||||
}
|
||||
err = binary.Read(dbf, binary.LittleEndian, &w.dbfRecordLength)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot read record length from DBF: %v", err)
|
||||
}
|
||||
|
||||
_, err = dbf.Seek(20, io.SeekCurrent) // skip padding
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot seek in DBF: %v", err)
|
||||
}
|
||||
numFields := int(math.Floor(float64(w.dbfHeaderLength-33) / 32.0))
|
||||
w.dbfFields = make([]Field, numFields)
|
||||
err = binary.Read(dbf, binary.LittleEndian, &w.dbfFields)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot read number of fields from DBF: %v", err)
|
||||
}
|
||||
_, err = dbf.Seek(0, io.SeekEnd) // skip padding
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot seek to DBF end: %v", err)
|
||||
}
|
||||
w.dbf = dbf
|
||||
|
||||
return w, nil
|
||||
}
|
||||
|
||||
// Write shape to the Shapefile. This also creates
|
||||
// a record in the SHX file and DBF file (if it is
|
||||
// initialized). Returns the index of the written object
|
||||
// which can be used in WriteAttribute.
|
||||
func (w *Writer) Write(shape Shape) int32 {
|
||||
// increate bbox
|
||||
if w.num == 0 {
|
||||
w.bbox = shape.BBox()
|
||||
} else {
|
||||
w.bbox.Extend(shape.BBox())
|
||||
}
|
||||
|
||||
w.num++
|
||||
binary.Write(w.shp, binary.BigEndian, w.num)
|
||||
w.shp.Seek(4, io.SeekCurrent)
|
||||
start, _ := w.shp.Seek(0, io.SeekCurrent)
|
||||
binary.Write(w.shp, binary.LittleEndian, w.GeometryType)
|
||||
shape.write(w.shp)
|
||||
finish, _ := w.shp.Seek(0, io.SeekCurrent)
|
||||
length := int32(math.Floor((float64(finish) - float64(start)) / 2.0))
|
||||
w.shp.Seek(start-4, io.SeekStart)
|
||||
binary.Write(w.shp, binary.BigEndian, length)
|
||||
w.shp.Seek(finish, io.SeekStart)
|
||||
|
||||
// write shx
|
||||
binary.Write(w.shx, binary.BigEndian, int32((start-8)/2))
|
||||
binary.Write(w.shx, binary.BigEndian, length)
|
||||
|
||||
// write empty record to dbf
|
||||
if w.dbf != nil {
|
||||
w.writeEmptyRecord()
|
||||
}
|
||||
|
||||
return w.num - 1
|
||||
}
|
||||
|
||||
// Close closes the Writer. This must be used at the end of
|
||||
// the transaction because it writes the correct headers
|
||||
// to the SHP/SHX and DBF files before closing.
|
||||
func (w *Writer) Close() {
|
||||
w.writeHeader(w.shx)
|
||||
w.writeHeader(w.shp)
|
||||
w.shp.Close()
|
||||
w.shx.Close()
|
||||
|
||||
if w.dbf == nil {
|
||||
w.SetFields([]Field{})
|
||||
}
|
||||
w.writeDbfHeader(w.dbf)
|
||||
w.dbf.Close()
|
||||
}
|
||||
|
||||
// writeHeader wrires SHP/SHX headers to ws.
|
||||
func (w *Writer) writeHeader(ws io.WriteSeeker) {
|
||||
filelength, _ := ws.Seek(0, io.SeekEnd)
|
||||
if filelength == 0 {
|
||||
filelength = 100
|
||||
}
|
||||
ws.Seek(0, io.SeekStart)
|
||||
// file code
|
||||
binary.Write(ws, binary.BigEndian, []int32{9994, 0, 0, 0, 0, 0})
|
||||
// file length
|
||||
binary.Write(ws, binary.BigEndian, int32(filelength/2))
|
||||
// version and shape type
|
||||
binary.Write(ws, binary.LittleEndian, []int32{1000, int32(w.GeometryType)})
|
||||
// bounding box
|
||||
binary.Write(ws, binary.LittleEndian, w.bbox)
|
||||
// elevation, measure
|
||||
binary.Write(ws, binary.LittleEndian, []float64{0.0, 0.0, 0.0, 0.0})
|
||||
}
|
||||
|
||||
// writeDbfHeader writes a DBF header to ws.
|
||||
func (w *Writer) writeDbfHeader(ws io.WriteSeeker) {
|
||||
ws.Seek(0, 0)
|
||||
// version, year (YEAR-1990), month, day
|
||||
binary.Write(ws, binary.LittleEndian, []byte{3, 24, 5, 3})
|
||||
// number of records
|
||||
binary.Write(ws, binary.LittleEndian, w.num)
|
||||
// header length, record length
|
||||
binary.Write(ws, binary.LittleEndian, []int16{w.dbfHeaderLength, w.dbfRecordLength})
|
||||
// padding
|
||||
binary.Write(ws, binary.LittleEndian, make([]byte, 20))
|
||||
|
||||
for _, field := range w.dbfFields {
|
||||
binary.Write(ws, binary.LittleEndian, field)
|
||||
}
|
||||
|
||||
// end with return
|
||||
ws.Write([]byte("\r"))
|
||||
}
|
||||
|
||||
// SetFields sets field values in the DBF. This initializes the DBF file and
|
||||
// should be used prior to writing any attributes.
|
||||
func (w *Writer) SetFields(fields []Field) error {
|
||||
if w.dbf != nil {
|
||||
return errors.New("Cannot set fields in existing dbf")
|
||||
}
|
||||
|
||||
var err error
|
||||
w.dbf, err = os.Create(w.filename + ".dbf")
|
||||
if err != nil {
|
||||
return fmt.Errorf("Failed to open %s.dbf: %v", w.filename, err)
|
||||
}
|
||||
w.dbfFields = fields
|
||||
|
||||
// calculate record length
|
||||
w.dbfRecordLength = int16(1)
|
||||
for _, field := range w.dbfFields {
|
||||
w.dbfRecordLength += int16(field.Size)
|
||||
}
|
||||
|
||||
// header lengh
|
||||
w.dbfHeaderLength = int16(len(w.dbfFields)*32 + 33)
|
||||
|
||||
// fill header space with empty bytes for now
|
||||
buf := make([]byte, w.dbfHeaderLength)
|
||||
binary.Write(w.dbf, binary.LittleEndian, buf)
|
||||
|
||||
// write empty records
|
||||
for n := int32(0); n < w.num; n++ {
|
||||
w.writeEmptyRecord()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Writes an empty record to the end of the DBF. This
|
||||
// works by seeking to the end of the file and writing
|
||||
// dbfRecordLength number of bytes. The first byte is a
|
||||
// space that indicates a new record.
|
||||
func (w *Writer) writeEmptyRecord() {
|
||||
w.dbf.Seek(0, io.SeekEnd)
|
||||
buf := make([]byte, w.dbfRecordLength)
|
||||
buf[0] = ' '
|
||||
binary.Write(w.dbf, binary.LittleEndian, buf)
|
||||
}
|
||||
|
||||
// WriteAttribute writes value for field into the given row in the DBF. Row
|
||||
// number should be the same as the order the Shape was written to the
|
||||
// Shapefile. The field value corresponds to the field in the slice used in
|
||||
// SetFields.
|
||||
func (w *Writer) WriteAttribute(row int, field int, value interface{}) error {
|
||||
var buf []byte
|
||||
switch v := value.(type) {
|
||||
case int:
|
||||
buf = []byte(strconv.Itoa(v))
|
||||
case float64:
|
||||
precision := w.dbfFields[field].Precision
|
||||
buf = []byte(strconv.FormatFloat(v, 'f', int(precision), 64))
|
||||
case string:
|
||||
buf = []byte(v)
|
||||
default:
|
||||
return fmt.Errorf("Unsupported value type: %T", v)
|
||||
}
|
||||
|
||||
if w.dbf == nil {
|
||||
return errors.New("Initialize DBF by using SetFields first")
|
||||
}
|
||||
if sz := int(w.dbfFields[field].Size); len(buf) > sz {
|
||||
return fmt.Errorf("Unable to write field %v: %q exceeds field length %v", field, buf, sz)
|
||||
}
|
||||
|
||||
seekTo := 1 + int64(w.dbfHeaderLength) + (int64(row) * int64(w.dbfRecordLength))
|
||||
for n := 0; n < field; n++ {
|
||||
seekTo += int64(w.dbfFields[n].Size)
|
||||
}
|
||||
w.dbf.Seek(seekTo, io.SeekStart)
|
||||
return binary.Write(w.dbf, binary.LittleEndian, buf)
|
||||
}
|
||||
|
||||
// BBox returns the bounding box of the Writer.
|
||||
func (w *Writer) BBox() Box {
|
||||
return w.bbox
|
||||
}
|
209
api/v1/common/tool/shp/writer_test.go
Normal file
209
api/v1/common/tool/shp/writer_test.go
Normal file
@ -0,0 +1,209 @@
|
||||
package shp
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"os"
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
var filenamePrefix = "test_files/write_"
|
||||
|
||||
func removeShapefile(filename string) {
|
||||
os.Remove(filename + ".shp")
|
||||
os.Remove(filename + ".shx")
|
||||
os.Remove(filename + ".dbf")
|
||||
}
|
||||
|
||||
func pointsToFloats(points []Point) [][]float64 {
|
||||
floats := make([][]float64, len(points))
|
||||
for k, v := range points {
|
||||
floats[k] = make([]float64, 2)
|
||||
floats[k][0] = v.X
|
||||
floats[k][1] = v.Y
|
||||
}
|
||||
return floats
|
||||
}
|
||||
|
||||
func TestAppend(t *testing.T) {
|
||||
filename := filenamePrefix + "point"
|
||||
defer removeShapefile(filename)
|
||||
points := [][]float64{
|
||||
{0.0, 0.0},
|
||||
{5.0, 5.0},
|
||||
{10.0, 10.0},
|
||||
}
|
||||
|
||||
shape, err := Create(filename+".shp", POINT)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, p := range points {
|
||||
shape.Write(&Point{p[0], p[1]})
|
||||
}
|
||||
wantNum := shape.num
|
||||
shape.Close()
|
||||
|
||||
newPoints := [][]float64{
|
||||
{15.0, 15.0},
|
||||
{20.0, 20.0},
|
||||
{25.0, 25.0},
|
||||
}
|
||||
shape, err = Append(filename + ".shp")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if shape.GeometryType != POINT {
|
||||
t.Fatalf("wanted geo type %d, got %d", POINT, shape.GeometryType)
|
||||
}
|
||||
if shape.num != wantNum {
|
||||
t.Fatalf("wrong 'num', wanted type %d, got %d", wantNum, shape.num)
|
||||
}
|
||||
|
||||
for _, p := range newPoints {
|
||||
shape.Write(&Point{p[0], p[1]})
|
||||
}
|
||||
|
||||
points = append(points, newPoints...)
|
||||
|
||||
shapes := getShapesFromFile(filename, t)
|
||||
if len(shapes) != len(points) {
|
||||
t.Error("Number of shapes read was wrong")
|
||||
}
|
||||
testPoint(t, points, shapes)
|
||||
}
|
||||
|
||||
func TestWritePoint(t *testing.T) {
|
||||
filename := filenamePrefix + "point"
|
||||
defer removeShapefile(filename)
|
||||
|
||||
points := [][]float64{
|
||||
{0.0, 0.0},
|
||||
{5.0, 5.0},
|
||||
{10.0, 10.0},
|
||||
}
|
||||
|
||||
shape, err := Create(filename+".shp", POINT)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, p := range points {
|
||||
shape.Write(&Point{p[0], p[1]})
|
||||
}
|
||||
shape.Close()
|
||||
|
||||
shapes := getShapesFromFile(filename, t)
|
||||
if len(shapes) != len(points) {
|
||||
t.Error("Number of shapes read was wrong")
|
||||
}
|
||||
testPoint(t, points, shapes)
|
||||
}
|
||||
|
||||
func TestWritePolyLine(t *testing.T) {
|
||||
filename := filenamePrefix + "polyline"
|
||||
defer removeShapefile(filename)
|
||||
|
||||
points := [][]Point{
|
||||
{Point{0.0, 0.0}, Point{5.0, 5.0}},
|
||||
{Point{10.0, 10.0}, Point{15.0, 15.0}},
|
||||
}
|
||||
|
||||
shape, err := Create(filename+".shp", POLYLINE)
|
||||
if err != nil {
|
||||
t.Log(shape, err)
|
||||
}
|
||||
|
||||
l := NewPolyLine(points)
|
||||
|
||||
lWant := &PolyLine{
|
||||
Box: Box{MinX: 0, MinY: 0, MaxX: 15, MaxY: 15},
|
||||
NumParts: 2,
|
||||
NumPoints: 4,
|
||||
Parts: []int32{0, 2},
|
||||
Points: []Point{{X: 0, Y: 0},
|
||||
{X: 5, Y: 5},
|
||||
{X: 10, Y: 10},
|
||||
{X: 15, Y: 15},
|
||||
},
|
||||
}
|
||||
if !reflect.DeepEqual(l, lWant) {
|
||||
t.Errorf("incorrect NewLine: have: %+v; want: %+v", l, lWant)
|
||||
}
|
||||
|
||||
shape.Write(l)
|
||||
shape.Close()
|
||||
|
||||
shapes := getShapesFromFile(filename, t)
|
||||
if len(shapes) != 1 {
|
||||
t.Error("Number of shapes read was wrong")
|
||||
}
|
||||
testPolyLine(t, pointsToFloats(flatten(points)), shapes)
|
||||
}
|
||||
|
||||
type seekTracker struct {
|
||||
io.Writer
|
||||
offset int64
|
||||
}
|
||||
|
||||
func (s *seekTracker) Seek(offset int64, whence int) (int64, error) {
|
||||
s.offset = offset
|
||||
return s.offset, nil
|
||||
}
|
||||
|
||||
func (s *seekTracker) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestWriteAttribute(t *testing.T) {
|
||||
buf := new(bytes.Buffer)
|
||||
s := &seekTracker{Writer: buf}
|
||||
w := Writer{
|
||||
dbf: s,
|
||||
dbfFields: []Field{
|
||||
StringField("A_STRING", 6),
|
||||
FloatField("A_FLOAT", 8, 4),
|
||||
NumberField("AN_INT", 4),
|
||||
},
|
||||
dbfRecordLength: 100,
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
row int
|
||||
field int
|
||||
data interface{}
|
||||
wantOffset int64
|
||||
wantData string
|
||||
}{
|
||||
{"string-0", 0, 0, "test", 1, "test"},
|
||||
{"string-0-overflow-1", 0, 0, "overflo", 0, ""},
|
||||
{"string-0-overflow-n", 0, 0, "overflowing", 0, ""},
|
||||
{"string-3", 3, 0, "things", 301, "things"},
|
||||
{"float-0", 0, 1, 123.44, 7, "123.4400"},
|
||||
{"float-0-overflow-1", 0, 1, 1234.0, 0, ""},
|
||||
{"float-0-overflow-n", 0, 1, 123456789.0, 0, ""},
|
||||
{"int-0", 0, 2, 4242, 15, "4242"},
|
||||
{"int-0-overflow-1", 0, 2, 42424, 0, ""},
|
||||
{"int-0-overflow-n", 0, 2, 42424343, 0, ""},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
buf.Reset()
|
||||
s.offset = 0
|
||||
|
||||
err := w.WriteAttribute(test.row, test.field, test.data)
|
||||
|
||||
if buf.String() != test.wantData {
|
||||
t.Errorf("got data: %v, want: %v", buf.String(), test.wantData)
|
||||
}
|
||||
if s.offset != test.wantOffset {
|
||||
t.Errorf("got seek offset: %v, want: %v", s.offset, test.wantOffset)
|
||||
}
|
||||
if err == nil && test.wantData == "" {
|
||||
t.Error("got no data and no error")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
151
api/v1/common/tool/shp/zipreader.go
Normal file
151
api/v1/common/tool/shp/zipreader.go
Normal file
@ -0,0 +1,151 @@
|
||||
package shp
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"fmt"
|
||||
"io"
|
||||
"path"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// ZipReader provides an interface for reading Shapefiles that are compressed in a ZIP archive.
|
||||
type ZipReader struct {
|
||||
sr SequentialReader
|
||||
z *zip.ReadCloser
|
||||
}
|
||||
|
||||
// openFromZIP is convenience function for opening the file called name that is
|
||||
// compressed in z for reading.
|
||||
func openFromZIP(z *zip.ReadCloser, name string) (io.ReadCloser, error) {
|
||||
for _, f := range z.File {
|
||||
if f.Name == name {
|
||||
return f.Open()
|
||||
|
||||
}
|
||||
}
|
||||
return nil, fmt.Errorf("No such file in archive: %s", name)
|
||||
}
|
||||
|
||||
// OpenZip opens a ZIP file that contains a single shapefile.
|
||||
func OpenZip(zipFilePath string) (*ZipReader, error) {
|
||||
z, err := zip.OpenReader(zipFilePath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
zr := &ZipReader{
|
||||
z: z,
|
||||
}
|
||||
shapeFiles := shapesInZip(z)
|
||||
if len(shapeFiles) == 0 {
|
||||
return nil, fmt.Errorf("archive does not contain a .shp file")
|
||||
}
|
||||
if len(shapeFiles) > 1 {
|
||||
return nil, fmt.Errorf("archive does contain multiple .shp files")
|
||||
}
|
||||
|
||||
shp, err := openFromZIP(zr.z, shapeFiles[0].Name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
withoutExt := strings.TrimSuffix(shapeFiles[0].Name, ".shp")
|
||||
// dbf is optional, so no error checking here
|
||||
dbf, _ := openFromZIP(zr.z, withoutExt+".dbf")
|
||||
zr.sr = SequentialReaderFromExt(shp, dbf)
|
||||
return zr, nil
|
||||
}
|
||||
|
||||
// ShapesInZip returns a string-slice with the names (i.e. relatives paths in
|
||||
// archive file tree) of all shapes that are in the ZIP archive at zipFilePath.
|
||||
func ShapesInZip(zipFilePath string) ([]string, error) {
|
||||
var names []string
|
||||
z, err := zip.OpenReader(zipFilePath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
shapeFiles := shapesInZip(z)
|
||||
for i := range shapeFiles {
|
||||
names = append(names, shapeFiles[i].Name)
|
||||
}
|
||||
return names, nil
|
||||
}
|
||||
|
||||
func shapesInZip(z *zip.ReadCloser) []*zip.File {
|
||||
var shapeFiles []*zip.File
|
||||
for _, f := range z.File {
|
||||
if strings.HasSuffix(f.Name, ".shp") {
|
||||
shapeFiles = append(shapeFiles, f)
|
||||
}
|
||||
}
|
||||
return shapeFiles
|
||||
}
|
||||
|
||||
// OpenShapeFromZip opens a shape file that is contained in a ZIP archive. The
|
||||
// parameter name is name of the shape file.
|
||||
// The name of the shapefile must be a relative path: it must not start with a
|
||||
// drive letter (e.g. C:) or leading slash, and only forward slashes are
|
||||
// allowed. These rules are the same as in
|
||||
// https://golang.org/pkg/archive/zip/#FileHeader.
|
||||
func OpenShapeFromZip(zipFilePath string, name string) (*ZipReader, error) {
|
||||
z, err := zip.OpenReader(zipFilePath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
zr := &ZipReader{
|
||||
z: z,
|
||||
}
|
||||
|
||||
shp, err := openFromZIP(zr.z, name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// dbf is optional, so no error checking here
|
||||
prefix := strings.TrimSuffix(name, path.Ext(name))
|
||||
dbf, _ := openFromZIP(zr.z, prefix+".dbf")
|
||||
zr.sr = SequentialReaderFromExt(shp, dbf)
|
||||
return zr, nil
|
||||
}
|
||||
|
||||
// Close closes the ZipReader and frees the allocated resources.
|
||||
func (zr *ZipReader) Close() error {
|
||||
s := ""
|
||||
err := zr.sr.Close()
|
||||
if err != nil {
|
||||
s += err.Error() + ". "
|
||||
}
|
||||
err = zr.z.Close()
|
||||
if err != nil {
|
||||
s += err.Error() + ". "
|
||||
}
|
||||
if s != "" {
|
||||
return fmt.Errorf(s)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Next reads the next shape in the shapefile and the next row in the DBF. Call
|
||||
// Shape() and Attribute() to access the values.
|
||||
func (zr *ZipReader) Next() bool {
|
||||
return zr.sr.Next()
|
||||
}
|
||||
|
||||
// Shape returns the shape that was last read as well as the current index.
|
||||
func (zr *ZipReader) Shape() (int, Shape) {
|
||||
return zr.sr.Shape()
|
||||
}
|
||||
|
||||
// Attribute returns the n-th field of the last row that was read. If there
|
||||
// were any errors before, the empty string is returned.
|
||||
func (zr *ZipReader) Attribute(n int) string {
|
||||
return zr.sr.Attribute(n)
|
||||
}
|
||||
|
||||
// Fields returns a slice of Fields that are present in the
|
||||
// DBF table.
|
||||
func (zr *ZipReader) Fields() []Field {
|
||||
return zr.sr.Fields()
|
||||
}
|
||||
|
||||
// Err returns the last non-EOF error that was encountered by this ZipReader.
|
||||
func (zr *ZipReader) Err() error {
|
||||
return zr.sr.Err()
|
||||
}
|
236
api/v1/common/tool/shp/zipreader_test.go
Normal file
236
api/v1/common/tool/shp/zipreader_test.go
Normal file
@ -0,0 +1,236 @@
|
||||
package shp
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func compressFileToZIP(zw *zip.Writer, src, tgt string, t *testing.T) {
|
||||
r, err := os.Open(src)
|
||||
if err != nil {
|
||||
t.Fatalf("Could not open for compression %s: %v", src, err)
|
||||
}
|
||||
w, err := zw.Create(tgt)
|
||||
if err != nil {
|
||||
t.Fatalf("Could not start to compress %s: %v", tgt, err)
|
||||
}
|
||||
_, err = io.Copy(w, r)
|
||||
if err != nil {
|
||||
t.Fatalf("Could not compress contents for %s: %v", tgt, err)
|
||||
}
|
||||
}
|
||||
|
||||
// createTempZIP packs the SHP, SHX, and DBF into a ZIP in a temporary
|
||||
// directory
|
||||
func createTempZIP(prefix string, t *testing.T) (dir, filename string) {
|
||||
dir, err := ioutil.TempDir("", "go-shp-test")
|
||||
if err != nil {
|
||||
t.Fatalf("Could not create temporary directory: %v", err)
|
||||
}
|
||||
base := filepath.Base(prefix)
|
||||
zipName := base + ".zip"
|
||||
w, err := os.Create(filepath.Join(dir, zipName))
|
||||
if err != nil {
|
||||
t.Fatalf("Could not create temporary zip file: %v", err)
|
||||
}
|
||||
zw := zip.NewWriter(w)
|
||||
for _, suffix := range []string{".shp", ".shx", ".dbf"} {
|
||||
compressFileToZIP(zw, prefix+suffix, base+suffix, t)
|
||||
}
|
||||
if err := zw.Close(); err != nil {
|
||||
t.Fatalf("Could not close the written zip: %v", err)
|
||||
}
|
||||
return dir, zipName
|
||||
}
|
||||
|
||||
func getShapesZipped(prefix string, t *testing.T) (shapes []Shape) {
|
||||
dir, filename := createTempZIP(prefix, t)
|
||||
defer os.RemoveAll(dir)
|
||||
zr, err := OpenZip(filepath.Join(dir, filename))
|
||||
if err != nil {
|
||||
t.Errorf("Error when opening zip file: %v", err)
|
||||
}
|
||||
for zr.Next() {
|
||||
_, shape := zr.Shape()
|
||||
shapes = append(shapes, shape)
|
||||
}
|
||||
if err := zr.Err(); err != nil {
|
||||
t.Errorf("Error when iterating over the shapes: %v", err)
|
||||
}
|
||||
|
||||
if err := zr.Close(); err != nil {
|
||||
t.Errorf("Could not close zipreader: %v", err)
|
||||
}
|
||||
return shapes
|
||||
}
|
||||
|
||||
func TestZipReader(t *testing.T) {
|
||||
for prefix := range dataForReadTests {
|
||||
t.Logf("Testing zipped reading for %s", prefix)
|
||||
testshapeIdentity(t, prefix, getShapesZipped)
|
||||
}
|
||||
}
|
||||
|
||||
func unzipToTempDir(t *testing.T, p string) string {
|
||||
td, err := ioutil.TempDir("", "")
|
||||
if err != nil {
|
||||
t.Fatalf("%v", err)
|
||||
}
|
||||
zip, err := zip.OpenReader(p)
|
||||
if err != nil {
|
||||
t.Fatalf("%v", err)
|
||||
}
|
||||
defer zip.Close()
|
||||
for _, f := range zip.File {
|
||||
_, fn := path.Split(f.Name)
|
||||
pn := filepath.Join(td, fn)
|
||||
t.Logf("Uncompress: %s -> %s", f.Name, pn)
|
||||
w, err := os.Create(pn)
|
||||
if err != nil {
|
||||
t.Fatalf("Cannot unzip %s: %v", p, err)
|
||||
}
|
||||
defer w.Close()
|
||||
r, err := f.Open()
|
||||
if err != nil {
|
||||
t.Fatalf("Cannot unzip %s: %v", p, err)
|
||||
}
|
||||
defer r.Close()
|
||||
_, err = io.Copy(w, r)
|
||||
if err != nil {
|
||||
t.Fatalf("Cannot unzip %s: %v", p, err)
|
||||
}
|
||||
}
|
||||
return td
|
||||
}
|
||||
|
||||
// TestZipReaderAttributes reads the same shapesfile twice, first directly from
|
||||
// the Shp with a Reader, and, second, from a zip. It compares the fields as
|
||||
// well as the shapes and the attributes. For this test, the Shapes are
|
||||
// considered to be equal if their bounding boxes are equal.
|
||||
func TestZipReaderAttribute(t *testing.T) {
|
||||
b := "ne_110m_admin_0_countries"
|
||||
skipOrDownloadNaturalEarth(t, b+".zip")
|
||||
d := unzipToTempDir(t, b+".zip")
|
||||
defer os.RemoveAll(d)
|
||||
lr, err := Open(filepath.Join(d, b+".shp"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer lr.Close()
|
||||
zr, err := OpenZip(b + ".zip")
|
||||
if os.IsNotExist(err) {
|
||||
t.Skipf("Skipping test, as Natural Earth dataset wasn't found")
|
||||
}
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer zr.Close()
|
||||
fsl := lr.Fields()
|
||||
fsz := zr.Fields()
|
||||
if len(fsl) != len(fsz) {
|
||||
t.Fatalf("Number of attributes do not match: Wanted %d, got %d", len(fsl), len(fsz))
|
||||
}
|
||||
for i := range fsl {
|
||||
if fsl[i] != fsz[i] {
|
||||
t.Fatalf("Attribute %d (%s) does not match (%s)", i, fsl[i], fsz[i])
|
||||
}
|
||||
}
|
||||
for zr.Next() && lr.Next() {
|
||||
ln, ls := lr.Shape()
|
||||
zn, zs := zr.Shape()
|
||||
if ln != zn {
|
||||
t.Fatalf("Sequence number wrong: Wanted %d, got %d", ln, zn)
|
||||
}
|
||||
if ls.BBox() != zs.BBox() {
|
||||
t.Fatalf("Bounding boxes for shape #%d do not match", ln+1)
|
||||
}
|
||||
for i := range fsl {
|
||||
la := lr.Attribute(i)
|
||||
za := zr.Attribute(i)
|
||||
if la != za {
|
||||
t.Fatalf("Shape %d: Attribute %d (%s) are unequal: '%s' vs '%s'",
|
||||
ln+1, i, fsl[i].String(), la, za)
|
||||
}
|
||||
}
|
||||
}
|
||||
if lr.Err() != nil {
|
||||
t.Logf("Reader error: %v / ZipReader error: %v", lr.Err(), zr.Err())
|
||||
t.FailNow()
|
||||
}
|
||||
}
|
||||
|
||||
func skipOrDownloadNaturalEarth(t *testing.T, p string) {
|
||||
if _, err := os.Stat(p); os.IsNotExist(err) {
|
||||
dl := false
|
||||
for _, a := range os.Args {
|
||||
if a == "download" {
|
||||
dl = true
|
||||
break
|
||||
}
|
||||
}
|
||||
u := "http://www.naturalearthdata.com/http//www.naturalearthdata.com/download/110m/cultural/ne_110m_admin_0_countries.zip"
|
||||
if !dl {
|
||||
t.Skipf("Skipped, as %s does not exist. Consider calling tests with '-args download` "+
|
||||
"or download manually from '%s'", p, u)
|
||||
} else {
|
||||
t.Logf("Downloading %s", u)
|
||||
w, err := os.Create(p)
|
||||
if err != nil {
|
||||
t.Fatalf("Could not create %q: %v", p, err)
|
||||
}
|
||||
defer w.Close()
|
||||
resp, err := http.Get(u)
|
||||
if err != nil {
|
||||
t.Fatalf("Could not download %q: %v", u, err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
_, err = io.Copy(w, resp.Body)
|
||||
if err != nil {
|
||||
t.Fatalf("Could not download %q: %v", u, err)
|
||||
}
|
||||
t.Logf("Download complete")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestNaturalEarthZip(t *testing.T) {
|
||||
type metaShape struct {
|
||||
Attributes map[string]string
|
||||
Shape
|
||||
}
|
||||
p := "ne_110m_admin_0_countries.zip"
|
||||
skipOrDownloadNaturalEarth(t, p)
|
||||
zr, err := OpenZip(p)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer zr.Close()
|
||||
|
||||
fs := zr.Fields()
|
||||
if len(fs) != 63 {
|
||||
t.Fatalf("Expected 63 columns in Natural Earth dataset, got %d", len(fs))
|
||||
}
|
||||
var metas []metaShape
|
||||
for zr.Next() {
|
||||
m := metaShape{
|
||||
Attributes: make(map[string]string),
|
||||
}
|
||||
_, m.Shape = zr.Shape()
|
||||
for n := range fs {
|
||||
m.Attributes[fs[n].String()] = zr.Attribute(n)
|
||||
}
|
||||
metas = append(metas, m)
|
||||
}
|
||||
if zr.Err() != nil {
|
||||
t.Fatal(zr.Err())
|
||||
}
|
||||
for _, m := range metas {
|
||||
t.Log(m.Attributes["name"])
|
||||
}
|
||||
}
|
308
api/v1/common/tool/tool.go
Normal file
308
api/v1/common/tool/tool.go
Normal file
@ -0,0 +1,308 @@
|
||||
package tool
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/md5"
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"math/rand"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"regexp"
|
||||
"runtime"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
/*
|
||||
判断文件或文件夹是否存在
|
||||
如果返回的错误为nil,说明文件或文件夹存在
|
||||
如果返回的错误类型使用os.IsNotExist()判断为true,说明文件或文件夹不存在
|
||||
如果返回的错误为其它类型,则不确定是否在存在
|
||||
*/
|
||||
func PathExists(path string) bool {
|
||||
|
||||
_, err := os.Stat(path)
|
||||
if err == nil {
|
||||
return true
|
||||
}
|
||||
if os.IsNotExist(err) {
|
||||
return false
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
/*
|
||||
检查字符串是否在某一数组中
|
||||
*/
|
||||
func IsContainStr(items []string, item string) bool {
|
||||
for _, eachItem := range items {
|
||||
if eachItem == item {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// 调用os.MkdirAll递归创建文件夹
|
||||
func CreateDir(dirPath string) {
|
||||
if !isExist(dirPath) {
|
||||
fmt.Println("路径不存在,创建路径", dirPath)
|
||||
_ = os.MkdirAll(dirPath, os.ModePerm)
|
||||
}
|
||||
}
|
||||
|
||||
// 判断所给路径文件/文件夹是否存在(返回true是存在)
|
||||
func isExist(path string) bool {
|
||||
_, err := os.Stat(path) //os.Stat获取文件信息
|
||||
if err != nil {
|
||||
if os.IsExist(err) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
/*获取uuid*/
|
||||
func GetUuid() string {
|
||||
str := GetRandstring(32)
|
||||
time.Sleep(time.Nanosecond)
|
||||
return strings.ToLower(Md5V(str))
|
||||
}
|
||||
|
||||
// #取得随机字符串:使用字符串拼接
|
||||
func GetRandstring(lenNum int) string {
|
||||
var CHARS = []string{"a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z",
|
||||
"A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z",
|
||||
"1", "2", "3", "4", "5", "6", "7", "8", "9", "0"}
|
||||
str := strings.Builder{}
|
||||
length := len(CHARS)
|
||||
for i := 0; i < lenNum; i++ {
|
||||
l := CHARS[rand.Intn(length)]
|
||||
str.WriteString(l)
|
||||
}
|
||||
return str.String()
|
||||
|
||||
}
|
||||
|
||||
// 获取当前执行程序所在的绝对路径
|
||||
func GetCurrentAbPathByExecutable() string {
|
||||
exePath, err := os.Executable()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
res, _ := filepath.EvalSymlinks(filepath.Dir(exePath))
|
||||
return res
|
||||
}
|
||||
|
||||
/*结构体转map且首字母小写*/
|
||||
func Struct2Map(obj interface{}) map[string]interface{} {
|
||||
t := reflect.TypeOf(obj)
|
||||
v := reflect.ValueOf(obj)
|
||||
var data = make(map[string]interface{})
|
||||
for i := 0; i < t.NumField(); i++ {
|
||||
data[strings.ToLower(string(t.Field(i).Name[0]))+t.Field(i).Name[1:]] = v.Field(i).Interface()
|
||||
//data[t.Field(i).Name] = v.Field(i).Interface()
|
||||
}
|
||||
return data
|
||||
}
|
||||
func PortInUse(port int) int {
|
||||
checkStatement := fmt.Sprintf("lsof -i:%d ", port)
|
||||
output, _ := exec.Command("sh", "-c", checkStatement).CombinedOutput()
|
||||
fmt.Println(output)
|
||||
if len(output) > 0 {
|
||||
return 1
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
/*检测端口是否在使用中*/
|
||||
// 传入查询的端口号
|
||||
// 返回端口号对应的进程PID,若没有找到相关进程,返回-1
|
||||
func portInUse(portNumber int) int {
|
||||
res := -1
|
||||
var outBytes bytes.Buffer
|
||||
sysType := runtime.GOOS
|
||||
fmt.Println(sysType)
|
||||
var cmdStr = ""
|
||||
if sysType == "linux" {
|
||||
cmdStr = fmt.Sprintf("lsof -i:%d ", portNumber)
|
||||
}
|
||||
//
|
||||
if sysType == "windows" {
|
||||
cmdStr = fmt.Sprintf("netstat -ano -p tcp | findstr %d", portNumber)
|
||||
}
|
||||
//checkStatement := fmt.Sprintf("lsof -i:%d ", portNumber)
|
||||
fmt.Println(cmdStr)
|
||||
cmd := exec.Command("cmd", "/c", cmdStr)
|
||||
cmd.Stdout = &outBytes
|
||||
cmd.Run()
|
||||
resStr := outBytes.String()
|
||||
fmt.Println(resStr)
|
||||
r := regexp.MustCompile(`\s\d+\s`).FindAllString(resStr, -1)
|
||||
if len(r) > 0 {
|
||||
pid, err := strconv.Atoi(strings.TrimSpace(r[0]))
|
||||
if err != nil {
|
||||
res = -1
|
||||
} else {
|
||||
res = pid
|
||||
}
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
func GetUnUsePort(port int) int {
|
||||
isInUse := PortInUse(port)
|
||||
if isInUse != -1 {
|
||||
fmt.Println("端口:" + strconv.Itoa(port) + " 被占用")
|
||||
port++
|
||||
port = GetUnUsePort(port)
|
||||
return port
|
||||
} else {
|
||||
return port
|
||||
}
|
||||
}
|
||||
|
||||
// WeekIntervalTime 获取某周的开始和结束时间,week为0本周,-1上周,1下周以此类推
|
||||
func WeekIntervalTime(week int) (startTime, endTime string) {
|
||||
now := time.Now()
|
||||
offset := int(time.Monday - now.Weekday())
|
||||
//周日做特殊判断 因为time.Monday = 0
|
||||
if offset > 0 {
|
||||
offset = -6
|
||||
}
|
||||
|
||||
year, month, day := now.Date()
|
||||
thisWeek := time.Date(year, month, day, 0, 0, 0, 0, time.Local)
|
||||
startTime = thisWeek.AddDate(0, 0, offset+7*week).Format("2006-01-02") + " 00:00:00"
|
||||
endTime = thisWeek.AddDate(0, 0, offset+6+7*week).Format("2006-01-02") + " 23:59:59"
|
||||
|
||||
return startTime, endTime
|
||||
}
|
||||
|
||||
// GetCurrentTime 获取当前系统时间
|
||||
func GetCurrentTime() string {
|
||||
return time.Now().Format("2006-01-02 15:04:05")
|
||||
}
|
||||
|
||||
/*删除文件或文件夹*/
|
||||
func RemoveFile(path string) {
|
||||
if isExist(path) {
|
||||
err := os.Remove(path)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 检查参数
|
||||
func Md5V(str string) string {
|
||||
h := md5.New()
|
||||
h.Write([]byte(str))
|
||||
return hex.EncodeToString(h.Sum(nil))
|
||||
}
|
||||
|
||||
// download file会将url下载到本地文件,它会在下载时写入,而不是将整个文件加载到内存中。
|
||||
func DownloadFile(filepath string, url string) error {
|
||||
|
||||
// Get the data
|
||||
resp, err := http.Get(url)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
// Create the file
|
||||
out, err := os.Create(filepath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer out.Close()
|
||||
|
||||
// Write the body to file
|
||||
_, err = io.Copy(out, resp.Body)
|
||||
return err
|
||||
}
|
||||
|
||||
// 通用排序
|
||||
// 结构体排序,必须重写数组Len() Swap() Less()函数
|
||||
type body_wrapper struct {
|
||||
Bodys []interface{}
|
||||
by func(p, q *interface{}) bool //内部Less()函数会用到
|
||||
}
|
||||
type SortBodyBy func(p, q *interface{}) bool //定义一个函数类型
|
||||
|
||||
// 数组长度Len()
|
||||
func (acw body_wrapper) Len() int {
|
||||
return len(acw.Bodys)
|
||||
}
|
||||
|
||||
// 元素交换
|
||||
func (acw body_wrapper) Swap(i, j int) {
|
||||
acw.Bodys[i], acw.Bodys[j] = acw.Bodys[j], acw.Bodys[i]
|
||||
}
|
||||
|
||||
// 比较函数,使用外部传入的by比较函数
|
||||
func (acw body_wrapper) Less(i, j int) bool {
|
||||
return acw.by(&acw.Bodys[i], &acw.Bodys[j])
|
||||
}
|
||||
|
||||
// 自定义排序字段,参考SortBodyByCreateTime中的传入函数
|
||||
func SortBody(bodys []interface{}, by SortBodyBy) {
|
||||
sort.Sort(body_wrapper{bodys, by})
|
||||
}
|
||||
|
||||
// 格式化时间
|
||||
type LocalTime time.Time
|
||||
|
||||
func (t *LocalTime) MarshalJSON() ([]byte, error) {
|
||||
tTime := time.Time(*t)
|
||||
return []byte(fmt.Sprintf("\"%v\"", tTime.Format("2006-01-02 15:04:05"))), nil
|
||||
}
|
||||
|
||||
// 写入文件,保存
|
||||
func WriteFile(path string, base64_image_content string) (error, string) {
|
||||
|
||||
//b, _ := regexp.MatchString(`^data:\s*image\/(\w+);base64,`, base64_image_content)
|
||||
//if !b {
|
||||
// return errors.New(""), ""
|
||||
//}
|
||||
base64_image_content = "data:image/png;base64," + base64_image_content
|
||||
re, _ := regexp.Compile(`^data:\s*image\/(\w+);base64,`)
|
||||
allData := re.FindAllSubmatch([]byte(base64_image_content), 2)
|
||||
fileType := string(allData[0][1]) //png ,jpeg 后缀获取
|
||||
|
||||
base64Str := re.ReplaceAllString(base64_image_content, "")
|
||||
|
||||
//date := time.Now().Format("2006-01-02")
|
||||
//if ok := IsFileExist(path + "/" + date); !ok {
|
||||
// os.Mkdir(path+"/"+date, 0666)
|
||||
//}
|
||||
|
||||
curFileStr := strconv.FormatInt(time.Now().UnixNano(), 10)
|
||||
|
||||
r := rand.New(rand.NewSource(time.Now().UnixNano()))
|
||||
n := r.Intn(99999)
|
||||
var filename = curFileStr + strconv.Itoa(n) + "." + fileType
|
||||
var file = path + "/" + filename
|
||||
byte, _ := base64.StdEncoding.DecodeString(base64Str)
|
||||
|
||||
err := ioutil.WriteFile(file, byte, 0666)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
}
|
||||
return err, filename
|
||||
}
|
||||
|
||||
func UploadsFile() {
|
||||
|
||||
}
|
26
api/v1/common/tool/turf/turf.go
Normal file
26
api/v1/common/tool/turf/turf.go
Normal file
@ -0,0 +1,26 @@
|
||||
package turf
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
"github.com/dop251/goja"
|
||||
)
|
||||
|
||||
//go:embed turf.min.js
|
||||
var turf_min string
|
||||
|
||||
//go:embed turf.js
|
||||
var turf string
|
||||
var Tin func(wgs84 [][]string) [][]string
|
||||
var BooleanPointInPolygon func(point []float64, polygon [][]float64) bool
|
||||
|
||||
//// 初始化
|
||||
//func init() {
|
||||
// InitTurfjs()
|
||||
//}
|
||||
|
||||
func InitTurfjs() {
|
||||
vm := goja.New()
|
||||
vm.RunString(turf_min + turf)
|
||||
vm.ExportTo(vm.Get("Tin"), &Tin)
|
||||
vm.ExportTo(vm.Get("BooleanPointInPolygon"), &BooleanPointInPolygon)
|
||||
}
|
26
api/v1/common/tool/turf/turf.js
Normal file
26
api/v1/common/tool/turf/turf.js
Normal file
@ -0,0 +1,26 @@
|
||||
function Tin(points=[]) {
|
||||
let arr = []
|
||||
points.forEach(p=>{
|
||||
arr.push(turf.point( [parseFloat(p[0]), parseFloat(p[1])]))
|
||||
})
|
||||
var tin = turf.tin(turf.featureCollection(arr));
|
||||
let polylines=[]
|
||||
tin.features.forEach((feature, index) => {
|
||||
feature.geometry.coordinates.forEach((coordinate,) => {
|
||||
polylines.push([
|
||||
coordinate[0],
|
||||
coordinate[1],
|
||||
coordinate[2],
|
||||
])
|
||||
})
|
||||
})
|
||||
return polylines
|
||||
return JSON.stringify(polylines)
|
||||
}
|
||||
|
||||
function BooleanPointInPolygon(point,polygon=[]) {
|
||||
var pt = turf.point([point[0],point[1]]);
|
||||
var poly = turf.polygon([polygon]);
|
||||
var scaledPoly = turf.transformScale(poly, 2);
|
||||
return turf.booleanPointInPolygon(pt, poly)
|
||||
}
|
91
api/v1/common/tool/turf/turf.min.js
vendored
Normal file
91
api/v1/common/tool/turf/turf.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
52
api/v1/common/tool/xyz2wgs84.go
Normal file
52
api/v1/common/tool/xyz2wgs84.go
Normal file
@ -0,0 +1,52 @@
|
||||
package tool
|
||||
|
||||
import (
|
||||
"math"
|
||||
)
|
||||
|
||||
var a = float64(6378137)
|
||||
|
||||
var b = 6356752.3142
|
||||
|
||||
var asqr = a * a
|
||||
|
||||
var bsqr = b * b
|
||||
|
||||
var e = math.Sqrt((asqr - bsqr) / asqr)
|
||||
|
||||
var eprime = math.Sqrt((asqr - bsqr) / bsqr)
|
||||
|
||||
func Xyz2Wgs84(X, Y, Z float64) (lng, lat, height float64) {
|
||||
var p = math.Sqrt(X*X + Y*Y)
|
||||
var theta = math.Atan((Z * a) / (p * b))
|
||||
var sintheta = math.Sin(theta)
|
||||
var costheta = math.Cos(theta)
|
||||
var num = Z + eprime*eprime*b*sintheta*sintheta*sintheta
|
||||
var denom = p - e*e*a*costheta*costheta*costheta
|
||||
//Now calculate LLA
|
||||
var latitude = math.Atan(num / denom)
|
||||
var longitude = math.Atan(Y / X)
|
||||
var N = getN(latitude)
|
||||
var altitude = (p / math.Cos(latitude)) - N
|
||||
|
||||
if X < 0 && Y < 0 {
|
||||
longitude = longitude - math.Pi
|
||||
}
|
||||
|
||||
if X < 0 && Y > 0 {
|
||||
longitude = longitude + math.Pi
|
||||
}
|
||||
return radiansToDegrees(longitude), radiansToDegrees(latitude), altitude
|
||||
}
|
||||
|
||||
func getN(latitude float64) float64 {
|
||||
var sinlatitude = math.Sin(latitude)
|
||||
var denom = math.Sqrt(1 - e*e*sinlatitude*sinlatitude)
|
||||
var N = a / denom
|
||||
return N
|
||||
}
|
||||
func radiansToDegrees(radians float64) float64 {
|
||||
return radians * 180 / math.Pi
|
||||
}
|
||||
|
||||
//106.54959740614493 23.47200769358978
|
113
api/v1/common/tool/zip/zip.go
Normal file
113
api/v1/common/tool/zip/zip.go
Normal file
@ -0,0 +1,113 @@
|
||||
package zip
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path"
|
||||
)
|
||||
|
||||
func ZipFiles(filename string, files []string) error {
|
||||
fmt.Println("start zip file......")
|
||||
//创建输出文件目录
|
||||
newZipFile, err := os.Create(filename)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer newZipFile.Close()
|
||||
//创建空的zip档案,可以理解为打开zip文件,准备写入
|
||||
zipWriter := zip.NewWriter(newZipFile)
|
||||
defer zipWriter.Close()
|
||||
// Add files to zip
|
||||
for _, file := range files {
|
||||
if err = AddFileToZip(zipWriter, file); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func AddFileToZip(zipWriter *zip.Writer, filename string) error {
|
||||
//打开要压缩的文件
|
||||
fileToZip, err := os.Open(filename)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer fileToZip.Close()
|
||||
//获取文件的描述
|
||||
info, err := fileToZip.Stat()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
//FileInfoHeader返回一个根据fi填写了部分字段的Header,可以理解成是将fileinfo转换成zip格式的文件信息
|
||||
header, err := zip.FileInfoHeader(info)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
header.Name = filename
|
||||
/*
|
||||
预定义压缩算法。
|
||||
archive/zip包中预定义的有两种压缩方式。一个是仅把文件写入到zip中。不做压缩。一种是压缩文件然后写入到zip中。默认的Store模式。就是只保存不压缩的模式。
|
||||
Store unit16 = 0 //仅存储文件
|
||||
Deflate unit16 = 8 //压缩文件
|
||||
*/
|
||||
header.Method = zip.Store
|
||||
//创建压缩包头部信息
|
||||
writer, err := zipWriter.CreateHeader(header)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
//将源复制到目标,将fileToZip 写入writer 是按默认的缓冲区32k循环操作的,不会将内容一次性全写入内存中,这样就能解决大文件的问题
|
||||
_, err = io.Copy(writer, fileToZip)
|
||||
return err
|
||||
}
|
||||
|
||||
// Decompressor 解压
|
||||
func Decompressor(zipFilePath string, targetDir string, filename string) error {
|
||||
reader, err := zip.OpenReader(zipFilePath)
|
||||
if nil != err {
|
||||
fmt.Println(err)
|
||||
return err
|
||||
}
|
||||
defer reader.Close()
|
||||
|
||||
_ = os.MkdirAll(targetDir, 0777)
|
||||
names := []string{}
|
||||
for _, f := range reader.File {
|
||||
err := func() error {
|
||||
if f.FileInfo().IsDir() {
|
||||
_ = os.MkdirAll(path.Join(targetDir, f.Name), f.Mode())
|
||||
return nil
|
||||
}
|
||||
suffix := path.Ext(f.Name)
|
||||
//fmt.Println(f.Name)
|
||||
//fmt.Println(path.Join(targetDir, f.Name))
|
||||
writeFile, err := os.OpenFile(path.Join(targetDir, filename+suffix), os.O_WRONLY|os.O_CREATE, f.Mode())
|
||||
if nil != err {
|
||||
return err
|
||||
}
|
||||
defer writeFile.Close()
|
||||
|
||||
readFile, err := f.Open()
|
||||
if nil != err {
|
||||
return err
|
||||
}
|
||||
defer readFile.Close()
|
||||
|
||||
n, err := io.Copy(writeFile, readFile)
|
||||
if nil != err {
|
||||
return err
|
||||
}
|
||||
if false {
|
||||
names = append(names, f.Name)
|
||||
fmt.Printf("解压文件: %s 大小: %v", f.Name, n)
|
||||
}
|
||||
return nil
|
||||
}()
|
||||
if nil != err {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
83
api/v1/system/app_menus.go
Normal file
83
api/v1/system/app_menus.go
Normal file
@ -0,0 +1,83 @@
|
||||
// ==========================================================================
|
||||
// GFast自动生成api操作代码。
|
||||
// 生成日期:2024-05-28 15:09:13
|
||||
// 生成路径: api/v1/system/app_menus.go
|
||||
// 生成人:gfast
|
||||
// desc:app菜单相关参数
|
||||
// company:云南奇讯科技有限公司
|
||||
// ==========================================================================
|
||||
|
||||
package system
|
||||
|
||||
import (
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
commonApi "github.com/tiger1103/gfast/v3/api/v1/common"
|
||||
"github.com/tiger1103/gfast/v3/internal/app/system/model"
|
||||
)
|
||||
|
||||
// AppMenusSearchReq 分页请求参数
|
||||
type AppMenusSearchReq struct {
|
||||
g.Meta `path:"/list" tags:"app菜单" method:"get" summary:"app菜单列表"`
|
||||
MenuId string `p:"menuId"` //
|
||||
MenuName string `p:"menuName"` //菜单名
|
||||
CreatedAt string `p:"createdAt" v:"createdAt@datetime#需为YYYY-MM-DD hh:mm:ss格式"` //
|
||||
commonApi.PageReq
|
||||
commonApi.Author
|
||||
}
|
||||
|
||||
// AppMenusSearchRes 列表返回结果
|
||||
type AppMenusSearchRes struct {
|
||||
g.Meta `mime:"application/json"`
|
||||
commonApi.ListRes
|
||||
List []*model.AppMenusListRes `json:"list"`
|
||||
}
|
||||
|
||||
// AppMenusAddReq 添加操作请求参数
|
||||
type AppMenusAddReq struct {
|
||||
g.Meta `path:"/add" tags:"app菜单" method:"post" summary:"app菜单添加"`
|
||||
commonApi.Author
|
||||
MenuName string `p:"menuName" v:"required#菜单名不能为空"`
|
||||
}
|
||||
|
||||
// AppMenusAddRes 添加操作返回结果
|
||||
type AppMenusAddRes struct {
|
||||
commonApi.EmptyRes
|
||||
}
|
||||
|
||||
// AppMenusEditReq 修改操作请求参数
|
||||
type AppMenusEditReq struct {
|
||||
g.Meta `path:"/edit" tags:"app菜单" method:"put" summary:"app菜单修改"`
|
||||
commonApi.Author
|
||||
MenuId uint `p:"menuId" v:"required#主键ID不能为空"`
|
||||
MenuName string `p:"menuName" v:"required#菜单名不能为空"`
|
||||
}
|
||||
|
||||
// AppMenusEditRes 修改操作返回结果
|
||||
type AppMenusEditRes struct {
|
||||
commonApi.EmptyRes
|
||||
}
|
||||
|
||||
// AppMenusGetReq 获取一条数据请求
|
||||
type AppMenusGetReq struct {
|
||||
g.Meta `path:"/get" tags:"app菜单" method:"get" summary:"获取app菜单信息"`
|
||||
commonApi.Author
|
||||
MenuId uint `p:"menuId" v:"required#主键必须"` //通过主键获取
|
||||
}
|
||||
|
||||
// AppMenusGetRes 获取一条数据结果
|
||||
type AppMenusGetRes struct {
|
||||
g.Meta `mime:"application/json"`
|
||||
*model.AppMenusInfoRes
|
||||
}
|
||||
|
||||
// AppMenusDeleteReq 删除数据请求
|
||||
type AppMenusDeleteReq struct {
|
||||
g.Meta `path:"/delete" tags:"app菜单" method:"delete" summary:"删除app菜单"`
|
||||
commonApi.Author
|
||||
MenuIds []uint `p:"menuIds" v:"required#主键必须"` //通过主键删除
|
||||
}
|
||||
|
||||
// AppMenusDeleteRes 删除数据返回
|
||||
type AppMenusDeleteRes struct {
|
||||
commonApi.EmptyRes
|
||||
}
|
86
api/v1/system/app_role_menus.go
Normal file
86
api/v1/system/app_role_menus.go
Normal file
@ -0,0 +1,86 @@
|
||||
// ==========================================================================
|
||||
// GFast自动生成api操作代码。
|
||||
// 生成日期:2024-05-28 15:11:14
|
||||
// 生成路径: api/v1/system/app_role_menus.go
|
||||
// 生成人:gfast
|
||||
// desc:app角色绑定菜单相关参数
|
||||
// company:云南奇讯科技有限公司
|
||||
// ==========================================================================
|
||||
|
||||
package system
|
||||
|
||||
import (
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
commonApi "github.com/tiger1103/gfast/v3/api/v1/common"
|
||||
"github.com/tiger1103/gfast/v3/internal/app/system/model"
|
||||
)
|
||||
|
||||
// AppRoleMenusSearchReq 分页请求参数
|
||||
type AppRoleMenusSearchReq struct {
|
||||
g.Meta `path:"/list" tags:"app角色绑定菜单" method:"get" summary:"app角色绑定菜单列表"`
|
||||
Id string `p:"id"` //
|
||||
RoleId string `p:"roleId" v:"roleId@integer#角色ID需为整数"` //角色ID
|
||||
MenuId string `p:"menuId" v:"menuId@integer#菜单ID需为整数"` //菜单ID
|
||||
CreatedAt string `p:"createdAt" v:"createdAt@datetime#需为YYYY-MM-DD hh:mm:ss格式"` //
|
||||
commonApi.PageReq
|
||||
commonApi.Author
|
||||
}
|
||||
|
||||
// AppRoleMenusSearchRes 列表返回结果
|
||||
type AppRoleMenusSearchRes struct {
|
||||
g.Meta `mime:"application/json"`
|
||||
commonApi.ListRes
|
||||
List []*model.AppRoleMenusListRes `json:"list"`
|
||||
}
|
||||
|
||||
// AppRoleMenusAddReq 添加操作请求参数
|
||||
type AppRoleMenusAddReq struct {
|
||||
g.Meta `path:"/add" tags:"app角色绑定菜单" method:"post" summary:"app角色绑定菜单添加"`
|
||||
commonApi.Author
|
||||
RoleId int `p:"roleId" `
|
||||
MenuId int `p:"menuId" `
|
||||
}
|
||||
|
||||
// AppRoleMenusAddRes 添加操作返回结果
|
||||
type AppRoleMenusAddRes struct {
|
||||
commonApi.EmptyRes
|
||||
}
|
||||
|
||||
// AppRoleMenusEditReq 修改操作请求参数
|
||||
type AppRoleMenusEditReq struct {
|
||||
g.Meta `path:"/edit" tags:"app角色绑定菜单" method:"put" summary:"app角色绑定菜单修改"`
|
||||
commonApi.Author
|
||||
Id uint `p:"id" v:"required#主键ID不能为空"`
|
||||
RoleId int `p:"roleId" `
|
||||
MenuId int `p:"menuId" `
|
||||
}
|
||||
|
||||
// AppRoleMenusEditRes 修改操作返回结果
|
||||
type AppRoleMenusEditRes struct {
|
||||
commonApi.EmptyRes
|
||||
}
|
||||
|
||||
// AppRoleMenusGetReq 获取一条数据请求
|
||||
type AppRoleMenusGetReq struct {
|
||||
g.Meta `path:"/get" tags:"app角色绑定菜单" method:"get" summary:"获取app角色绑定菜单信息"`
|
||||
commonApi.Author
|
||||
Id uint `p:"id" v:"required#主键必须"` //通过主键获取
|
||||
}
|
||||
|
||||
// AppRoleMenusGetRes 获取一条数据结果
|
||||
type AppRoleMenusGetRes struct {
|
||||
g.Meta `mime:"application/json"`
|
||||
*model.AppRoleMenusInfoRes
|
||||
}
|
||||
|
||||
// AppRoleMenusDeleteReq 删除数据请求
|
||||
type AppRoleMenusDeleteReq struct {
|
||||
g.Meta `path:"/delete" tags:"app角色绑定菜单" method:"delete" summary:"删除app角色绑定菜单"`
|
||||
commonApi.Author
|
||||
Ids []uint `p:"ids" v:"required#主键必须"` //通过主键删除
|
||||
}
|
||||
|
||||
// AppRoleMenusDeleteRes 删除数据返回
|
||||
type AppRoleMenusDeleteRes struct {
|
||||
commonApi.EmptyRes
|
||||
}
|
108
api/v1/system/app_roles.go
Normal file
108
api/v1/system/app_roles.go
Normal file
@ -0,0 +1,108 @@
|
||||
// ==========================================================================
|
||||
// GFast自动生成api操作代码。
|
||||
// 生成日期:2024-05-28 15:11:36
|
||||
// 生成路径: api/v1/system/app_roles.go
|
||||
// 生成人:gfast
|
||||
// desc:app角色相关参数
|
||||
// company:云南奇讯科技有限公司
|
||||
// ==========================================================================
|
||||
|
||||
package system
|
||||
|
||||
import (
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
commonApi "github.com/tiger1103/gfast/v3/api/v1/common"
|
||||
"github.com/tiger1103/gfast/v3/internal/app/system/model"
|
||||
)
|
||||
|
||||
// AppRolesSearchReq 分页请求参数
|
||||
type AppRolesSearchReq struct {
|
||||
g.Meta `path:"/list" tags:"app角色" method:"get" summary:"app角色列表"`
|
||||
RoleId string `p:"roleId"` //
|
||||
RoleName string `p:"roleName"` // 角色名
|
||||
CreatedAt string `p:"createdAt" v:"createdAt@datetime#需为YYYY-MM-DD hh:mm:ss格式"` //
|
||||
commonApi.PageReq
|
||||
commonApi.Author
|
||||
}
|
||||
|
||||
// AppRolesSearchRes 列表返回结果
|
||||
type AppRolesSearchRes struct {
|
||||
g.Meta `mime:"application/json"`
|
||||
commonApi.ListRes
|
||||
List []*model.AppRolesListRes `json:"list"`
|
||||
}
|
||||
|
||||
// AppRolesAddReq 添加操作请求参数
|
||||
type AppRolesAddReq struct {
|
||||
g.Meta `path:"/add" tags:"app角色" method:"post" summary:"app角色添加"`
|
||||
commonApi.Author
|
||||
RoleName string `p:"roleName" v:"required#角色名不能为空"`
|
||||
}
|
||||
|
||||
// AppRolesAddRes 添加操作返回结果
|
||||
type AppRolesAddRes struct {
|
||||
commonApi.EmptyRes
|
||||
}
|
||||
|
||||
// AppRolesAddMenuReq 添加角色时绑定菜单
|
||||
type AppRolesAddMenuReq struct {
|
||||
g.Meta `path:"/addMenu" tags:"app角色" method:"post" summary:"app添加角色的同时绑定菜单"`
|
||||
RoleName string `p:"roleName" v:"required#角色名不能为空" dc:"角色名"` // 角色名字
|
||||
MenuIds []int `p:"menuIds" v:"required#菜单ID不能为空" dc:"菜单ID 数组"` // 菜单ID
|
||||
commonApi.Author
|
||||
}
|
||||
|
||||
type AppRolesAddMenuRes struct {
|
||||
commonApi.EmptyRes
|
||||
}
|
||||
|
||||
// AppRolesEditReq 修改操作请求参数
|
||||
type AppRolesEditReq struct {
|
||||
g.Meta `path:"/edit" tags:"app角色" method:"put" summary:"app角色修改"`
|
||||
commonApi.Author
|
||||
RoleId uint `p:"roleId" v:"required#主键ID不能为空"`
|
||||
RoleName string `p:"roleName" v:"required#角色名不能为空"`
|
||||
MenuIds []int `p:"menuIds" dc:"菜单列表"` // 菜单列表
|
||||
}
|
||||
|
||||
// AppRolesEditRes 修改操作返回结果
|
||||
type AppRolesEditRes struct {
|
||||
commonApi.EmptyRes
|
||||
}
|
||||
|
||||
// AppRolesGetReq 获取一条数据请求
|
||||
type AppRolesGetReq struct {
|
||||
g.Meta `path:"/get" tags:"app角色" method:"get" summary:"获取app角色信息"`
|
||||
commonApi.Author
|
||||
RoleId uint `p:"roleId" v:"required#主键必须"` // 通过主键获取
|
||||
}
|
||||
|
||||
// AppRolesGetRes 获取一条数据结果
|
||||
type AppRolesGetRes struct {
|
||||
g.Meta `mime:"application/json"`
|
||||
*model.AppRolesInfoRes
|
||||
}
|
||||
|
||||
// AppRolesDeleteReq 删除数据请求
|
||||
type AppRolesDeleteReq struct {
|
||||
g.Meta `path:"/delete" tags:"app角色" method:"delete" summary:"删除app角色"`
|
||||
commonApi.Author
|
||||
RoleIds []uint `p:"roleIds" v:"required#主键必须"` // 通过主键删除
|
||||
}
|
||||
|
||||
// AppRolesDeleteRes 删除数据返回
|
||||
type AppRolesDeleteRes struct {
|
||||
commonApi.EmptyRes
|
||||
}
|
||||
|
||||
// 获取一个角色的菜单
|
||||
type AppRolesGetMenuReq struct {
|
||||
g.Meta `path:"/getMenu" tags:"app角色" method:"get" summary:"获取角色的菜单"`
|
||||
commonApi.Author
|
||||
RoleId uint `p:"roleId" v:"required#角色ID不能为空"`
|
||||
}
|
||||
|
||||
type AppRolesGetMenuRes struct {
|
||||
g.Meta `mime:"application/json"`
|
||||
List model.AppRoleDetails
|
||||
}
|
86
api/v1/system/app_user_disable_menus.go
Normal file
86
api/v1/system/app_user_disable_menus.go
Normal file
@ -0,0 +1,86 @@
|
||||
// ==========================================================================
|
||||
// GFast自动生成api操作代码。
|
||||
// 生成日期:2024-05-28 15:11:37
|
||||
// 生成路径: api/v1/system/app_user_disable_menus.go
|
||||
// 生成人:gfast
|
||||
// desc:app用户禁用菜单关联相关参数
|
||||
// company:云南奇讯科技有限公司
|
||||
// ==========================================================================
|
||||
|
||||
package system
|
||||
|
||||
import (
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
commonApi "github.com/tiger1103/gfast/v3/api/v1/common"
|
||||
"github.com/tiger1103/gfast/v3/internal/app/system/model"
|
||||
)
|
||||
|
||||
// AppUserDisableMenusSearchReq 分页请求参数
|
||||
type AppUserDisableMenusSearchReq struct {
|
||||
g.Meta `path:"/list" tags:"app用户禁用菜单关联" method:"get" summary:"app用户禁用菜单关联列表"`
|
||||
UserDisabledMenuId string `p:"userDisabledMenuId"` //
|
||||
UserId string `p:"userId" v:"userId@integer#用户ID需为整数"` //用户ID
|
||||
MenuId string `p:"menuId" v:"menuId@integer#菜单ID需为整数"` //菜单ID
|
||||
CreatedAt string `p:"createdAt" v:"createdAt@datetime#需为YYYY-MM-DD hh:mm:ss格式"` //
|
||||
commonApi.PageReq
|
||||
commonApi.Author
|
||||
}
|
||||
|
||||
// AppUserDisableMenusSearchRes 列表返回结果
|
||||
type AppUserDisableMenusSearchRes struct {
|
||||
g.Meta `mime:"application/json"`
|
||||
commonApi.ListRes
|
||||
List []*model.AppUserDisableMenusListRes `json:"list"`
|
||||
}
|
||||
|
||||
// AppUserDisableMenusAddReq 添加操作请求参数
|
||||
type AppUserDisableMenusAddReq struct {
|
||||
g.Meta `path:"/add" tags:"app用户禁用菜单关联" method:"post" summary:"app用户禁用菜单关联添加"`
|
||||
commonApi.Author
|
||||
UserId int `p:"userId" `
|
||||
MenuId int `p:"menuId" `
|
||||
}
|
||||
|
||||
// AppUserDisableMenusAddRes 添加操作返回结果
|
||||
type AppUserDisableMenusAddRes struct {
|
||||
commonApi.EmptyRes
|
||||
}
|
||||
|
||||
// AppUserDisableMenusEditReq 修改操作请求参数
|
||||
type AppUserDisableMenusEditReq struct {
|
||||
g.Meta `path:"/edit" tags:"app用户禁用菜单关联" method:"put" summary:"app用户禁用菜单关联修改"`
|
||||
commonApi.Author
|
||||
UserDisabledMenuId uint `p:"userDisabledMenuId" v:"required#主键ID不能为空"`
|
||||
UserId int `p:"userId" `
|
||||
MenuId int `p:"menuId" `
|
||||
}
|
||||
|
||||
// AppUserDisableMenusEditRes 修改操作返回结果
|
||||
type AppUserDisableMenusEditRes struct {
|
||||
commonApi.EmptyRes
|
||||
}
|
||||
|
||||
// AppUserDisableMenusGetReq 获取一条数据请求
|
||||
type AppUserDisableMenusGetReq struct {
|
||||
g.Meta `path:"/get" tags:"app用户禁用菜单关联" method:"get" summary:"获取app用户禁用菜单关联信息"`
|
||||
commonApi.Author
|
||||
UserDisabledMenuId uint `p:"userDisabledMenuId" v:"required#主键必须"` //通过主键获取
|
||||
}
|
||||
|
||||
// AppUserDisableMenusGetRes 获取一条数据结果
|
||||
type AppUserDisableMenusGetRes struct {
|
||||
g.Meta `mime:"application/json"`
|
||||
*model.AppUserDisableMenusInfoRes
|
||||
}
|
||||
|
||||
// AppUserDisableMenusDeleteReq 删除数据请求
|
||||
type AppUserDisableMenusDeleteReq struct {
|
||||
g.Meta `path:"/delete" tags:"app用户禁用菜单关联" method:"delete" summary:"删除app用户禁用菜单关联"`
|
||||
commonApi.Author
|
||||
UserDisabledMenuIds []uint `p:"userDisabledMenuIds" v:"required#主键必须"` //通过主键删除
|
||||
}
|
||||
|
||||
// AppUserDisableMenusDeleteRes 删除数据返回
|
||||
type AppUserDisableMenusDeleteRes struct {
|
||||
commonApi.EmptyRes
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user