Jack Huang's Blog


  • 首页

  • 标签

  • 归档

  • 搜索

sequelize-cli使用经验总结

发表于 2018-06-22 | 更新于 2022-10-17

Sequelize 是一个基于 promise 的 Node.js ORM, 目前支持 Postgres, MySQL, SQLite 和 Microsoft SQL Server. 它具有强大的事务支持, 关联关系, 读取和复制等功能[2]. 下面主要介绍如何使用sequelize-cli工具快速构建nodejs的数据访问层。

基于express创建nodejs web应用

基于express建立nodejs的web应用,在此基础上再使用sequlize建立数据访问层和数据库。

1
2
3
4
mkdir myapp
cd myapp
express
npm install

基于sequelize创建数据访问层和数据库

安装sequlize

1
2
npm install --save sequelize
npm install --save mysql2

安装sequelize-cli

首先应安装sequelize-cli工具,可以选择全局安装,也可以选择本地安装。

1
2
npm install --save-dev sequelize-cli
npm install -g sequelize-cli

建立初始的ORM引导框架

使用如下命令在Nodejs Web应用中建立初始的数据访问层。

1
2
3
mkdir db
cd db
../node_modules/.bin/sequelize init

这将创建以下文件夹:

  • config, 包含配置文件,它告诉CLI如何连接数据库
  • models,包含您的项目的所有模型
  • migrations, 包含所有迁移文件
  • seeders, 包含所有种子文件

修改配置文件以连接到数据库管理系统,并创建数据库

在建立模型之前,应先修改config/config.json,以告诉 CLI 如何连接到数据库。config/config.json内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
{
"development": {
"username": "root",
"password": "root",
"database": "database_development",
"host": "127.0.0.1",
"dialect": "mysql",
"pool":{
"max": 5,
"min": 0,
"idle": 10000
},
"timezone": "+08:00",
"define":{
"charset": "utf8",
"dialectOptions":{
"collate": "utf8_general_ci"
}
}
},
"test": {
"username": "root",
"password": "root",
"database": "database_test",
"host": "127.0.0.1",
"dialect": "mysql"
},
"production": {
"username": "root",
"password": "root",
"database": "database_test",
"host": "127.0.0.1",
"dialect": "mysql",
"pool":{
"max": 5,
"min": 0,
"idle": 10000
},
"timezone": "+08:00",
"define":{
"charset": "utf8",
"dialectOptions":{
"collate": "utf8_general_ci"
}
}
}
}

上述配置修改了数据库的字符集,使之能支持中文。当然在创建数据库之前,应配置mysql数据库管理系,使其支持utf8字符集。可执行如下命令查看其支持的字符集,如果其不支持uft字符集,请按照参考链接[3]进行修改。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
mysql> show variables like '%char%';
+--------------------------+---------------------------------------------------------+
| Variable_name | Value |
+--------------------------+---------------------------------------------------------+
| character_set_client | utf8 |
| character_set_connection | utf8 |
| character_set_database | utf8 |
| character_set_filesystem | binary |
| character_set_results | utf8 |
| character_set_server | utf8 |
| character_set_system | utf8 |
| character_sets_dir | C:\Program Files\MySQL\MySQL Server 5.5\share\charsets\ |
+--------------------------+---------------------------------------------------------+
8 rows in set (0.00 sec)

使用如下命令创建数据库:

1
../node_modules/.bin/sequelize db:create

使用如下命令删除数据库:

1
../node_modules/.bin/sequelize db:drop

创建模型

我们将使用 model:generate 命令。 此命令需要两个选项:

  • name, 模型的名称
  • attributes, 模型的属性列表

创建一个名叫 User 的模型:

1
../node_modules/.bin/sequelize model:generate --name User --attributes firstName:string,lastName:string,email:string

这将发生以下事情:

  • 在 models 文件夹中创建了一个 user 模型文件
  • 在 migrations 文件夹中创建了一个名字像 XXXXXXXXXXXXXX-create-user.js 的迁移文件

注意: _Sequelize 将只使用模型文件,它是表描述。另一边,迁移文件是该模型的更改,或更具体的是说 CLI 所使用的表。 处理迁移,如提交或日志,以进行数据库的某些更改。

再创建一个名为Role的模型,它跟User是一对多的关系:

1
../node_modules/.bin/sequelize model:generate --name Role --attributes roleName:string

定义关系

Role和User是一对多的关系,因此需要修改它们的模型定义。
修改models/role.js如下:

1
2
3
4
5
6
7
8
9
10
11
'use strict';
module.exports = (sequelize, DataTypes) => {
var Role = sequelize.define('Role', {
roleName: DataTypes.STRING
}, {});
Role.associate = function(models) {
// associations can be defined here
Role.hasMany(models.User)
};
return Role;
};

修改models/user.js如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
'use strict';
module.exports = (sequelize, DataTypes) => {
var User = sequelize.define('User', {
firstName: DataTypes.STRING,
lastName: DataTypes.STRING,
email: DataTypes.STRING
}, {});
User.associate = function(models) {
// associations can be defined here
User.belongsTo(models.Role, {
onDelete: "NULL",
foreignKey: {
allowNull: false
}
})
};
return User;
};

修改和运行迁移

Role和User是一对多的关系,因此需要修改User迁移文件的定义。
修改migrates/20180622153152-create-user.js如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
'use strict';
module.exports = {
up: (queryInterface, Sequelize) => {
return queryInterface.createTable('Users', {
id: {
allowNull: false,
autoIncrement: true,
primaryKey: true,
type: Sequelize.INTEGER
},
roleId: { // name of the key we're adding
type: Sequelize.INTEGER,
allowNull: false,
references: {
model: 'Roles', // name of Target model
key: 'id', // key in Target model that we're referencing
},
onUpdate: 'CASCADE',
onDelete: 'CASCADE',
},
firstName: {
type: Sequelize.STRING
},
lastName: {
type: Sequelize.STRING
},
email: {
type: Sequelize.STRING
},
createdAt: {
allowNull: false,
type: Sequelize.DATE
},
updatedAt: {
allowNull: false,
type: Sequelize.DATE
}
});
},
down: (queryInterface, Sequelize) => {
return queryInterface.dropTable('Users');
}
};

注意:这里主要给User添加了外键字段,外键字段命名有规则:通常为小写的模型名加Id,即驼峰风格。例如这里加了roleId的外键字段。

直到目前为止,CLI没有将任何东西插入数据库。 刚刚为模型 User和Role创建了必需的模型和迁移文件。 现在要在数据库中实际创建该表,需要运行 db:migrate 命令。

1
../node_modules/.bin/sequelize db:migrate

此命令将执行这些步骤:

  • 将在数据库中创建一个名为 SequelizeMeta 的表。 此表用于记录在当前数据库上运行的迁移
  • 开始寻找尚未运行的任何迁移文件。 这可以通过检查 SequelizeMeta 表。 在这个例子中,它将运行创建的 XXXXXXXXXXXXXX-create-role.js和XXXXXXXXXXXXXX-create-user.js 迁移。
  • 创建一个名为 Roles 的表,其中包含其迁移文件中指定的所有列。
  • 创建一个名为 Users 的表,其中包含其迁移文件中指定的所有列。

除了使用sequelize db:migrate命令创建数据库表之外,可以使用models同步创建或删除数据库表,例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
if (appConfig.dbCreated === false) {
models.sequelize.sync().then(function() {
//保存应用配置
appConfig.dbCreated = true;
fs.writeFile(__dirname + '/../config/appConfig.json', JSON.stringify(appConfig), function(err) {
if (err) return console.error(err);
console.log('app config save is done');
});
Promise.all([
models.T_Role.create({ID:pwdHash.createMd5(Math.random().toString()),Role_Name:'管理员'}),
models.T_User.create({ID:pwdHash.createMd5(Math.random().toString()),Account:appConfig.adminAccount,Password:pwdHash.createHash(appConfig.adminPassword),Audit:true,Avatar:'/avatar/default.jpg'}),
models.T_Role.create({ID:pwdHash.createMd5(Math.random().toString()),Role_Name:'普通用户'})

]).then(function (result){
//console.log(result);
var role1=result[0];
var user1=result[1];
role1.setT_Users(user1).catch(err=>console.log(err));
}).catch(err=>console.log(err));


server.listen(port);
server.on('error', onError);
server.on('listening', onListening);
});
} else {
server.listen(port);
server.on('error', onError);
server.on('listening', onListening);
}

创建种子,生成测试数据

假设我们希望在默认情况下将一些数据插入到几个表中。 例如创建几个用户和角色:

1
2
../node_modules/.bin/sequelize seed:generate --name demo-role
../node_modules/.bin/sequelize seed:generate --name demo-user

这个命令将会在 seeders 文件夹中创建两个种子文件。文件名看起来像是 XXXXXXXXXXXXXX-demo-role.js和XXXXXXXXXXXXXX-demo-user.js,它遵循相同的 up/down 语义,如迁移文件。

现在我们应该编辑这两个文件,将演示角色插入Role表,将演示用户插入User表。修改XXXXXXXXXXXXXX-demo-role.js如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
'use strict';

module.exports = {
up: (queryInterface, Sequelize) => {
return queryInterface.bulkInsert('Roles', [{
roleName: '管理员',
createdAt:new Date(),
updatedAt:new Date()
},{
roleName: '普通用户',
createdAt:new Date(),
updatedAt:new Date()
}], {});

},

down: (queryInterface, Sequelize) => {
return queryInterface.bulkDelete('Roles', null, {});

}
};

修改XXXXXXXXXXXXXX-demo-user.js如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
'use strict';

module.exports = {
up: (queryInterface, Sequelize) => {
return queryInterface.bulkInsert('Users', [{
roleId:1,
firstName: 'John',
lastName: 'Doe',
email: 'demo@demo.com',
createdAt:new Date(),
updatedAt:new Date()
},{
roleId:1,
firstName: 'Jack',
lastName: 'Smith',
email: 'jack@demo.com',
createdAt:new Date(),
updatedAt:new Date()
}], {});

},

down: (queryInterface, Sequelize) => {
return queryInterface.bulkDelete('Users', null, {});
}
};

种子文件修改后,即可使用如下命令将演示数据插入数据库中:

1
../node_modules/.bin/sequelize db:seed:all

工程实践经验总结

在工程实践中,只需使用sequelize-cli创建数据库,生成models即可,然后使用代码直接根据models创建数据库表,并生成测试数据。

参考链接

  1. Sequelize Reference,by sequelizejs.
  2. Sequelize 中文文档,by demopark.
  3. 解决Mysql中文乱码问题, by 最怕认真.
  4. How to set primary key type to UUID via Sequelize CLI,by stackoverflow.
  5. 数据库表主键设计方法,by jackhuang.
  6. Variable,by sequelize.
  7. cascade delete does not work,by sequelize.
  8. Adding foreign key to a table using Sequelize,by Nathan Sebhastian.
  9. Eager Loading - 预先加载,by demopark.

Visual_Studio_Community_2017离线安装

发表于 2018-06-21 | 更新于 2024-05-22

Visual Studio 2017版本采用模块化安装方式,根据用户选择模块按需安装。这种方式在用户能够上网的情况下十分方便,但在离线情况下就无法使用。下面介绍在离线情况下如何打包安装Visual Studio community 2017的方法。

下载VS2017社区版安装包获取程序

到官网下载最新版本的vs_community_xxxxxx.exe的下载器。

运行命令下载所需安装包

运行如下命令获取VS2017的工作负载:

1
.\vs_community__208176120.1522073957.exe --layout e:\vs2017_offline --lang zh-CN --add Microsoft.VisualStudio.Workload.CoreEditor --add Microsoft.VisualStudio.Workload.NativeDesktop --includeRecommended --includeOptional

可从参考链接[2]中获取工作负载ID。如需更多语言包或者更多安装指令,请参考官方离线安装命令指南,即参考链接[3]。

VS2017社区版 30天许可证过期解决方法

解决方法步骤如下:

  1. 外网电脑打开Vistual Studio Community2017。
  2. 在许可证过期弹窗中登陆即可自动下载许可证完成激活。许可证下载路径(C:\Users\user\AppData\Local\Microsoft\VSCommon\OnlineLicensing)
  3. 拷贝外网中的OnlineLicensing文件夹到内网电脑相应路径下(C:\Users\user\AppData\Local\Microsoft\VSCommon)
  4. 打开内网电脑Vistual Studio Community2017将不会再弹出许可证到期窗口。

VS2017控制台输出中文乱码问题

请参考:

  • VS2017控制台输出中文乱码 ,怎么设置编码格式(utf-8)?
  • 修改cmd控制台默认代码页编码的几种方法【GBK、UTF-8】
  • VS2017新建windows控制台程序打印中文乱码问题
  • VS2019控制台输出中文乱码问题已解决

VS2017中.ipch文件删除问题

.ipch文件和.sdf文件是Visual Studio用来保存预编译的头文件 和Intellisense 用的,删除这些文件对于工程的开发完全没有影响。具体方法请参考:

  • VisualStudio 产生的.ipch文件可以删除吗?

参考链接

  1. Create an offline installation of Visual Studio, by microsoft
  2. Visual Studio Community 组件目录, by microsoft
  3. Use command-line parameters to install Visual Studio, by microsoft
  4. Vistual Studio Community 2017 30天许可证过期,by 井底一蛤蟆.
  5. VS2017控制台输出中文乱码 ,怎么设置编码格式(utf-8)?,by 小凡1991.
  6. VisualStudio 产生的.ipch文件可以删除吗?,by Jacob-xyb.

Node_C++_Addon插件编写方法

发表于 2018-06-20 | 更新于 2019-10-03

最近研究在Nodejs中调用DLL,上网搜索发现主要有两种方法:

  • 使用Nodeffi调用C风格接口的DLL,但是无法调用C++风格导出类的DLL。
  • 使用Nodejs C++ Addon 插件,该方法可直接与C++代码交互,理论上可以调用C++风格导出类的DLL。

下面研究Nodejs Addon C++插件的编写方法。

什么是Nodejs C++ Addon

Node.js插件(Addons)是C/C++编写的动态链接对象,这些对象可以被Node.js的require()函数引用,并可以像普通的Node.js模块一样使用。Addons主要用于提供一个Node.js中运行的JavaScript和C/C++库之间的接口。

插件(Addons)是动态链接的共享对象,它提供了C/C++类库的调用能力。实现插件的方法比较复杂,涉及到以下元组件及API:

  • V8:C++库,Node.js用于提供JavaScript执行环境。V8提供了对象创建、函数调用等执行机制,V8相关API包含在了v8.h头文件中(位于Node.js源码树的deps/v8/include/v8.h),也可以查看在线文档。
  • libuv:C库,实现了Node.js中的事件循环、工作线程及在不同平台中异步行为的相关功能。也可以做为是一个跨平台的抽象库,提供了简单的、类POSIX的对主要操作系统的常见系统任务功能,如:与文件系统、套接字、计时器、系统事件的交互等。libuv还提供了一个类pthreads的线程池抽象对象,可用于更复杂的、超越标准事件循环的异步插件的控制功能。
  • 内部Node.js库:Node.js自身提供了一定义数量的C/C++API的插件可以使用 - 其中最重要的可能是node::ObjectWrap类
  • Node.js静态链接库:Node.js自身还包含了一部分静态链接库,如OpenSSL。这些位于Node.js源码树的deps/目录下,只有V8和OpenSSL提供了符号出口,可以供Node.js和基它插件所使用。详见Node.js依赖链接

Node Addon插件编写方法

Node Addon插件的编写需要解决两个关键问题:

  • 当数据流向 javaScript -> C++时,如何将javascript类型数据包装成C++类型数据,供C++代码使用。
  • 当数据流向 C++ -> JavaScript时,如何将C++类型数据包装成JavaScript类型数据,供JavaScript代码使用。

这两个关键问题的分析请参见淘宝前端团队成员发表的文章“Node.js 和 C++ 之间的类型转换[3]”。解决这两个关键问题后,Node Addon插件编写难度就不大了。

Node Addon插件调用C++导出类DLL方法测试

现有一个采用成熟方法导出类接口的DLL[4],如何在Node Addon插件中调用该DLL呢?下面nodejs官网Node Addon插件例子Factory of wrapped objects为例进行讲解。

Factory of wrapped objects例子在Addon插件中包装了一个MyObject类,现在就在MyObject类调用DLL导出类的接口方法。修改MyObject.h代码,增加DLL导出类接口方法,增加类接口成员变量IExport和DLL句柄变量hDll,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
// myobject.h
#ifndef MYOBJECT_H
#define MYOBJECT_H

#include <node.h>
#include <iostream>
#include <windows.h>
#include <node_object_wrap.h>
#include "MatureApproach.h"

namespace demo {
typedef IExport*(*TYPE_fnCreateExportObj) (void);//定义函数指针
typedef void(*TYPE_fnDestroyExportObj) (IExport*);//定义函数指针

class MyObject : public node::ObjectWrap {
public:
static void Init(v8::Isolate* isolate);
static void NewInstance(const v8::FunctionCallbackInfo<v8::Value>& args);

private:
explicit MyObject(double value = 0);
~MyObject();

static void New(const v8::FunctionCallbackInfo<v8::Value>& args);
static void PlusOne(const v8::FunctionCallbackInfo<v8::Value>& args);
static void Hi(const v8::FunctionCallbackInfo<v8::Value>& args);
static void Test(const v8::FunctionCallbackInfo<v8::Value>& args);
static v8::Persistent<v8::Function> constructor;
HMODULE hDll;
IExport* pExport;
double value_;
};

} // namespace demo

#endif

接下来将在MyObject类的构造函数中动态加载DLL,创建DLL导出类对象,在析构函数中析构DLL导出类对象,动态卸载DLL,在MyObject成员方法中调用DLL导出类方法,代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
// myobject.cc
#include <node.h>
#include "myobject.h"

namespace demo {

using v8::Context;
using v8::Function;
using v8::FunctionCallbackInfo;
using v8::FunctionTemplate;
using v8::Isolate;
using v8::Local;
using v8::Number;
using v8::Object;
using v8::Persistent;
using v8::String;
using v8::Value;

Persistent<Function> MyObject::constructor;

MyObject::MyObject(double value) : value_(value){
MyObject::hDll = LoadLibrary("MatureApproach.dll"); //加载动态链接库DllDemo.dll文件;
TYPE_fnCreateExportObj fnCreateExportObj = (TYPE_fnCreateExportObj)GetProcAddress(MyObject::hDll, "CreateExportObj");
MyObject::pExport = fnCreateExportObj();
}

MyObject::~MyObject() {
TYPE_fnDestroyExportObj fnDestroyExportObj = (TYPE_fnDestroyExportObj)GetProcAddress(MyObject::hDll, "DestroyExportObj");
fnDestroyExportObj(MyObject::pExport);
FreeLibrary(MyObject::hDll);
}

void MyObject::Init(Isolate* isolate) {
// Prepare constructor template
Local<FunctionTemplate> tpl = FunctionTemplate::New(isolate, New);
tpl->SetClassName(String::NewFromUtf8(isolate, "MyObject"));
tpl->InstanceTemplate()->SetInternalFieldCount(3);

// Prototype
NODE_SET_PROTOTYPE_METHOD(tpl, "plusOne", PlusOne);
NODE_SET_PROTOTYPE_METHOD(tpl, "hi", Hi);
NODE_SET_PROTOTYPE_METHOD(tpl, "test", Test);

constructor.Reset(isolate, tpl->GetFunction());
}

void MyObject::New(const FunctionCallbackInfo<Value>& args) {
Isolate* isolate = args.GetIsolate();

if (args.IsConstructCall()) {
// Invoked as constructor: `new MyObject(...)`
double value = args[0]->IsUndefined() ? 0 : args[0]->NumberValue();
MyObject* obj = new MyObject(value);
obj->Wrap(args.This());
args.GetReturnValue().Set(args.This());
} else {
// Invoked as plain function `MyObject(...)`, turn into construct call.
const int argc = 1;
Local<Value> argv[argc] = { args[0] };
Local<Function> cons = Local<Function>::New(isolate, constructor);
Local<Context> context = isolate->GetCurrentContext();
Local<Object> instance =
cons->NewInstance(context, argc, argv).ToLocalChecked();
args.GetReturnValue().Set(instance);
}
}

void MyObject::NewInstance(const FunctionCallbackInfo<Value>& args) {
Isolate* isolate = args.GetIsolate();

const unsigned argc = 1;
Local<Value> argv[argc] = { args[0] };
Local<Function> cons = Local<Function>::New(isolate, constructor);
Local<Context> context = isolate->GetCurrentContext();
Local<Object> instance =
cons->NewInstance(context, argc, argv).ToLocalChecked();

args.GetReturnValue().Set(instance);
}

void MyObject::PlusOne(const FunctionCallbackInfo<Value>& args) {
Isolate* isolate = args.GetIsolate();

MyObject* obj = ObjectWrap::Unwrap<MyObject>(args.Holder());
obj->value_ += 1;

args.GetReturnValue().Set(Number::New(isolate, obj->value_));
}

void MyObject::Hi(const FunctionCallbackInfo<Value>& args) {
Isolate* isolate = args.GetIsolate();

MyObject* obj = ObjectWrap::Unwrap<MyObject>(args.Holder());
Local<String> str=v8::String::NewFromUtf8(isolate, obj->pExport->Hi().data());

args.GetReturnValue().Set(str);
}

void MyObject::Test(const FunctionCallbackInfo<Value>& args) {
Isolate* isolate = args.GetIsolate();

MyObject* obj = ObjectWrap::Unwrap<MyObject>(args.Holder());
Local<String> str=v8::String::NewFromUtf8(isolate, obj->pExport->Test().data());

args.GetReturnValue().Set(str);
}

} // namespace demo

参考链接

  1. Node.js C/C++插件(Addons), by IT笔录
  2. Node.js v8.16.1 Documentation,by nodejs
  3. type-casts-between-node-and-cpp,by 淘宝前端团队
  4. DLL导出类和函数,by jackhuang

如何在Vistual_Studio中调试Node_Addon_C++插件

发表于 2018-06-18 | 更新于 2019-09-26

最近研究如何在Vistual Studio 2017社区版中调试Node addon C++插件,遇到很多问题,花费了不少时间,为防止遗忘,将方法记录下来。

准备工作

  1. 安装VS2017社区版
  2. 安装python 2.7
  3. 安装Nodejs 8.11.3 LTS
  4. 下载Nodejs 8.11.3 LTS源码

安装配置环境

  1. 安装node-gyp和nan
    node-gyp和nan用于编译生成node addon 插件。

    1
    2
    npm install -g node-gyp
    npm install -g nan
  2. 编译nodejs源码
    在nodejs源码解压后文件夹中执行如下命令:

    1
    .\vcbuild.bat debug x64
  3. 创建nodejs addon c++插件示例工程
    使用如下命名生成调试用的vs工程:

    1
    node-gyp configure rebuild --nodedir="D:\project\cpp_project\node-v8.11.3" --debug
  4. 修改测试用的index.js,使其调用上一步骤生成的调试用的插件

    //该js调用生成的插件
    var addon = require('./build/Debug/max');
    var y=addon.max(2,3);
    console.log(y);
  5. 使用VS2017打开插件示例工程的build文件夹下node-gyp生成的解决方案。右键单击工程,在上下文菜单中点击属性,配置其调试命令和命令参数,如下图所示。

    工程调试配置

  6. 设置断点进行调试

参考链接

  1. 如何在VS2015中搭建可以写node.js的C++Addon的环境, by Zmyths.
  2. how-to-debug-nodejs-addons-in-visual-studio,by cvtalks.
  3. Vistual Studio Community 2017 30天许可证过期,by 井底一蛤蟆.

DLL导出类和函数

发表于 2018-06-15 | 更新于 2025-09-13

最近研究在DLL中导出类,探寻最佳的DLL导出类的方法和技术。把整个过程记录一下,防止遗忘。

基础知识

动态链接库

动态链接库(英语:Dynamic-link library,缩写为DLL)是微软公司在微软视窗操作系统中实现共享函数库概念的一种实现方式。这些库函数的扩展名是.DLL、.OCX(包含ActiveX控制的库)或者.DRV(旧式的系统驱动程序)。

所谓动态链接,就是把一些经常会共用的代码(静态链接的OBJ程序库)制作成DLL档,当可执行文件调用到DLL档内的函数时,Windows操作系统才会把DLL档加载存储器内,DLL档本身的结构就是可执行档,当程序有需求时函数才进行链接。通过动态链接方式,存储器浪费的情形将可大幅降低。静态链接库则是直接链接到可执行文件。

编写方法

使用DLL导出C函数或全局变量很简单,具体代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 下列 ifdef 块是创建使从 DLL 导出更简单的
// 宏的标准方法。此 DLL 中的所有文件都是用命令行上定义的 DLLDEMO_EXPORTS
// 符号编译的。在使用此 DLL 的
// 任何其他项目上不应定义此符号。这样,源文件中包含此文件的任何其他项目都会将
// DLLDEMO_API 函数视为是从 DLL 导入的,而此 DLL 则将用此宏定义的
// 符号视为是被导出的。

#ifdef DLLDEMO_EXPORTS
#define DLLDEMO_API __declspec(dllexport)
#else
#define DLLDEMO_API __declspec(dllimport)
#endif

extern "C" extern DLLDEMO_API int nDllDemo;

//不使用extern "C"将导致函数名字改编
DLLDEMO_API int fnDllDemo(int);

extern "C" DLLDEMO_API int fnExternCDllDemo(int);

运行时通知DLL进程/线程加载

进程/线程加载时,可以通过DllMain函数通知DLL相关信息,提供对应处理的机会。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
BOOL WINAPI DLLMain(HINSTANCE hinstDLL,DWORD fdwReason,LPVOID fImpLoad)
{
switch(fdwReason)
{
case DLL_PROCESS_ATTACH:
//当这个DLL第一次被映射到了这个进程的地址空间时。DLLMain函数的返回值为FALSE,说明DLL的初始化没有成功,系统就会终结整个进程,去掉所有文件映象,之后显示一个对话框告诉用户进程不能启动。
break;
case DLL_THREAD_ATTACH:
//一个线程被创建,新创建的线程负责执行这次的DllMain函数。系统不会让进程已经存在的线程以DLL_THREAD_ATTACH的值来调用DllMain函数。主线程永远不会以DLL_THREAD_ATTACH的值来调用DllMain函数。系统是顺序调用DllMain函数的,一个线程执行完DllMain函数才会让另外一个线程执行DllMain函数。
break;
case DLL_THREAD_DETACH:
//如果线程调用了ExitThread来结束线程(线程函数返回时,系统也会自动调用ExitThread)。线程调用了TerminateThread,系统就不会用值DLL_THREAD_DETACH来调用所有DLL的DllMain函数。
break;
case DLL_PROCESS_DETACH:
//这个DLL从进程的地址空间中解除映射。如果进程的终结是因为调用了TerminateProcess,系统就不会用DLL_PROCESS_DETACH来调用DLL的DllMain函数。这就意味着DLL在进程结束前没有机会执行任何清理工作。
break;
}
return(TRUE);
}

DLL的静态调用方法

采用静态调用方法,DLL最终将打包到生成的EXE中。静态调用方法步骤如下[2]:

  1. 把你的youApp.DLL拷到你目标工程(需调用youApp.DLL的工程)的Debug目录下;
  2. 把你的youApp.lib拷到你目标工程(需调用youApp.DLL的工程)目录下;
  3. 把你的youApp.h(包含输出函数的定义)拷到你目标工程(需调用youApp.DLL的工程)目
    录下;
  4. 打开你的目标工程选中工程,选择Visual C++的Project主菜单的Settings菜单;
  5. 执行第4步后,VC将会弹出一个对话框,在对话框的多页显示控件中选择Link页。然
    后在Object/library modules输入框中输入:youApp.lib
  6. 选择你的目标工程Head Files加入:youApp.h文件;
  7. 最后在你目标工程(*.cpp,需要调用DLL中的函数)中包含你的:#include “youApp.h”

DLL的动态调用方法

动态调用DLL的步骤:

  1. 创建一个函数指针,其指针数据类型要与调用的DLL引出函数相吻合。
  2. 通过Win32 API函数LoadLibrary()显式的调用DLL,此函数返回DLL的实例句柄。
  3. 通过Win32 API函数GetProcAddress()获取要调用的DLL的函数地址, 把结果赋给自定义函数的指针类型。
  4. 使用函数指针来调用DLL函数。
  5. 最后调用完成后,通过Win32 API函数FreeLibrary()释放DLL函数。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
int main()
{
HMODULE hModule = LoadLibrary(_T("DllDemo.dll"));
typedef int(*TYPE_fnDllDemo) (int);//定义函数指针
typedef int(*TYPE_fnExternCDllDemo) (int);//定义函数指针
//创建类对象
CDllDemo* pCDllDemo = (CDllDemo*)malloc(sizeof(CDllDemo));

TYPE_fnDllDemo fnDllDemo = (TYPE_fnDllDemo)GetProcAddress(hModule, "?fnDllDemo@@YAHH@Z");
int *nDllDemo = (int *)GetProcAddress(hModule, "nDllDemo");
TYPE_fnExternCDllDemo fnExternCDllDemo = (TYPE_fnExternCDllDemo)GetProcAddress(hModule, "fnExternCDllDemo");

if (pCDllDemo != NULL)
// printf("pCDllDemo->Max(32,42) = %d\n", pCDllDemo->Max(32, 42));//Dll导出类的调用太麻烦,因为DLL本来就是为C函数服务设计的。
if (fnDllDemo != NULL)
printf("fnDllDemo(32) = %d\n", fnDllDemo(32));
if (nDllDemo != NULL)
printf("*nDllDemo = %d\n", *nDllDemo);
if (fnExternCDllDemo != NULL)
printf("fnExternCDllDemo(22) = %d\n", fnExternCDllDemo(22));
_tsystem(_T("pause"));
FreeLibrary(hModule);
return 0;
}

COM技术

COM主要是一套给C/C++用的接口,当然为了微软的野心,它也被推广到了VB、Delphi以及其他一大堆奇奇怪怪的平台上。它主要为了使用dll发布基于interface的接口。我们知道dll的接口是为了C设计的,它导出的基本都是C的函数,从原理上来说,将dll加载到内存之后,会告诉你一组函数的地址,你自己call进去就可以调用相应的函数[3]。

但是对于C++来说这个事情就头疼了,现在假设你有一个类,我们知道使用一个类的第一步是创建这个类:new MyClass()。这里直接就出问题了,new方法通过编译器计算MyClass的大小来分配相应的内存空间,但是如果库升级了,相应的类可能会增加新的成员,大小就变了,那么使用旧的定义分配出来的空间就不能在新的库当中使用。

要解决这问题,我们必须在dll当中导出一个CreateObject的方法,用来代替构造函数,然后返回一个接口。然而,接口的定义在不同版本当中也是有可能会变化的,为了兼容以前的版本同时也提供新功能,还需要让这个对象可以返回不同版本的接口。接口其实是一个只有纯虚函数的C++类,不过对它进行了一些改造来兼容C和其他一些编程语言。

在这样改造之后,出问题的还有析构过程~MyClass()或者说delete myClass,因为同一个对象可能返回了很多个接口,有些接口还在被使用,如果其中一个被人delete了,其他接口都会出错,所以又引入了引用计数,来让许多人可以共享同一个对象。

其实到此为止也并不算是很奇怪的技术,我们用C++有的时候也会使用Factory方法来代替构造函数实现某些特殊的多态,也会用引用计数等等。COM技术的奇怪地方在于微软实在是脑洞太大了,它们构造了一个操作系统级别的Factory,规定所有人的Interface都统一用UUID来标识,以后想要哪个Interface只要报出UUID来就行了。这样甚至连链接到特定的dll都省了。

这就好比一个COM程序员,只要他在Windows平台上,调用别的库就只要首先翻一下魔导书,查到了一个用奇怪文字写的“Excel = {xxx-xxx-xxxx…}”的记号,然后它只要对着空中喊一声:“召唤,Excel!CoCreateInstance, {xxx-xxx-xxxx…}”然后呼的从魔法阵里面窜出来了一个怪物,它长什么样我们完全看不清,因为这时候它的类型是IUnknow,这是脑洞奇大无比的微软为所有接口设计的一个基类。

我们需要进一步要求它变成我们能控制的接口形态,于是我们再喊下一条指令:“变身,Excel 2003形态!QueryInterface, {xxx-xxx-xxxx…}”QueryInterface使用的是另一个UUID,用来表示不同版本的接口。于是怪物就变成了我们需要的Excel 2003接口,虽然我们不知道它实际上是2003还是2007还是更高版本。等我们使唤完这只召唤兽,我们就会对它说“回去吧,召唤兽!Release!”但是它不一定听话,因为之前给它的命令也许还没有执行完,它会忠诚地等到执行完再回去,当然我们并不关心这些细节。

微软大概会觉得自己设计出了软件史上最完美的二进制接口,从今以后所有的第三方库都可以涵盖在这套接口之下。然而历史的车轮是无情的,它碾过那些自以为是的人的速度总是会比想象的更快。Java的直接基于类的接口被广泛应用,开发使用起来远远来的简单,即便偶尔出点问题大家也都想办法解决了,事实证明程序员并不愿意花10倍的编写代码的时间来解决二进制库的版本兼容问题,他们更愿意假装没看见。很快微软也抄了一个.NET托管dll的方案出来,于是纯的二进制接口COM就慢慢被抛弃了。

COM,OLE,ActiveX,OCX,VBScript,历史不会忘记你们的,如果历史忘了,我替历史记住你们。安息吧。

DLL导出类

借鉴COM技术,这里直接给出DLL到处类的成熟方法,可有效避免DLL地狱问题。具体结构为:

导出类是一个派生类,派生自一个抽象类——都是纯虚函数。使用者需要知道这个抽象类的结构。DLL最少只需要提供一个用于获取类对象指针的接口。使用者跟DLL提供者共用一个抽象类的头文件,使用者依赖于DLL的东西很少,只需要知道抽象类的接口,以及获取对象指针的导出函数,对象内存空间的申请是在DLL模块中做的,释放也在DLL模块中完成,最后记得要调用释放对象的函数。

这种方式比较好,通用,产生的DLL没有特定环境限制。借助了C++类的虚函数。一般都是采用这种方式。除了对DLL导出类有好处外,采用接口跟实现分离,可以使得工程的结构更清晰,使用者只需要知道接口,而不需要知道实现。

具体代码如下:

  1. DLL导出类

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    //DLL导出类头文件MatureApproach.h,与DLL使用者共享
    #ifdef MATUREAPPROACH_EXPORTS
    #define MATUREAPPROACH_API __declspec(dllexport)
    #else
    #define MATUREAPPROACH_API __declspec(dllimport)
    #endif

    class IExport
    {
    public:
    virtual void Hi() = 0;
    virtual void Test() = 0;
    virtual void Release() = 0;
    };


    extern "C" MATUREAPPROACH_API IExport* _stdcall CreateExportObj();
    extern "C" MATUREAPPROACH_API void _stdcall DestroyExportObj(IExport* pExport);

    //DLL导出接口函数的实现MatureApproach.cpp
    #include "stdafx.h"
    #include "MatureApproach.h"
    #include "ExportClassImpl.h"

    BOOL APIENTRY DllMain( HMODULE hModule,
    DWORD ul_reason_for_call,
    LPVOID lpReserved
    )
    {
    switch (ul_reason_for_call)
    {
    case DLL_PROCESS_ATTACH:
    case DLL_THREAD_ATTACH:
    case DLL_THREAD_DETACH:
    case DLL_PROCESS_DETACH:
    break;
    }
    return TRUE;
    }

    MATUREAPPROACH_API IExport* APIENTRY CreateExportObj()
    {
    return new ExportImpl;
    }


    //这里不能直接delete pExport,因为没有把IExport的析构函数定义为虚函数
    MATUREAPPROACH_API void APIENTRY DestroyExportObj(IExport* pExport)
    {
    pExport->Release();
    }
  2. DLL导出类的具体实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
//DLL导出类头文件ExportClassImpl.h
#include "MatureApproach.h"

class ExportImpl : public IExport
{
public:
virtual void Hi();
virtual void Test();
virtual void Release();
~ExportImpl();
private:
};

//DLL导出类的实现ExportClassImpl.cpp
#include "stdafx.h"
#include "ExportClassImpl.h"

void ExportImpl::Hi()
{
wcout << L"Hello World" << endl;
}

void ExportImpl::Test()
{
wcout << L"Hi cswuyg" << endl;
}

void ExportImpl::Release()
{
delete this;
}

ExportImpl::~ExportImpl()
{
cout << "Release OK" << endl;
}

Dll导出C++类的3种方式

Using pure C (纯C语言方式)

这种方式类似与Win32的窗口句柄,用户将句柄作为参数传递给函数,并对对象执行各种操作。存在以下缺点:

  1. 调用创建对象函数的时候编译器无法判断类型是否匹配
  2. 需要手动调用Release函数,一旦忘记则会造成内存泄露
  3. 如果导出的函数的参数支持除基本数据类型以外的其他类型的参数(例如:class),则也得为这些类型提供接口。

Using a regular C++ class (C++直接导出类)

缺点:

  1. 这种方式虽然简单易用,但是局限性很大,而且后期维护会很麻烦,除了导出的东西太多、使用者对类的实现依赖太多之外,还有其它问题:必须保证使用同一种编译器。导出类的本质是导出类里的函数,因为语法上直接导出了类,没有对函数的调用方式、重命名进行设置,导致了产生的dll并不通用。
  2. Dll地狱问题:

Using an abstract C++ interface (使用抽象接口方式)

C++抽象接口(仅包含纯虚函数且不包含数据成员的C++类)同时兼顾以下两个方面:与对象无关的纯净接口,以及方便的的面向对象的调用方式。

推荐使用该种方式导出类库。

优点:

  • 导出的C++类可以通过抽象接口与任何C++编译器一起使用。
  • DLL的C运行时库和客户端彼此独立。因为资源获取和释放完全发生在DLL模块内部,客户端不受DLL内部改变的影响。
  • 实现了真正的模块分离。可以重新设计和重建生成的DLL模块,而不会影响项目的其余部分。
  • 如果需要,可以将DLL模块轻松转换为成熟的COM模块。

缺点:

  • 创建新对象实例并将其删除需要显式函数调用。但是,智能指针可以解决。

  • 抽象接口方法不能返回或接受常规C++对象作为参数。它是A内置类型(如int,double,char*等)或另一抽象接口。它与C接口的限制相同。

参考链接

  1. 动态链接库, by wikipedia
  2. C++调用DLL有两种方法——静态调用和动态调用,by 特洛伊-Micro
  3. 怎么通俗的解释COM组件?, by 灵剑.
  4. 编写DLL所学所思(2)——导出类,by 烛秋.
  5. Visibility,by Niall Douglas.
  6. Linux 編譯 shared library 的方法和注意事項,by fcamel.
  7. GCC制作Library–shared部分相当不错,by kk.
  8. [原创]Dll导出C++类的3种方式(多干货) ,by Jmsrwt.
  9. C++类库开发详解,by 奔跑的小河.
  10. C++ DLL导出类 知识大全,by 三小.
  11. std::vector needs to have dll-interface to be used by clients of class ‘X warning,by stackoverflow.
  12. C++的dll库,采用动态调用更好还是静态调用更好?,by Eleven.

Cesium卫星地图和高程数据切片经验总结

发表于 2018-06-13 | 更新于 2023-03-19

最近研究在Cesium中调用自定义的图像和地形,不借助任何GIS系统(如Arcgis、天地图等),方便在网络不好或离线的情况下使用。下面对整个过程进行总结。

基础知识

地图服务提供方法

目前互联网地图服务商提供的地图服务分成两种[3]:

  • 图像瓦片地图服务
    这种方法目前最常见,其提供的是图片格式的瓦片,在客户端将拼接成真正的地图。
  • 矢量瓦片地图服务
    这种方法正在发展,其提供矢量的瓦片数据,然后在客户端进行渲染,是今后的发展方向。

地图服务在使用过程中需要获取特定经纬度所在区域的瓦片和获取瓦片上像素点对应的经纬度,因此需要进行经纬度坐标与瓦片坐标、像素坐标的相互转换。

经纬度与瓦片编号互换

互联网地图的经纬度坐标与瓦片坐标相互转换只与该地图商的墨卡托投影和瓦片编号的定义有关,跟地图商采用的大地坐标系标准无关。

经纬度与瓦片像素互换

转换方式与地图商采用的大地坐标系有关。国际标准的经纬度坐标是WGS84。Open Street Map、外国版的Google Map都是采用WGS84;高德地图使用的坐标系是GCJ-02;百度地图使用的坐标系是BD-09。

地图投影

地图投影,是指按照一定的数学法则将地球椭球面上的经纬网转换到平面上,使地面的地理坐标与平面直角坐标建立起函数关系。这是绘制地图的数学基础之一。由于地球是一个不可展的球体,使用物理方法将其展平会引起褶皱、拉伸和断裂,因此要使用地图投影实现由曲面向平面的转化。

麦卡托投影法

麦卡托投影法 (Mercator projection),又称麦卡托投影法、正轴等角圆柱投影,是一种等角的圆柱形地图投影法。本投影法得名于法兰德斯出身的地理学家杰拉杜斯·麦卡托,他于1569年发表长202公分、宽124公分以此方式绘制的世界地图。在以此投影法绘制的地图上,经纬线于任何位置皆垂直相交,使世界地图可以绘制在一个长方形上。由于可显示任两点间的正确方位,航海用途的海图、航路图大都以此方式绘制。在该投影中线型比例尺在图中任意一点周围都保持不变,从而可以保持大陆轮廓投影后的角度和形状不变(即等角);但麦卡托投影会使面积产生变形,极点的比例甚至达到了无穷大。

墨卡托投影示意图

图1 墨卡托投影示意图
各大地图服务商大都采用了Web Mercator进行投影,瓦片坐标系的不同主要是投影截取的地球范围不同、瓦片坐标起点不同[3]。

瓦片切割和瓦片坐标

对于经过墨卡托投影为平面的世界地图,在不同的地图分辨率(整个世界地图的像素大小)下,通过切割的方式将世界地图划分为像素为256px/256px的地图单元,划分成的每一块地图单元称为地图瓦片。其特定如下:

  • 具有唯一的瓦片等级(Level)和瓦片坐标编号(tileX, tileY)。
  • 瓦片分辨率为256*256。
  • 最小的地图等级是0,此时世界地图只由一张瓦片组成。
  • 瓦片等级越高,组成世界地图的瓦片数越多,可以展示的地图越详细。
  • 某一瓦片等级地图的瓦片是由低一级的各瓦片切割成的4个瓦片组成,形成了瓦片金字塔。

瓦片金字塔

过程总结

将自定义图像和地形以Web形式发布后,在cesium中调用自定义图像和地形很简单,代码如下,关键是图像和地形的获取和处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
var tmsImageryProvider=Cesium.createTileMapServiceImageryProvider({
url:'http://localhost:8080/test_tms_tiles'
});

var terrainProvider=new Cesium.CesiumTerrainProvider({
url:'http://localhost:8080/terrain_tiles'
});
var viewer = new Cesium.Viewer('cesiumContainer', {
imageryProvider : tmsImageryProvider,
terrainProvider: terrainProvider,
baseLayerPicker: false,
geocoder: false,
shadows: false,
shouldAnimate: true
})
// 摄像头使用默认姿态在指定高度指向指定位置
viewer.camera.setView({
destination : Cesium.Cartesian3.fromDegrees(108.60, 34.15, 6000.0)
});

卫星图像的获取和处理

卫星图像获取可以使用一些相关软件,如水经注、LocaSpaceViewer、SXearth等。其原理通常是从一些地图服务商下载地图瓦片后重新拼接,再分割成对应格式的瓦片。拼接的图像最好输出为tif格式,因为tif格式可以在头部包含一些信息,例如地理坐标等信息,以方便后续的瓦片切割,以及被GIS系统直接识别导入。为使切割后的瓦片能够被Cesium直接调用,应采用标准TMS方式。

通常卫星图像下载软件提供多种瓦片分割方式,如果能直接进行标准TMS瓦片分割最好不过,如果不可以进行标准TMS分割,则可使用GDAL。

GDAL是地理空间数据抽象库的简称,是一个地理空间数据的格式转换及处理工具。文章最后将详细介绍其安装使用方法。使用GDAL进行卫星图像瓦片分割主要使用gdal2tiles.py工具。具体步骤如下:

  1. 选取之前下载拼接的最大分辨率的tif卫星图像
  2. 使用如下命令切割卫星图片,将自动生成瓦片金字塔。
    1
    gdal2tiles.py <image> <tilesdir>

或者参考瓦片底图:TIF影像金字塔切片处理及加载。

卫星图像蓝边的处理

卫星图像在浏览的过程中有可能会出现蓝边,其原因是瓦片下载过程中的边界没有处理好。解决思路是严格按照瓦片的经纬度设定边界。可通过google-maps-coordinates-tile-bounds-projection查询特定级别瓦片的各个顶点的经纬度。

地形的获取和处理

Cesium支持多种地形provider来接收地形数据瓦片[8]:

  1. Cesium Terrain Server——高分辨率的全球地形数据,支持地形光照和水流效果。地形瓦片提交给客户端的数据的格式用的是quantized-mesh v1.0。
  2. Esri ArcGIS Image Server——从Esri影像服务里的高度图中产生地形数据集。详情见Cesium里的ArcGisImageServerTerrainProvider.
  3. VR-TheWorld Server——从一个VR-TheWorld服务里的高度图中产生地形数据。它们的托管服务器有全球90米的数据,包括深度测量。
  4. Ellipsoid——是Cesium默认的地形provider,是一个光滑的椭球面,没有现实的地形,地形高度为0。

常用的CesiumTerrainProvider支持两种格式的地形:一种是quantized-mesh格式的地形数据,另一种是基于高度图技术的DEM。前一种技术是Cesium独有不开放,因此将使用DEM数据在Cesium中渲染地形。

DEM数据是数字高程模型,是描述每个点位的高程数据,没有其他附加信息[9]。DEM数据在ArcGIS中打开只能看到是灰度图。通过相关软件可直接下载各大地图服务商的高程数据,但没有在Cesium中加载成功。下面介绍一种经过验证可在Cesium中成功加载地形的方法:

  1. 下载地形数据。可以从地理空间数据云下载地形数据。注册登录后,在首页选择“DEM 数字高程数据”,在选择“GDEMV2 30M 分辨率数字高程数据”。
  2. 合并地形数据。使用GDAL下的gdal_merge.py工具将下载的地形数据合并成一个Tif文件。
    1
    gdal_merge.py -o out.tif input1.tif input2.tif
  3. 切割地形数据。使用牛人制作的gdal2srtmtiles.py脚本分割地形数据。编辑gdal2srtmtiles.py,在最后设置高程文件和输出目录。参考链接工具gdal2srtmtiles的安装使用。或者参考地形数据:TIF地形转terrain格式。
  4. 发布地形服务。把terrain_tiles直接放到与 Cesium同端口的Tomcat或IIS站点。
    下(不能跨域),即可发布服务。 需要在Web Server中增加配置 .terrain文件的 Content-Type设为 application/octet-stream,同时拷贝“覆盖至生成tiles结果”目录中的 layer.json文件和0文件夹至生成结果目录下,比如我当前是 terrain_tiles目录下。

注意:地形切片至少要到14级,地形分辨率太低将导致卫星影像失真。

GDAL的安装使用

GDAL(Geospatial Data Abstraction Library)是一个在X/MIT许可协议下的开源栅格空间数据转换库。该项目由Frank Warmerdam教授于1998年发起。 它利用抽象数据模型来表达所支持的各种文件格式。 它还有一系列命令行工具来进行数据转换和处理。 OGR(OpenGIS Simple Features Reference Implementation)是GDAL项目的一个子项目, 提供对矢量数据的支持。 一般把这两个库合称为GDAL/OGR,或者简称为GDAL。

GDAL安装方法之一

GDAL不推荐从源码编译安装,建议从gisinternals下载gdal-203-1911-x64-core.msi、GDAL-2.3.0.win-amd64-py2.7.msi直接二进制安装。安装步骤如下:

  1. 安装python2.7。安装完成后配置环境变量,在path中添加 C:\Python27;
  2. 安装gdal-203-1911-x64-core.msi。安装完成后,设置环境变量GDAL_DATA,值为C:\Program Files\GDAL\gdal-data\,修改环境变量PATH,增加C:\Program Files\GDAL;
  3. pip install numpy。这是为了使用地形切割的脚本gdal2srtmtiles.py。
  4. 安装GDAL-2.3.0.win-amd64-py2.7.msi。

注意:当瓦片过多时,会报“OverflowError: range() result has too many items”的错误,而且python2已不再维护,因此不建议采用该方法安装GDAL。

GDAL安装方法之二

OSGeo4W是用于开源GIS项目的Windows安装程序。 开源的本质意味着许多项目/程序相互依赖才能实现功能。 GDAL就是一个很好的例子。 几乎所有的开源GIS项目都在某种程度上使用GDAL来读写数据。 但是因为Windows是一个封闭的平台,所以它没有像基于Unix的操作系统那样开发软件包管理器。 因此,如果使用GRASS和QGIS的独立安装程序安装它们,则最终会进行2次GDAL安装。 添加3或4个以上的开源安装,最终您将获得十几个GDAL安装,此外还有一个仅用于GDAL的安装。

这就是OSGeo4W的用处。它可以跟踪开源GIS软件包的共享需求,因此QGIS和GRASS可以共享一次GDAL安装。 它还跟踪版本,因此您可以简单地升级程序。

从官网GDAL2Tiles Project找到 OSGeo4W 的下载链接,下载OSGeo4W并安装即可。

参考链接

  1. Imagery-Layers-Tutorial, by cesiumjs
  2. Terrain-Tutorial, by cesiumjs
  3. 国内主要地图瓦片坐标系定义及计算原理, by CntChen
  4. 地图投影, by wikipedia
  5. Hexo博客搭建之在文章中插入图片, by Yan Yinhong
  6. 用ArcMap给遥感影像(tif格式)加入地理坐标,by wbz810.
  7. GDAL,by gdal
  8. Cesium之地形(1), by Super洛伽
  9. Cesium中地形数据的加载,by CrazyGIS
  10. OpenLayers之多源数据加载二:瓦片地图原理,by giser.
  11. cesium加载离线tms切片,by nygfcn.
  12. TIFF,by wikipedia.
  13. Tile_Map_Service_Specification,by osgeo.
  14. TIFF图像文件格式详解(1),by xdyang.
  15. TIFF图像文件格式分析,by windcsn.
  16. 1. 地理数据处理软件包GDAL简介,by osgeo.cn.
  17. google-maps-coordinates-tile-bounds-projection,by maptiler.
  18. geopy,by theonegis.
  19. 工具gdal2srtmtiles的安装使用,by 四域公子.
  20. 地形数据:TIF地形转terrain格式,by mars3d.
  21. 瓦片底图:TIF影像金字塔切片处理及加载,by mars3d.

Cesium中飞行器姿态控制

发表于 2018-06-10 | 更新于 2020-11-29

问题提出

最近研究开源虚拟地球Cesium,遇到一个问题:在获取飞行器经度纬度高度偏航俯仰滚转六个参数的前提下,如何在Cesium中用CZML文件表示,从而完美实现飞行器姿态轨迹的复现。在CZML的position属性中使用cartographicDegrees表示飞行器的经度纬度高度,可以很好复现飞行器的轨迹。然而在CZML的orientation属性中使用unitQuaternion表示飞机自身姿态时,飞机姿态总是不对。

问题分析

基本知识

在Cesium中存在两种常用坐标系:

  1. 世界坐标系
    世界坐标系统是以椭球中心为原点的笛卡尔空间直角坐标系。
  2. 地理坐标系
    地理坐标系即人们常用的使用经度、纬度、高度表示位置的坐标系。
  3. 站心坐标系
    又称为Earth-fixed coordinate system(站点坐标系、东-北-天坐标系ENU)。可分为站心直角坐标系和站心极坐标系。
    常用的站心直角坐标系定义为:以站心(如GPS接收天线中心)为坐标系原点O,Z轴与椭球法线重合,向上为正(天向),y与椭球短半轴重合(北向),x轴与地球椭球的长半轴重合(东向)所构成的直角坐标系,称为当地东北天坐标系(ENU)。
    飞行器的偏航、俯仰、滚转即是以站心直角坐标系为参考。
  4. 模型坐标系
    模型坐标系以物体的中心为坐标原点,物体旋转、平移等操作都是围绕局部坐标系进行的。这时当物体模型进行旋转、平移等操作时,局部坐标系也执行相应的旋转、平移等操作。
    局部坐标系是一个假想的坐标系,该坐标系与物体的相对位置至始至终是不变的,假想出这个坐标系的目的主要是为了正向理解对三维场景中物体执行的平移和旋转操作。使用局部坐标系理解模型变换时,所有的变换操作直接作用与局部坐标系,由于局部坐标系与物体的相对位置不变,因此对局部坐标系进行平移、旋转和缩放时,物体在场景中位置和形状也会发生相应的变化。

在Cesium中存在两种世界坐标系和地理坐标系之间坐标转换的方法:

1
2
3
4
5
6
7
8
9
# 经纬度转换为世界坐标
Cesium.Cartesian3.fromDegrees(longitude, latitude, height, ellipsoid, result)
# 世界坐标转换为经纬度
var ellipsoid=viewer.scene.globe.ellipsoid;
var cartesian3=new Cesium.cartesian3(x,y,z);
var cartographic=ellipsoid.cartesianToCartographic(cartesian3);
var lat=Cesium.Math.toDegrees(cartograhphic.latitude);
var lng=Cesium.Math.toDegrees(cartograhpinc.longitude);
var alt=cartographic.height;

CZML分析

CZML是一种用来描述动态场景的JSON架构的语言,主要用于Cesium在浏览器中的展示。它可以用来描述点、线、布告板、模型以及其他的图元,同时定义他们是怎样随时间变化的[3]。

CZML可使用model属性直接加载3D模型,由position属性决定模型在世界坐标系内的位置,由orientation决定模型在世界坐标系内的姿态,并最终由orientation的unitQuaternion表示。

unitQuaternion即单位四元数,可与偏航俯仰滚转相互转换,并避免偏航俯仰滚转的万向节锁定问题,但其本质上都是一个旋转矩阵,可决定模型在世界坐标系内的姿态。

获取正确的飞行器姿态

首先计算飞行器模型坐标系对站点直角坐标系的旋转矩阵(在制作飞行器3D模型时,应使其中心在本地坐标系原点,机头机翼与轴平行,方便计算在站点直角坐标系内的旋转角度),再乘以由飞行器偏航俯仰滚转得到的旋转矩阵,再乘以站点直角坐标系对世界坐标系的旋转矩阵。将最终得到旋转矩阵转换为单位四元数,即得到正确的飞行器姿态。

小技巧:飞机3D模型的中心设置在本地坐标系原点,机头机翼与轴平行后,可能在Cesium中渲染时姿态还是不对。原因是绕Z轴旋转的角度还是不对。可以在HeadingPitchRoll示例中使用自己的飞机3D模型,参照Cesium给的运输机模型,比较它们之间绕Z轴旋转的差异,然后调整飞机3D模型绕Z轴旋转的角度。

解决方法

参考代码如下:

1
2
3
4
5
6
var lon=113,lat=34;//模型的站心经纬度
var h1=0,p1=0,r1=0;//模型坐标系对站点坐标系的旋转角度
var h2=0,p2=0,r2=0;//模型在站点坐标系的姿态
var center = Cesium.Cartesian3.fromDegrees(lon, lat)
var hpr = new Cesium.HeadingPitchRoll(h1+h2, p1+p2, r1+r2)
var q1 = Cesium.Transforms.headingPitchRollQuaternion(center, hpr)

参考链接

  1. CESIUM : How to animate an aircraft from pitch, roll, heading?, by stackoverflow.
  2. Cesium中的几种坐标和相互转换, by finalLi.
  3. Cesium Language (CZML) 入门1 — CZML Structure(CZML的结构), by laixiangran.
  4. CZML Structure, by AnalyticalGraphicsInc.
  5. 3D坐标系, by S_H_C.
  6. 3D空间的坐标系, by trojanpizza.
  7. cesium获取某个位置垂直于当前地表的垂直坐标系,by 暮志未晚Webgl.
  8. Cesium控制模型旋转2019-11-15,by _____xyz.

春江花月夜

发表于 2018-06-09
作者:张若虚 春江潮水连海平,海上明月共潮生。 滟滟随波千万里,何处春江无月明? 江流宛转绕芳甸,月照花林皆似霰。 空里流霜不觉飞,汀上白沙看不见。 江天一色无纤尘,皎皎空中孤月轮。 江畔何人初见月,江月何年初照人? 人生代代无穷已,江月年年祇相似。 不知江月待何人?但见长江送流水。 白云一片去悠悠,青枫浦上不胜愁。 谁家今夜扁舟子,何处相思明月楼? 可怜楼上月徘徊,应照离人妆镜台。 玉户帘中卷不去,捣衣砧上拂还来。 此时相望不相闻,愿逐月华流照君。 鸿雁长飞光不度,鱼龙潜跃水成文。 昨夜闲潭梦落花,可怜春半不还家。 江水流春去欲尽,江潭落月复西斜。 斜月沉沉藏海雾,碣石潇湘无限路。 不知乘月几人归,落月摇情满江树。

Cesium小部件animation和timeline的系统时间显示

发表于 2018-06-09 | 更新于 2019-12-26

Cesium的小部件animation和timeline显示UTC系统时间,需要将其改为本地系统时间。修改方法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
this.viewer.animation.viewModel.dateFormatter = localeDateTimeFormatter
this.viewer.animation.viewModel.timeFormatter = localeTimeFormatter
this.viewer.timeline.makeLabel = function (time) { return localeDateTimeFormatter(time) }

// Date formatting to a global form
function localeDateTimeFormatter(datetime, viewModel, ignoredate) {
var julianDT = new Cesium.JulianDate();
Cesium.JulianDate.addHours(datetime,8,julianDT)
var gregorianDT= Cesium.JulianDate.toGregorianDate(julianDT)
var objDT;
if (ignoredate)
objDT = '';
else {
objDT = new Date(gregorianDT.year, gregorianDT.month - 1, gregorianDT.day);
objDT = gregorianDT.year + '年' +objDT.toLocaleString("zh-cn", { month: "short" })+ gregorianDT.day + '日' ;
if (viewModel || gregorianDT.hour + gregorianDT.minute === 0)
return objDT;
objDT += ' ';
}
return objDT + Cesium.sprintf("%02d:%02d:%02d", gregorianDT.hour, gregorianDT.minute, gregorianDT.second);
}

function localeTimeFormatter(time, viewModel) {
return localeDateTimeFormatter(time, viewModel, true);
}

上述代码还存在一个问题,当timeline小部件不活动时,其仍然显示UTC标准系统时间。

Cesium的credit十分碍眼,采用如下方法去掉:

1
2
//去除版权信息
viewer._cesiumWidget._creditContainer.style.display = "none";

参考链接

  1. EST,CST,PST,GMT,UTC,EDT等等时间缩写, by chienchia.
  2. Change UTC timezone to SGT timezone in Cesiumjs, by stackoverflow.
  3. Cesium去掉logo,by 跃然实验室.

Matplotlib蜡烛图绘制教程

发表于 2018-06-03 | 更新于 2020-01-01

股票分析离不开各种图表的绘制,尤其是最常用的蜡烛图。下面介绍python中使用matplotlib绘制蜡烛图的过程。

环境及配置

使用的环境如下:

1
2
3
4
5
6
7
8
9
10
11
12
$ python3 --version
Python 3.6.5rc1
$ ipython3
In [1]: import matplotlib
In [2]: matplotlib.__version__
Out[2]: '2.2.2'
In [3]: import tushare as ts
In [4]: ts.__version__
Out[4]: '1.1.9'
In [5]: import talib
In [6]: talib.__version__
Out[6]: '0.4.17'

绘制蜡烛图通常使用matplotlib.finance库,但这个库在matplotlib 2.0后已经被废弃,并被移到一个名叫mpl_finance的库中。可使用如下命令安装mpl_finance。

1
pip3 install https://github.com/matplotlib/mpl_finance/archive/master.zip

mpl_finance使用雅虎接口获取股票数据,但是不稳定,因此推荐使用tushare库获取股票数据。另外使用talib计算股票各种技术分析指标,例如常用的均线数据。需要注意的是通过pip3按照的talib库是对C/C++版talib的包装,因此需先安装C/C++版talib。

蜡烛图绘制

蜡烛图绘制函数简介

mpl_finance库中蜡烛图的绘制主要有以下四个函数:

1
2
3
4
candlestick2_ochl(ax, opens, closes, highs, lows, width=4, colorup='k', colordown='r', alpha=0.75)
candlestick2_ohlc(ax, opens, highs, lows, closes, width=4, colorup='k', colordown='r', alpha=0.75)
candlestick_ochl(ax, quotes, width=0.2, colorup='k', colordown='r', alpha=1.0)
candlestick_ohlc(ax, quotes, width=0.2, colorup='k', colordown='r', alpha=1.0)

在这四个函数的使用过程中需要注意的是:

  • candlestick2_ohlc函数假定opens, highs, lows, closes中任意一个值不存在,则其他值也不存在。

  • candlestick_ochl和candlestick_ohlc的输入参数quotes是(time, open, close, high, low, …)元组序列,其中time必须是浮点日期格式,具体参见date2num。

蜡烛图绘制函数存在日线图的时间间隔问题(非交易日无法跳过,导致图像断裂)。常用解决方法是建议重新调整横坐标,被动地过滤掉非交易时间段[2]。例如:

1
2
ax.set_xticks(range(0, len(data['date']), 10))
ax.set_xticklabels(data['date'][::10])

蜡烛图的简单绘制

蜡烛图的简单例子如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import numpy as np
import talib
import tushare as ts
import matplotlib.pyplot as plt
import mpl_finance as mpf

data = ts.get_k_data('002320')
sma_10 = talib.SMA(np.array(data['close']), 10)
sma_30 = talib.SMA(np.array(data['close']), 30)
fig = plt.figure(figsize=(24, 8))
ax = fig.add_subplot(1, 1, 1)
ax.set_xticks(range(0, len(data['date']), 50))
ax.set_xticklabels(data['date'][::50])
ax.plot(sma_10, label='10 日均线')
ax.plot(sma_30, label='30 日均线')
ax.legend(loc='upper left')
mpf.candlestick2_ochl(ax, data['open'], data['close'], data['high'], data['low'],width=0.5, colorup='r', colordown='green',alpha=0.6)
plt.grid()
plt.show()

参考链接

  1. finance api, by matplotlib
上一页1…50515253下一页

Jack Huang

524 日志
68 标签
© 2025 Jack Huang
由 Hexo 强力驱动
|
主题 — NexT.Muse