2024年4月

前言:

经过前面几篇的学习,我们了解到指令的大概分类,如:

参数加载指令,该加载指令以 Ld 开头,将参数加载到栈中,以便于后续执行操作命令。

参数存储指令,其指令以 St 开头,将栈中的数据,存储到指定的变量中,以方便后续使用。

创建实例指令,其指令以 New 开头,用于在运行时动态生成并初始化对象。

方法调用指令,该指令以 Call 开头,用于在运行时调用其它方法。

支条件指令,该指令通常以 Br、或 B、C 开头,用于在运行分支条件时跳转指令。

本篇介绍类型转换指令,该指令通常以 Cast、Conv 开头或box结尾,用于在运行时对类型进行转换。

类型转换指令介绍:

在.NET中,类型转换是一个常见的操作,它允许我们在不同的数据类型之间进行转换。ILGenerator 提供了一系列的指令来执行各种类型转换操作。这些指令可以分为三类:强制类型转换指令、隐式类型转换指令和数值类型转换指令。

  1. 强制类型转换指令:这些指令用于执行显式的类型转换操作,如果转换失败则会抛出异常。常见的强制类型转换指令包括
    castclass
    和 isinst 指令。

  2. 隐式类型转换指令:这些指令用于执行从引用类型到值类型或者从值类型到引用类型的转换,或者在值类型之间执行转换。
    unbox

    box
    指令是常见的隐式类型转换指令。

  3. 数值类型转换指令:这些指令用于执行不同数值类型之间的转换,比如将整数转换为浮点数,或者将浮点数转换为整数。
    conv
    指令系列提供了这些功能。

通过这些类型转换指令,我们可以在 IL 级别执行各种类型转换操作,为动态生成的代码增加了灵活性和功能性。

接下来我们将详细介绍这些指令的用法和示例。

1、强制类型转换指令:

Castclass 指令:强制类型转换

示例代码:

MethodBuilder methodBuilder = tb.DefineMethod("ConvertTo", MethodAttributes.Public | MethodAttributes.Static, typeof(XmlEntity), new Type[] { typeof(object) });
ILGenerator il=methodBuilder.GetILGenerator();

il.Emit(OpCodes.Ldarg_0 );
il.Emit(OpCodes.Castclass,
typeof(XmlEntity));

il.Emit(OpCodes.Ret);
//返回该值

对应代码:

Isinst 指令: as 类型转换

示例代码:

MethodBuilder methodBuilder = tb.DefineMethod("ConvertTo", MethodAttributes.Public | MethodAttributes.Static, typeof(XmlEntity), new Type[] { typeof(object) });ILGenerator il=methodBuilder.GetILGenerator();

il.Emit(OpCodes.Ldarg_0 );
il.Emit(OpCodes.Isinst,typeof(XmlEntity));

il.Emit(OpCodes.Ret);
//返回该值

对应代码:

2、隐式类型转换指令:

1、Box 指令:装箱

示例代码:

MethodBuilder methodBuilder = tb.DefineMethod("ConvertTo", MethodAttributes.Public | MethodAttributes.Static, typeof(object), new Type[] { typeof(int) });
ILGenerator il
=methodBuilder.GetILGenerator();

il.Emit(OpCodes.Ldarg_0);
il.Emit(OpCodes.Box,
typeof(int));

il.Emit(OpCodes.Ret);
//返回该值

对应代码:

2、Unbox_Any 指令:拆箱

示例代码:

MethodBuilder methodBuilder = tb.DefineMethod("ConvertTo", MethodAttributes.Public | MethodAttributes.Static, typeof(int), new Type[] { typeof(object) });

ILGenerator il
=methodBuilder.GetILGenerator();

il.Emit(OpCodes.Ldarg_0);
il.Emit(OpCodes.Unbox_Any,
typeof(int));

il.Emit(OpCodes.Ret);
//返回该值

对应代码:

3、Unbox 指令:拆箱并返回指向值的引用地址

对应代码:

可以看出,返回的是引用,如果需要获取值,需要配置 Ldobj 指令:

来一个示例代码:

var dynamicMethod = new DynamicMethod("ConvertTo", typeof(int), new Type[] { typeof(object) }, typeof(AssMethodIL_Condition));

ILGenerator il
=dynamicMethod.GetILGenerator();
il.Emit(OpCodes.Ldarg_0);

il.Emit(OpCodes.Unbox,
typeof(int));
il.Emit(OpCodes.Ldobj,
typeof(int));

il.Emit(OpCodes.Ret);
//返回该值 var result = dynamicMethod.Invoke(null, new object[] { 11});
Console.WriteLine(result);
Console.Read();

运行效果:

可以理解为:

Unbox_Any 指令 = Unbox 指令 + Ldobj 指令。 

3、数值类型转换指令:

在CIL(Common Intermediate Language)中,"conv"(convert)相关指令用于进行类型转换,将一个数据类型转换为另一个数据类型。这些指令通常用于在不同的数据类型之间进行显式转换。

以下是一些常用的"conv"相关指令及其功能:

  1. conv.i1
    : 将值转换为有符号 8 位整数类型(sbyte)。
  2. conv.i2
    : 将值转换为有符号 16 位整数类型(short)。
  3. conv.i4
    : 将值转换为有符号 32 位整数类型(int)。
  4. conv.i8
    : 将值转换为有符号 64 位整数类型(long)。
  5. conv.u1
    : 将值转换为无符号 8 位整数类型(byte)。
  6. conv.u2
    : 将值转换为无符号 16 位整数类型(ushort)。
  7. conv.u4
    : 将值转换为无符号 32 位整数类型(uint)。
  8. conv.u8
    : 将值转换为无符号 64 位整数类型(ulong)。
  9. conv.r4
    : 将值转换为单精度浮点数类型(float)。
  10. conv.r8
    : 将值转换为双精度浮点数类型(double)。

这些指令在IL代码中用于执行类型转换操作。下面是一个简单的示例,演示如何使用这些指令:

var dynamicMethod = new DynamicMethod("ConvertTo", typeof(float), new Type[] { typeof(int) }, typeof(AssMethodIL_Condition));
ILGenerator il
=dynamicMethod.GetILGenerator();
il.Emit(OpCodes.Ldarg_0);
il.Emit(OpCodes.Conv_R4);

il.Emit(OpCodes.Ret);
//返回该值 var result = dynamicMethod.Invoke(null, new object[] { 11});
Console.WriteLine(result);
Console.Read();

运行结果:

在 Emit 中,类型是需要精确转换的,如果不进行转换,你可能得到类似这样的结果:

总结:

在本教程的第六部分中,我们深入探讨了 ILGenerator 中的类型转换指令。

通过了解这些指令,你可以在动态生成的代码中执行各种类型转换操作,从而更好地控制程序的行为和数据流。

类型转换指令在 .NET 开发中非常有用,特别是在需要进行数据类型转换或操作时。

通过本教程,你应该已经了解了如何使用 ILGenerator 来生成这些转换指令,并且知道它们在 IL 代码中的具体用法和语法。

掌握 ILGenerator 中的类型转换指令将为你的动态代码生成带来更大的灵活性和效率。

继续学习并探索 ILGenerator 中其他功能和指令,以加深对 .NET 平台底层运行机制的理解,并提升自己在 .NET 开发领域的技能水平。

领域驱动设计(DDD)里面有一堆专业术语,比如领域、子域、核心域、通用域、支撑域等等,听着是不是觉得挺吓人?别怕,我来带你轻松搞懂它们。

如何理解领域和子域?

领域是指一定的业务范围或问题域。在解决业务问题时,DDD 会将业务领域进行细分,将问题范围限定在一定的边界内,在这个边界内建立领域模型,用代码实现这些领域模型,解决相应的业务问题。实际上,核心思想就是分而治之。

领域可以进一步划分为子域,每个子域对应一个更小的问题域或业务范围。

DDD 本质上是一种处理复杂领域的设计方法,它试图通过不断的细分,使业务从复杂变得简单,更容易理解,技术实现也更容易。

就像一个公司里面有不同级别的部门一样。例如,一家互联网创业公司,有产品研发部、市场营销部、客户服务部等。

领域相当于公司中的一个大部门,比如产品研发部。这个部门负责产品的设计与研发,确定公司的主要业务方向和策略。

子域就像是大部门下的小团队,例如在产品开发部下,可能会有产品团队、前端团队、后端团队、测试团队等。每个子域团队专注于更具体的任务,但都是为了支撑上级部门的目标而设立的。

通过这样的分级,公司可以确保每个部门、每个团队甚至每个小组都有明确的责任和目标,让公司的运作更加有序和高效。

同样,在DDD中通过划分领域、子域,可以帮助软件研发团队更好地理解和处理复杂的业务需求,每个层级关注不同的细节,但共同协作完成整个系统的开发。这样不仅提高了开发效率,还能确保软件能够准确地满足业务需求。

如何理解核心域、通用域和支撑域?

在领域划分的过程中,子域可以根据其重要性和功能属性划分为核心域、通用域和支撑域。

核心域决定产品和公司核心竞争力,通用域是被多个子域使用的通用功能域,支撑域是支撑业务的运转而存在,但不能决定产品和公司核心竞争力,也不包含通用功能。

划分核心域、支撑域和通用域的主要目标是
聚焦关键事项
,通过领域划分,区分不同子域在公司内的重要性,让公司更有效地分配资源和关注度,确保能在激烈的市场竞争中保持优势。

例如,以电商领域为例,常见的子域包括:商品子域、订单子域、用户子域、支付子域、物流子域、客服子域、数据分析子域。

在电商领域,核心域是直接与业务的核心价值和主要收入相关的领域。通常包括:

  • 商品子域:管理商品信息,包括商品展示、分类、搜索和推荐等,是电商平台的基础。
  • 订单子域:处理订单的创建、修改、查询和状态管理等,是完成交易的关键环节。
  • 支付子域:处理支付事务,包括支付方式管理、支付状态跟踪、支付渠道对接等,是完成交易的另一个关键环节。

通用域是支持业务运作的领域,它的能力可以在多个业务领域中使用:

  • 用户子域:管理用户信息,包括用户注册、登录、资料编辑等。虽然用户管理在很多系统中很重要,但在电商系统中,它更多地是支持核心业务流程。

支撑域是指那些为核心域和通用域提供支持的领域,通常涉及基础设施、运营管理、不直接关联收入的领域:

  • 物流子域:处理商品的配送,包括物流公司管理、配送状态跟踪等,是确保商品成功送达消费者的关键环节。
  • 客服子域:提供客户支持,包括咨询、投诉处理等,帮助解决用户在使用过程中遇到的问题。
  • 数据分析子域:分析业务数据,包括用户行为分析、销售数据分析等,支持决策制定和业务优化。

总结

领域的核心思想是将问题域逐级细分,降低业务理解和系统实现的复杂度。

核心域、通用域和支撑域则是子域的进一步分类。核心域是产品和公司的核心竞争力,通用域是被多个子域使用的能力,支撑域是支撑业务运转的领域。这种划分有助于理解和处理复杂的业务需求,提高开发效率,确保软件满足业务需求。

社区老给我推Canvas,于是我也学习Canvas做了个简历编辑器

大概一个月前,我发现社区老是给我推荐
Canvas
相关的内容,比如很多 小游戏、流程图编辑器、图片编辑器 等等各种各样的项目,不知道是不是因为我某一天点击了相关内容触发了推荐机制,还是因为现在
Canvas
比较火大家都在卷,本着我可以用不上但是不能不会的原则,我也花了将近一个月的时间通过
Canvas
实现了简历编辑器。

关于
Canvas
简历编辑器项目的相关文章:

为什么要自行实现一个简历编辑器:

  1. 固定模版不好用,各种模版用起来细节上并不是很满意,要么是模块的位置固定,要么是页面边距不满意,而通过
    Canvas
    实现的简历编辑器都是图形,完全依靠画布绘制图形,在给定的基础图形上可以任意绘制,不会有排版问题。
  2. 数据安全不能保证,因为简历上通常会存在很多个人信息,例如电话、邮箱等等,这些简历网站通常都需要登录才能用,数据都存在服务端,虽然泄漏的可能性不大,但是保护隐私还是很重要的,此编辑器是纯前端项目,数据全部存储在本地,没有任何服务器上传行为,可以完全保证数据安全。
  3. 维持一页简历不易,之前使用某简历模版网站时,某一项写的字较多时导出就会出现多页的情况,而我们大家大概都听说过简历最好是一页,所以在实现此编辑器时是直接通过排版的方式生成
    PDF
    ,所以在设置页面大小后,导出的
    PDF
    总会是保持一页,看起来会更美观。

背景

我是有个基于
DOM
实现的简历编辑器项目的,因为暂时找不到可以用
Canvas
实现的比较有意思的场景,所以才选择了继续做简历编辑器,最开始做简历编辑器就是因为很多简历网站都是要开会员的,要不就是简历的自定义程度比较差,达不到我想要的效果,在学校的某一个晚上突发奇想于是自己做了一个出来。

因为是本着学习的态度以及对技术的好奇心来做的,所以除了一些工具类的包例如
ArcoDesign

ResizeObserve

Jest
等包之外,关于 数据结构
packages/delta
、插件化
packages/plugin
、核心模块
packages/core
等都是手动实现的。实际上这也是本着 自己学习的项目能自己写就自己写,公司/商业化项目能有已有包就用已有包 的原则来的,在这里的目标是学习而不是做产品,自己学习肯定是希望能够更多地接触相对底层一些的能力,自己可以多踩一些坑会对相关能力有更深的理解,如果是公司的项目那肯定是成熟的产品优先,成熟的产品对于边界
case
的处理以及积攒的
issue
也不是轻易能够比拟的。

开源地址:
https://github.com/WindrunnerMax/CanvasEditor

在线
DEMO
:
https://windrunnermax.github.io/CanvasEditor/

笔记

因为我的主要目标是学习基本的
Canvas
知识和能力,所以很多功能模块都是采用简单的方式实现的,主打一个能用就行。而实际上做好图形编程是一件非常困难的事,如果要做一些复杂的能力我会更倾向于用
konva
等工具包来实现,而即使是简单地实现功能,在写代码的时候我也遇到了很多问题,也记录一些思考来解决问题。

数据结构

数据结构的设计,类似于
DeltaSet
,最终呈现的数据结构形式是扁平化的,但是在
Core
中需要设计
State
来管理树形结构,因为要设计
Undo/Redo
的功能,在不全量存储快照的情况下就意味着必须设计原子化的
Op
,因为想实现的功能有组合这个能力,所以最终实现的形式实际上是树形的结构,而我希望的结构是扁平化的,因为树形结构查找起来比较费劲,需要实现的
Op
类型也会变多,我希望能尽量减少
Op
的类型并且能够做到
History
,所以最终定下的数据结构是
DeltaSet
作为存储,通过
State
来管理整个编辑器状态。

History

原子化的
Op
已经设计好了,所以在设计
History
模块时就不需要全量保存快照了,但是如果每个操作都需要并入
History Stack
的话可能并不是很好,通常都是有
N

Op
的一并
Undo/Redo
,所以这个模块应该有一个定时器,如果在
N
毫秒秒内没有新的
Op
加入的话就将
Op
并入
History Stack
,但是当时我在思考一个问题,如果这
N
毫秒内用户进行了
Undo
操作应该怎么办,后来想想实际上很简单,此时只需要清除定时器,将暂存的
Op[]
立即放置于
Redo Stack
即可。

绘制

任何元素都是矩形,数据结构也是据此设计抽象出来的,在绘制的时候分为两层
Canvas
重叠的方式,内层的
Canvas
是用来绘制具体图形的,这里预计需要实现增量更新,而外层的
Canvas
是用来绘制中间状态的,例如选中图形、多选、调整图形位置/大小等,在这里是会全量刷新的,并且后边可能会在这里绘制标尺。在实现交互的过程中我遇到了一个比较棘手的问题,因为不存在
DOM
,所有的操作都是需要根据位置信息来计算的,比如选中图形后调整大小的点就需要在选中状态下并且点击的位置恰好是那几个点外加一定的偏移量,然后再根据
MouseMove
事件来调整图形大小,而实际上在这里的交互会非常多,包括多选、拖拽框选、
Hover
效果,都是根据
MouseDown

MouseMove

MouseUp
三个事件完成的,所以如何管理状态以及绘制
UI
交互就是个比较麻烦的问题,在这里我只能想到根据不同的状态来携带不同的
Payload
,进而绘制交互。

绘制状态

在实现绘制的时候,我一直在考虑应该如何实现这个能力,因为上边也说了这里是没有
DOM
的,所以最开始的时候我通过
MouseDown

MouseMove

MouseUp
实现了一个非常混乱的状态管理,完全是基于事件的触发然后执行相关副作用从而调用
Mask
的方法进行重新绘制。再后来我觉得这样的代码根本没有办法维护,所以改动了一下,将我所需要的状态全部都存储到一个
Store
中,通过我自定义的事件管理来通知状态的改变,最终通过状态改变的类型来严格控制将要绘制的内容,也算是将相关的逻辑抽象了一层,只不过在这里相当于是我维护了大量的状态,而且这些状态是相互关联的,所以会有很多的
if/else
去处理不同类型的状态改变,而且因为很多方法会比较复杂,传递了多层,导致状态管理虽然比之前好了一些可以明确知道状态是因为哪里导致变化的,但是实际上依旧不容易维护。最终我又思考了一下,决定在绘图这里实现类似于
DOM
的能力,因为我想实现的能力似乎本质上就是
DOM
与事件的关联,而
DOM
结构是一种非常成熟的设计了,这其中有一些很棒的点子,例如
DOM
的事件流,我不需要扁平化地调整每个
Node
的事件,而是只需要保证事件是从
ROOT
节点起始,最终又在
ROOT
上结束即可,并且整个树形结构以及状态是靠用户利用
DOM

API
来实现的,我们管理之需要处理
ROOT
就好了,这样就会很方便,下个阶段的状态管理是准备用这种方式来实现的。

渲染与事件

在前边我们提到了我们想通过模拟
DOM
来完成
Canvas
的绘制与交互,那么在这里就很明显涉及到
DOM
的两个重要内容,即
DOM
渲染与事件处理。那么就先聊下渲染方面的内容,使用
Canvas
实际上就很像将所有
DOM

position
设置为
absolute
,所有的渲染都是相对于
Canvas
这个
DOM
元素的位置绘制,那么我们就需要考虑重叠的情况,那么想一个例子,
A

zIndex

10

A
的子元素
B

zIndex

100

C

A
是平级的且
zIndex

20
,那么当这三个元素重叠的时候,在最顶部的元素是
C
,也就是说
zIndex
实际上只看平级元素,再假如
A

zIndex

10

A
的子元素
B

zIndex

1
,那么在这两个元素重叠的时候,在最顶部的元素是
B
,也就是说子元素通常都是渲染在父元素之上的。那么我们在这里也需要模拟这个行为,但是因为我们没有浏览器的渲染合成层,我们能够操作的只有一层,所以在这里我们需要根据一定的策略进行渲染,在渲染时我们与
DOM
的渲染策略相同,即先渲染父元素再渲染子元素,类似于深度优先递归遍历的渲染顺序,不同的是我们需要在每个节点遍历之前,将子节点根据
zIndex
排序来保证同层级的节点渲染重叠关系。

在渲染的基础上,我们还需要考虑事件的实现,例如我们的选中状态,八向调整元素大小的点一定是在选区节点的上层的,那么假如现在我们需要实现
onMouseEnter
事件的模拟,那么因为
Resize
这八个点位与选区节点是有一定重叠的,所以如果此时鼠标移动到重叠的点因为
Resize
的实际渲染位置更高,所以只应该触发这个点的事件而不应该触发后边的选区节点事件,而实际上由于没有
DOM
结构的存在我们就只能使用坐标计算,那么在这里我们最简单的方法就是保证整个遍历的顺序,也就是说高节点的遍历一定是要先于低节点的,当我们找到这个节点就结束遍历然后触发事件,事件的捕获与冒泡机制我们也需要模拟,实际上这个顺序跟渲染是反过来的,我们想要的是优点顶部的元素,优先更像树的右子树优先后序遍历,也就是把前序遍历的输出、左子树、右子树三个位置调换一下即可,但是问题来了,在
onMouseMove
这种高频事件触发的时候,我们每次都去计算节点的位置并且采用深度优先遍历,是非常耗费性能的,所以在这里实现一个典型的空间换时间,将当前节点的子节点按顺序全部存储起来,如果有节点的变动,就直接通知该节点的所有每一层父节点重新计算,这里做成按需计算即可,这样当另一颗子树不变的时候还可以节省下次计算的时间,并且存储的是节点的引用,不会有太大的消耗,这样就变递归为迭代了,另外因为找到了当前的节点,在模拟捕获与冒泡的时候就不需要再递归触发了,通过两个栈即可模拟。

焦点

平时我做富文本相关的功能比较多,所以在实现画板的时候总想按照富文本的设计思路来实现,因为之前也说过要实现
History
以及在编辑面板富文本的能力,所以焦点就很重要,如果焦点不在画板上的时候如果按下
Undo/Redo
键画板是不应该响应的,所以现在就需要有一个状态来控制当前焦点是否在
Canvas
上,经过调研发现了两个方案,方案一是使用
document.activeElement
,但是
Canvas
是不会有焦点的,所以需要将
tabIndex="-1"
属性赋予
Canvas
元素,这样就可以通过
activeElement
拿到焦点状态了,方案二是在
Canvas
上方再覆盖一层
div
,通过
pointerEvents: none
来防止事件的鼠标指针事件,但是此时通过
window.getSelection
是可以拿到焦点元素的,此时只需要再判断焦点元素是不是设置的这个元素就可以了。

无限画布

之前因为没有打算实现平移拖拽也就是无限画布的能力,但是后来真的开始通过这个主框架来实现想做的业务功能的时候发现这样是不行的,所以在后期想把这个能力加上,虽然本身这个能力并不复杂,但是因为最开始没有设计这个能力,导致后边做的时候有点难受,比如
Mask
批量刷新频率不对齐、
ctx

translate
应该是偏移值取反、之前多处超出画布不绘制的计算有误等等,就感觉在没有设计的情况下突然增加功能确实是有点难受的,不过好处是不需要大规模重构,只是个别点位的修正。

此外多扯点别的,这个项目除了一些辅助性的工具例如
resize-observer
以及组件库例如
arco-design
都是自己写的,相当于实现了
Canvas
的引擎,特别是在现在的
core-delta-plugin-utils
结构设计下,是完全可以抽离处理作为工具包使用的,当然易用性与性能方面肯定比不上那些有名的开源框架。只不过今天我恰好看到了一个评论说的挺好的:如果是个人能力提升,那么最好是首先理解开源库,然后仿照实现开源库的功能,主要的目标是学习;而如果是商业化的使用,那就变成了知名的开源库优先,这样可以很大程度上降低成本。

性能优化

在实现的过程中,绘制的性能优化主要有:

  1. 可视区域绘制,完全超出画布的元素不绘制。
  2. 按需绘制,只绘制当前操作影响范围内的元素。
  3. 分层绘制,高频操作绘制在上层画布,基础元素绘制在下层画布。
  4. 节流批量绘制,高频操作节流绘制,上层画布收集依赖批量绘制。

超链接

众所周知
Canvas
绘制出来就是纯粹的图片,而实际使用导出
PDF
的超链接是可以点击的,而我们当前就单纯只是图片无法做到这一点,所以需要解决这个问题,我想到的一个解决方案是在导出的时候,通过
DOM
生成透明的
a
标签,覆盖在原本的超链接位置,这样就可以实现点击跳转效果了。
PDF
本身也是文件格式,所以是可以借助
PDFKit/PDFjs

PDF
排版生成工具来导出的,通过这种方式也可以直接在导出的时候直接将其写入固定位置,并且可以不受浏览器打印的分页限制。

TODO

因为前边提到了我现在还是比较简单的实现方式,所以很多功能都不完善,还有很多想做的能力:

  • 层级调整,这个之前我想到了并且在
    core
    中设计了这个能力,现在只是缺乏调整的按钮用来调用,这个
    UI
    我还没考虑好应该怎么做。
  • 页面配置,我发现很多同学的简历都是不是标准的
    A4
    纸大小,所以这里还需要一个调整页面画布大小的问题。
  • 导入导出
    JSON
    ,这个就不用多说了,就是把底层数据结构导入导出的能力。
  • 排版
    PDF
    导出,这个应该需要跟页面配置一起做,现在的
    PDF
    导出是依赖浏览器的打印,会有一些分页的限制,如果自己排版的话就可以突破这个问题,多长的画布都是一页的简历大小。
  • 复制粘贴模块,在编辑的时候这个操作是很有用的,需要增加这个模块。

最后

这次对于
Canvas
的体验让我感觉还是不错的,后边我也会写一些在实现的时候碰到的问题以及如何解决问题的文章,不过我目前的主业还是还是写富文本编辑器,富文本编辑器也是天坑中的一员,后边也可能会先写编辑器相关的文章。

本文介绍基于
ArcMap
软件,自动批量计算矢量图层中
各个要素

面积
的方法。

一次,遇到一个问题,需要分别计算
ArcMap
软件中一个图层的所有面要素的面积。如图,这个图层中包括多个省级行政区矢量面要素,现在需要分别计算其中每一个要素各自的面积。

image

这里有一个方便的办法。

首先我们新建一个字段。打开图层属性表,选择“
Add Field
”。

在弹出的界面中,配置如下。其中,“
Name
”大家可以随意,“
Type
”选择“
Float
”,下方的字段属性中,“
Precision
”为字段的所有数字位数,无论是在小数点左边还是右边;“
Scale
”则为小数点右边的数字位数,也就是保留几位小数。例如:
365.20

Precision
就是
5

Scale
就是
2

可以看到,已经新建了一个字段。

在新建的字段名称上右键,选择“
Calculate Geometry
”。

弹出了一个提示,大概意思是说:我没有在开启编辑模式的情况下进行字段计算,那么这样会让计算变得快一些,但是一旦计算开始,就不能撤回。

因此,追求计算速度还是追求可以撤回,依据大家的实际情况来就好。因为我这里数据不多,计算也比较简单,因此就直接选择了继续。

点击“
Yes
”之后,在弹出的窗口中第一个“
Property
”下拉框中选择面积,也就是“
Area
”;此时发现我的“
Area
”后面跟着一个“
Disabled
”。为什么处于这种状态呢?

一般的,出现上述情况都是因为我们的面要素(也就是这个图层)处于地理坐标系,而并不是投影坐标系。二者最简单的区别就是,地理坐标系用经度、纬度作为空间衡量指标,而投影坐标系用米、千米等长度单位作为空间衡量指标。

那么,我们就来看看我们现在是不是处于地理坐标系状态。在图层列表中,右击我们当前的图层的名称,选择“
Properties
”。

选择“
Source
”,可以看到,图层的地理坐标系统(“Geographic Coordinate System”)为
WGS_1984
,这是一个地理坐标系,而非投影坐标系。

因此,如果我们需要计算面积,必须将这一图层转为投影坐标系。

具体转换的方法,点击查看
ArcGIS地图投影与坐标系转换的方法
即可,这里就不赘述啦~

转换为投影坐标系后,打开新图层的属性表,可以看到我们刚刚新建的字段在这里也是存在的。

依据同样的方法,打开“
Calculate Geometry
”,可以看到此时“
Area
”就不再带着“
Disabled
”了。

其中,“
Coordinate System
”选择第一项,也就是图层自身的坐标系,也就是刚刚我们给他投影所选用的投影坐标系;“
Units
”是单位,大家依据实际情况来就好。我在这里选择
平方米
~

点击“
OK
”。可以看到“
Area
”字段已经有面积了。

那么,我们对这个计算出来的面积随机验证一下。用河南来验证,计算面积为
165982687427.8129
平方米,也就是
165982.69
平方千米;我们看看实际面积:

差了大概1000平方千米。不能说很准确,但是还是可以满足基本的大尺度区域分析了。在这里,精度误差可能的来源包括所用省级行政区
.shp
文件精度、投影坐标系选用等。大家依据各自的研究需要,来决定需不需要对面积数据精度加以提升;可用的提升方法包括更换投影坐标系、采用更精确的行政区图层等方法。

再用北京试一下。计算的面积为
16394318646.91106
平方米,也就是
16394.32
平方千米;实际面积:

这个就很准确了~

那么,能不能求出图层中所有矢量要素面积的和呢?可以的——在“
Area
”字段名称上右键,选择“
Statistics
”。

这里可以对全部字段的样本数量、极值、综合、平均值、标准差、
Null
值个数等加以查看,非常方便。

至此,大功告成。

今年是园子能否找到商业化出路的决定性一年,这是一场破釜沉舟的决战,这是一个山穷水尽疑无路的巨大挑战。

在当前一穷二白三无人脉的困境下,这场决战,这个挑战,更是难上加难。

能否在园子里找到足够的支持、帮助与合作力量,商业模式是否能够与用户实现双赢,是破局的关键。

我们计划这个季度启动园子的新一轮天使融资,在启动融资之前,我们产生了一个想法。

我们相信用户的力量。园子能发展到现在就是来自用户的支持,几个关键发展节点都离不开用户的重要作用。记忆犹新的是2012年当时也是极度困难,服务器硬件坏了却没钱买服务器,有用户建议我们上阿里云,我们听取了用户的建议,并最终因此拯救了困境中的园子(因为上云带来的新机会)。

我们相信开源的力量。2004年刚创建园子的时候,用的就是一款开源软件 .Text,然后修修补补很多年,如果没有这款开源软件,当时园子网站根本搭建不起来。而现在更是离不开开源,园子的网站部署在开源的 Kubernetes 上,我们用开源的 .NET 和 Angular 开发着园子的产品。

结合用户与开源的力量,我们想到把园子的商业化作为一个开源项目,以开放共赢的方式和大家共同探索园子的商业化出路,于是就有了这篇“开源的脚步,商业化的出路”。

这个另类的开源项目已经托管在github上,项目名称是 OpenBusiness,仓库地址:
https://github.com/cnblogs/open-business

期待您关注这个开源项目,期待您关注、助力、参与园子的商业化过程,我们相信为开发者而存在的园子,在大家的支持下一定能找到一种与开发者共赢的商业模式,从而柳暗花明又一春。