Zade's Weblog

程序人生

Monthly Archives: 6月 2009

GeoAPI的绘制接口探讨-FeatureLayer

FeatureLayer是FeatureReader和FeatureTypeStyle的合集, 如果FeatureLayer不能提供比二者更多的功能,那么其必要性就值得怀疑.

仔细审查这个接口,可以看到唯一增加的接口是get/setLevel,这个Level即是图层显示的zorder次序;另外就是和这个zorder相关的事件.

在网页设计中, zorder是页面元素一个很常见的属性,用来控制显示的次序. 在我们以前的实现版本中, 我们往往使用要素在容器中的存储次序表示其显示次序. 现在的问题是独立的zorder属性是必须的吗?

zorder的主要优势是其独立性, 作为layer的一个单独的属性存在, 这样我们可以独立的修改这个属性,并且增加针对这个属性的事件.如果我们以其存储次序确定其显示次序, 那么这个属性就是相对的, 即相对其他图层的存储而言. 这样就丧失了独立性, 导致这个属性是只读的, 也不能(至少很不方便)针对这个属性设置事件.

所以zorder属性还是存在为好,在网页实践中,已经很好的证明了这个属性的可用性.

除了zorder,我们前面在Graphic讨论的各种控制图层的可见性, 可选择性,min/maxScale等属性, 都可以体现在FeatureLayer当中.

很明显,我们需要FeatureLayer这个类型.

我们看看设计FeatureLayer的主要问题, 即是所有权的问题.

FeatureTypeStyle可以直接的被FeatureLayer拥有, 这个没有异议; 但是FeatueReader则不然. 在我们的设计中, 所有权关系是: DataStore->FeatureSource->FeatueReader & FeatureEditor. FeatureReader 只能以指针引用的形式存在于FeatureLayer中, 这可能导致悬挂指针.

而且为了支持编辑接口,我们最好使用FeatureSource,而不是FeatureReader. 为了能够正确的维护FeatureLayer的生命周期, 我们必须在内部使用引用计数.

总结一下 :

1)FeatureLayer 应该存在

2)FeatureLayer除了支持zorder以外,还应该支持visible, showEditHandles, min/maxScale, selectable

3)FeatureLayer是FeatureSource,而不是FeatureReader的浅层拥有者, 要正确的维护FeatureSource的生命周期

GeoAPI的绘制接口探讨-PathType的含义

在GeoAPI2.1中,org.opengis.display.primitive.GraphicLineString和GraphicPolygon接口定义中,存在一个PathType  getPathType() 的接口定义.

在<<GO-1 Application Objects,OGC 03-064r10>>标准中6.3.5中,这样描述PathType:

Path types describe how lines are rendered with respect to the modelled surface of the earth. The categories of path type are:
• Global, consisting of rhumbline and great circle types
• Unprojected, consisting of pixel straight and spline types
• Vector
PathType serves as the base class for objects that represent the various methods for computing a path between two locations. Singleton instances of PathType will exist to represent, for example, a path of constant bearing (rhumbline), or a great circle path.
Path type is an algorithmic sequence of interpolation and projection.

简单的说, PathType是两点间进行差值和投影的算法序列, 分成Global,Unprojected和Vector三类.

我开始不是很明白这样的解释,因为里面甚至描述了对于显示设备上定义两点,如何绘制直线的算法. 我们知道这是图形学的基本算法,从实现的角度看,绘制引擎已经都实现,我们只是简单的调用. 至于像DDA, 反走样等技术, 我们只是使用, 并没有创造其实现(至少目前没有).

从实现的角度看, 我们这么理解:

  1. Global PathType, 这在绘制之前是需要投影的(例如经纬度数据), 或者是数据本身的投影(默认的),或者是动态投影
  2. Unprojected, 直接调用绘制引擎的实现即可, 差值方法和投影由绘制引擎控制
  3. Vector,和Unprojected类似. 例如直接的现实经纬度坐标,不经过投影

总的一句话, 我们按照数据本身的坐标,经过变换矩阵控制,直接投影到显示设备上; 如果必要,进行动态投影.

关于大圆,航海线等概念,参考 http://www.mathworks.com/access/helpdesk/help/toolbox/map/index.html?/access/helpdesk/help/toolbox/map/f5-7173.html

GeoAPI的绘制接口探讨-Graphic的合理性

我们前面提到Graphic是Feature和Symbolizer的组合, 我们讨论起存在的合理性.

在C++的设计中,中立是一个重要的概念,比如异常中立, 生命所有权中立等. 中立的核心思想是(从c++的角度来看):我是算法,我执行指令,我并不维护数据本身的合法性,也并不特别关心底层函数调用的异常抛出情况.

C++这样做,一个很重要的原因是C++没有垃圾回收,对象的生命周期必须用户自己管理. 另外从设计来说, 对象的生命周期管理和异常处理,只有最上层的用户才确切的知道, 中间的处理过程并不知道如何处理这些情况, 所以只能保持中立.

Feature和Symbolizer组合的Graphic, 必然涉及到生命周期的管理. 我们这里并不提倡使用智能指针, 那么从这个角度来说, Graphic最好不要存在.

当然这个理由有点牵强, 因为这是语言的特性,Java就没有这个问题. 我们再从别的方面论述.

Graphic在Feature和Symbolizier的基础上,增加了那些功能呢?

  1. 编辑,包括对Feature.Geometry和Symbolizer的编辑,例如 GraphicLineString.addPoint, Graphic.setGraphicStyle等
  2. 和显示相关的属性,例如 visible, zorder, showEditHandles, min/maxScale, selectable, blinking
  3. 标示 name
  4. 位置叠加:parent
  5. 事件

Geometry的编辑应该放在Geometry本身上, Symbolizier也是这样. 把它们放在Graphic上,从概念和实现来看, 都没有特别的好处.

一般来说,和显示相关的属性中, visible, zorder,min/maxScale, selectable,这些是和图层相关,而不是和要素相关. 当然,如果控制了所有要素的这些属性, 图层的这些属性也可以控制. 但是这样做:1)从用户的角度来说,增加了概念的复杂性,2)从实现的角度来说, 增加了实现的复杂性.

showEditHandles和blinking 没有太大的必要性, 如果要的话,也应该放在图层而不是要素上.

从全局的角度来看,标示一个要素的因素是LayerID+FeatureID; 在特定的图层上,我们通过FeatureID标示它.通过name标示每个要素过于繁琐, 不可接受.

要素之间的叠加是动态的,随着图层的增减而不断的变化,所以我们可以通过特定的查询接口进行定位.通过parent定位,可能是受HTML DIV的影响,但是这大大增加了实现的复杂性. 考虑一下, 为了实现此功能, 当前的Graphic需要知道其周围Graphic的信息, 这样它就必须保留一个指向总的数据接口的引用. 应该知道,我们现在处在很低的层次(要素级), 每个Graphic增加一个这样的引用, 意味着很高的存储代价. 而且, 把这样的查询放在总的数据接口层(例如SLD.Layer接口上面), 不仅实现方便, 在概念上也更加的清晰. 一般来说, 概念和实现总是一致的.

如果1-4没有必要,那么5也就没有了必要,因为这些事件也可以随着一起迁移到更高的层次上.

总结一下:

  1. Graphic的很多属性可以移植到Geometry和Symbolizer上, 放在Graphic上没有必要
  2. 和显示,编辑相关的一些属性以及相关的事件更加应该放在图层,而不是要素Graphic上
  3. 定位查询应该在总的数据接口层,而不是在Graphic上
  4. Graphic没有充分存在的理由,在设计上应该舍弃

GeoAPI的绘制接口探讨-体系架构

我们使用一张简单的图说明之:

image

简单的描述一下:

  1. Feature是属性的集合, 属性的基本类型是Object, 其中有一个特殊的类型Geometry. Feature的表现和数据库的Record非常的类似.
  2. Geometry用来控制Feature输出的矢量控制点
  3. 除了Geometry以外的属性多是String,Double,Int等简单类型
  4. 用于渲染Geometry的显示信息是一个或多个Symbolizer
  5. Feature的集合表示为FeatureReader
  6. Symbolizer的集合表示为FeatureTypeStyle, 这个通过Rule和Feature的属性关联
  7. Feature和Symbolizer组合为Graphic,这是Canvas渲染的基本对象
  8. 基于Geometry和Symbolizer的不同,Graphic分为GraphicLineString, GraphicPolygon, GraphicLabel等
  9. FeatureReader和FeatureTypeStyle组合为FeatureLayer,即带有渲染信息的图层
  10. FeatureLayer和Canvas组合为FeatureCanvas,即带有渲染信息的图层和渲染对象的对象

这个架构是否存在问题? 我们逐步的讨论一下.

Geometry的抽象规范和实现规范

OGC有两类规范,抽象规范和是实现规范. 以Geometry为例,抽象规范是<<Topic 1 – Feature Geometry>>,后来演化为ISO 19107 <<Geographic information — Spatial schema>>; 实现规范是<<OpenGIS® Implementation Specification for Geographic information – Simple feature access – Part 1: Common architecture>>.

绝大部分的空间数据厂商,例如Oracle Spatial, DB2 Spatial/Geodetic Extender, SQLServer 2008,在数据存储的方式上大都遵循了OGC的实现规范.

但是GeoAPI在定义Geometry接口的时候,却是遵循了ISO 19107规范,也即抽象规范.

从实现的角度来看, 抽象规范过于繁琐, 实现起来会很麻烦. 但是存在即是合理, 何况这是国际规范, GeoAPI也把它奉若神明, 这说明ISO 19107有它的道理. 或者反过来说, 实现规范虽然简单, 但是必定存在一些固有的缺陷.

下面是实现规范的类框图(参见实现规范):

image

我从实现的角度来探讨一下实现规范的缺陷:

  1. 概念混乱,存在冗余
    不考虑继承,Curve的成员函数:
  2. + length() : Double
    + startPoint() : Point
    + endPoint() : Point
    + isClosed() : Boolean
    + isRing() : Boolean

    MultiCurve的成员函数:

    + isClosed() : Boolean
    + length() : Double

    看到了吗,存在两个完全相同的接口函数. 一般来说,在这种情况下,意味着可以定义一个共同的基类,来抽象这种共性. 抽象规范正是这么做的,即是GenericCurve的由来.
    这种分析同样适合于Surface和MultiSurface.
    这种概念上的冗余表明逻辑上的不严密性.

  3. Geometry类的体系结构不合理
    GeometryCollection是叶子节点类 MulltiPoint,MultiLineString和MultiPolygon的父类(直接的或者间接的). 一般来说, 叶子节点的子类是可以实例化的, 非叶子节点不应该实例化. 但是GeometryCollection允许实例化.
    例如GeometryCollection(Point(1 2), LineString(3 4, 5 6))就是一个简单的GeometryCollection的实例(WKT表示法).
    这会带来什么问题吗?
    当然! 举例来说, 我们要把LineString(1 2, 3 4)和LineString(5 6,7 8)合并为一个新的几何体, 那么几何体的类型可以是GeometryCollection(LineString(1 2, 3 4),LineString(5 6,7 8)), 当然也可以是MultiLineString(LineString(1 2, 3 4),LineString(5 6,7 8)). 这是不是有点别扭?
    再如,几何体g = GeometryCollection(Point(1 2), LineString(3 4, 5 6),Point(7 8)),我们删除其第二个几何体,那么结果是GeometryCollection(Point(1 2),Point(7 8)), 还是应该是MultiPoint(Point(1 2),Point(7 8))? 如果你觉得第二个结果更加合理, 那么结果是: 我修改一个实例对象g的内部状态(删除其第二个几何体组成要素), 却改变了这个对象g的类型! 这可真够诡异的.
    这种别扭, 诡异都是因为Geometry类的体系结构不合理, 即允许非叶子节点可以实例化造成的, 这违反了OOP设计的一般性准则.

当然, 实现规范可能还有其他的缺陷, 但是上面两条已经足够说明问题了.

但是,即便如此, 实现规范仍然在实践中大行其道, 原因是简单和方便, 并且我们上面提到的缺陷实践中很少遇到或者可以通过其他的方法补救.

例如第一点, 在概念上虽然冗余, 但是不伤大雅. 我们甚至可以把length(), area()等函数提到Geometry接口上面. 这样做, 后果是length area对于点型对象没有意义,area对于线性对象没有意义. 但是在实现上, 我们把没有意义的结果 return 0 即可. 这样做不会带来大的伤害, 反而更加的简单方便(我们sic3.0就是这么做的).

对于第二点, GeometryCollection对象在实践中非常的少见, 一般做装饰对象用, 例如图例或者草稿图层等. 如果是纯粹的MulltiPoint, MultiLineString 和 MultiPolygon,不会存在这样的问题.

总之, 实现规范在实践中还是很实用的, 虽然这对理想主义和完美主义者而言是白璧微瑕, 但这就是生活.

After reading <>

文章来源: http://queue.acm.org/detail.cfm?id=1255422

  1. An API must provide sufficient functionality for the caller to achieve its task.
    这虽然很显然,但是也不易做到
  2. An API should be minimal, without imposing undue inconvenience on the caller.
    接口最小化,这非常利于调用者进行组合复用,这不正是构件的内在精神吗?
  3. APIs cannot be designed without an understanding of their context.
    接口不是孤立的,和其他接口合作完成用户的功能是其最终目的.所谓上下文,即是接口组(也即构成的模块)的相互关系
  4. General-purpose APIs should be “policy-free;” special purpose APIs should be “policy-rich.”
    对用户上下文知道的越少,API应该越通用,也即用户可以施加更多的控制,用户的自由度越高;对用户上下文知道的越多,API应该越专用
  5. APIs should be designed from the perspective of the caller.
  6. A great way to get usable APIs is to let the customer (namely, the caller) write the function signature, and to give that signature to a programmer to implement.
    应该从调用者而不是实现者的角度设计API.
    作者后面也解释了,正确的次序应该是调用者初始化接口定义,实现者通过实现校正,然后二者经过迭代式的协商决定
  7. Good APIs don’t pass the buck.
    调用者非常不喜欢浮夸的接口.实现者往往替用户考虑的太多,而用户并不需要这么多的控制,造成接口定义复杂,用户使用不方便.
  8. APIs should be documented before they are implemented.
    在实现之前就定义好,听起来不难,实现起来非常的不容易.这需要架构师的协调和威望.
  9. Good APIs are ergonomic.
    接口应该人性化,接口的使用者是人,而不是机器, 虽然最终的执行者是机器.
    作者举了一致性的例子,例如C的fopen,fwrite和fprintf等在FILE传递次序上的不一致,这确实让人感觉不愉快
  10. As a result, the process of creating software has changed considerably: instead of creating functionality, much of today’s software engineering is about integrating existing functionality or about repackaging it in some way.
    深以为然!
    软件开发已经和原来发生了很大的不同.
  11. Looking at the curriculum of many universities, it seems that this shift in emphasis has gone largely unnoticed.
    看来不光我们国家这样,老外也不例外.
    我真诚的希望看到学校的老师更多的传授如何设计好的API,而不老是把精力放在排序算法上.这和当前的工程需要已经不吻合了.

后面作者提到了如何看待老年程序员的问题:

There is also a belief that older programmers “lose the edge” and don’t cut it anymore. That belief is mistaken, in my opinion: older programmers may not burn as much midnight oil as younger ones, but that’s not because they are old, but because they get the job done without having to stay up past midnight.

作者本人(MICHI HENNING, ICE的实现者之一,相当牛x)当年(2007)已经47岁,但是仍然工作在编程的第一线,而他身边的曾经的同事绝大部分走上了其他的工作岗位.这给人的误解是老年的程序员已经不再具有优势.作者的观点是:老年的程序员没有必要像年轻的程序员那样熬夜加班,因为有足够的经验使得他们不需要这样做!

好亲切呀!

发布地图的维度-地图发布流程4

如何确定发布地图维度?

这个问题至少表面看来不是很难. 首先我们根据投影数据的外包确定地图的物理维度,例如是5km*5km,然后根据比例尺确定发布地图的维度,例如1:5000,那么发布的地图的维度是(5km/5000)*(5km/5000),也就是1m*1m.

由于发布的地图的维度测量标准是像素,那么我们还需要转换. 一般来说, 一个像素的宽度是0.28mm*0.28mm,那么要发布的地图大小是3571px*3571px.

这里面有两个问题:

  1. 我们要发布的地图是需要分割的,一般的分割尺寸是256px*256px,也就是说,我们要发布的地图需要调整为256的倍数
  2. 0.28mm是一个一般的设置,实际的计算机分辨率不一定是这个数值,参见http://en.wikipedia.org/wiki/Dot_pitch

先看第一个问题.

很显然,我们需要为地图添加padding,也即map padding.3571 = 14 * 256 – 13,也就是说,我们只需要在长宽各补充13个像素即可.这13个像素补充在那里呢?

在左右,上下两个维度我们各有3中种选择(例如在左右维度上,我们有只向右,只向左,向右且向左),我们总共有3*3=9种选择.

为了能够和原始数据的中心点保持一致,我们使用向右且向左和向上且向下的策略.例如向左6个像素,向右7个像素;向上6个像素,向下7个像素.当然,发布地图调整以后,原始数据的外包也需要调整,以保持一致, 这个调整公式不难计算.

再看第二个问题.

一般来说,我们要发布的地图和当前屏幕看到的效果要保持一致,所以默认的设置是当前屏幕的分辨率,这一般可以从当前操作系统的一个函数调用得到.但是这也不是固定的,当前发布地图的屏显设置不一定是最终用户的设置,所以我们也要允许用户定制这个设置.

架构设计

特定比例尺的地图发布内容-地图发布流程4

根据地图发布规划->地图发布流程4,我们需要确定地图发布的内容,也就是说那些要素需要发布.

这里问题的实质是,一个地理要素要出现在发布的地图之上,是由那些要素决定的呢?

根据要素类型不同, 取决因素也不同:

  1. 点状要素:
    点状要素在物理意义上是没有大小(也就是说长度或者面积)的,为了显示,需要一个特定大小的点状符号(PointSymbolizer). 这样, 在特定大小的比例尺下面,点状要素可能相互压盖. 和注记一样,为了避免压盖,我们需要抽细,也就说,某些要素不能出现在地图上,需要被淘汰.
    很显然, 被淘汰的要素的重要性要低.要素的重要性是要素本身的一个性质, 例如天安门广场的重要性就比周围的商铺要高.所以 我们可以合理的假设要素存在一个属性字段Weight,表明其重要性,这也是我们淘汰策略的一个重要指标.
    重要性越高,表明这类要素出现在地图上的可能性越大. 但是这并不意味着Weight(Feature1)>Weight(Feature2) & Showed(Feature2)->Showed(Feature1). 具体来说,一个要素是否在地图上出现,取决于以下几个因素:
    1)要素的weight值
    2)其周围要素的密集程度
    3)要素的Id排序
    从实现的角度看,我们首先对要素进行排序,第一个参考指标是weight,如果weight相等,则比较其id值(在shp文件中,id值即其存储次序),这样要素的次序就是唯一确定的.然后我们按照这个次序和压盖避免原则,对点状要素进行显示配置和淘汰策略.
  2. 线状要素
    线状要素本身具有大小(其长度),所以线状要素一般不会压盖,我们也不必进行避让.但是在采集数据的过程中,由于历史的原因,某些线状要素被采集成面状要素.其目的是为了在特定的比例尺下显示面状要素,当比例尺缩小的时候,还需要还原为线状要素.也就是说,相对于点状要素,线状要素需要的原始数据可能存在多分,在不同的比例尺下面使用不同的数据.
    个人认为,这完全没有必要.我们可以通过配置线状符号(LineSymbolizer)的宽度,来控制线状要素的显示宽度(完全没有必要把线状要素采集为面状要素),通过规则(Rule)控制在不同比例尺下线状要素的显示样式.
  3. 面状要素
    和线状要素一样,面状要素本身也具有大小,所以一般不存在压盖问题.
    线状和面状要素存在一个共同的问题,在特定比例尺下面,要素的显示尺寸过小,有必要进行过滤.过滤的条件是线状要素的长度或者面状要素的面积.但是具体的参数,需要根据需要或者标准制定.
  4. 文本符号要素
    根据出图的要求,文本符号出现在地图上取决于4个因素:
    1)冲突, 注记之间不能冲突
    2)压盖,注记和要素之间不能压盖
    3)重要性, 重要的注记要优先显示
    4)相关性, 注记要和被注记的要素紧密关联
    从实现的角度来看,我们首先按照重要性对要素进行排序,然后初始化冲突列表,把已经存在或者非常重要的要素外包添加进去(这一步已经包含了对压盖的考虑),然后按照冲突避让原则绘制文本符号.
    相关性如何体现呢?在定义文本符号(PointSymbolizer)的时候,存在接口getDisplacement(),用来定义显示和要素之间的偏移.这个偏移一般是显示符号的维度,这样的目的是为了避免符号的文本之间的压盖.但是调整这个数值,也可以在一定程度上控制相关性.
  5. 装饰要素
    这些要素包含,比例尺和文本描述等.这些要素一般是在出图的最后添加的,而且往往呈现模板特性,相对而言比较容易控制.

显示控制要充分的参考这些内容.

地图发布规划

地图的发布时一个复杂的过程,我们要抽象一个发布流程模型来规范化我们的行为,我们把这个规范化的过程叫做地图发布规划.

地图发布流程:

    1. 原始数据采集
    2. 确定要发布的内容,也就是说那些图层需要发布
    3. 确定要发布的多少个比例尺范围(比如说,1:2500,1:5000,1:10000…)
    4. 确定特定比例尺对应的图层内容和要素内容以及对应的栅格地图的尺寸
    5. 对每个要确定发布的图层要素配置显示符号,如果必要,也要配置注记
    6. 地图工具根据用户的配置自动生成预发布地图
    7. 地图工作人员根据实际的需要对预发布地图进行适当的调整,生成准地图
    8. 回到4,循环所有的比例尺
    9. 对准地图进行必要的审核,如有必要,进入修改流程
    10. 地图工具发布审核过的地图,向大众发布

地图发布以后,可能需要修改,根据修改内容的不同,我们也有不同的策略

地图修改流程:

    1. 删除一个或多个比例尺的地图,直接删除即可
    2. 删除一个或多个图层,相当于地图重新发布
    3. 增加一个或多个比例尺的发布,进入发布流程4
    4. 增加一个或多个图层,相当于地图重新发布
    5. 修改一个或多个比例尺,相当于删除再增加特定比例尺的地图
    6. 修改(包括增删改)一个或多个要素,审查每个比例尺下的地图受这个要素影响的范围,重新发布这个范围的地图,替换原来的地图
    7. 修改发布的栅格地图的尺寸,修改配置符号,注记,相当于删除再增加

这里的流程还相当粗糙,需要我们进一步的细化.