Category Archives: Uncategorized

Python魅力之地理数据快速处理方法

        Python作为一种成熟的脚本语言,发展势头迅猛,长期徘徊在“编程语言谱”排行前5的位置,在脚本语言中仅次于php。通过Python,开发人员可以快速试验一个想法、研究一种对象属性或不同算法,而无需编译、执行和测试任何代码。正是因为跨平台、简洁、优美的特点,它也如同GIS应用渗透于各各个行业一样,渗透在科学计算的各个领域。在GIS领域,几乎可以说开源言必称Python;而ArcGIS则将Python称为the scientific programming language for GIS:ArcGIS Server的猛将Geoprocessing Service依赖于Python,从9.4将Python IDE集成到ArcMap中也可见一斑。
        前一阵Flyingis贴出了一篇类似的文章,不过还是通过动手完成一个实例,来看看它在ArcMap中的数据快速处理能力吧。描述:上传到Panoramio的照片都会有位置信息(经纬度坐标),有些朋友非常可敬,上传的照片数量可达上万张。分析一下某个用户拍照的活动范围,是件有趣和有意义的事情。思路:1、利用Panoramio的API,获取指定用户的所有照片信息;2、将关心的信息,比如作者、照片名称、照片文件链接提取出来,并将其存储到地理数据库中。
        第一步:获取照片信息。先看一下Panoramio的API,REST风格,返回JSON字符串。比如通过http://www.panoramio.com/map/get_panoramas.php?order=popularity&set=public&from=0&to=20&minx=-180&miny=-90&maxx=180&maxy=90&size=medium,即可按点击次数排序获得世界范围内所有受欢迎的照片。

{
"count": 773840,"photos": [
{
"photo_id": 532693,
"photo_title": "Wheatfield in afternoon light",
"photo_url": "http://www.panoramio.com/photo/532693",
"photo_file_url": "http://static2.bareka.com/photos/medium/532693.jpg",
"longitude": 11.280727,
"latitude": 59.643198,
"width": 500,
"height": 333,
"upload_date": "22 January 2007",
"owner_id": 39160,
"owner_name": "Snemann",
"owner_url": "http://www.panoramio.com/user/39160",
},
{
"photo_id": 505229,
"photo_title": "Etangs près de Dijon",
"photo_url": "http://www.panoramio.com/photo/505229",
"photo_file_url": "http://static2.bareka.com/photos/medium/505229.jpg",
"longitude": 5.168552,
"latitude": 47.312642,
"width": 350,
"height": 500,
"upload_date": "20 January 2007",
"owner_id": 78506,
"owner_name": "Philippe Stoop",
"owner_url": "http://www.panoramio.com/user/78506"
}, ...
]
}


        Panoramio文档说有773840张,但是JSON里的count只有338张,搜其论坛,发现这是一个bug。当然,为了安全起见,一次请求最多只能返回100张照片的信息,Python刚好来完成若干个“100”的拼装任务。代码如下:

import arcgisscripting,json,urllib,os,threading
lphotos=[]#list saves all photo json objects
lock=threading.Lock()

#function to construct the panoramio request url and retrieve the corresponding photos as json array
#for eg: http://www.panoramio.com/map/get_panoramas.php?order=upload_date&set=139922&from=0&to=20&minx=108.355671725&miny=33.807692976&maxx=109.475020647&maxy=34.732003828&size=medium
#set: public (popular photos); full (all photos); user ID number
#size: original; medium (default value) ;small ; thumbnail ; square ; mini_square
def retrievephotos(set,fromindex,toindex,size):
global lphotos,gp
#extent=r"&minx=108.355671725&miny=33.807692976&maxx=109.475020647&maxy=34.732003828"
extent=r"&minx=-180&miny=-90&maxx=180&maxy=90"
url=r"http://www.panoramio.com/map/get_panoramas.php?order=upload_date&set="+set+"&from="+str(fromindex)+"&to="+str(toindex)+extent+"&size="+size
try:
jsonresult=json.read(urllib.urlopen(url).read())
except:
raise Exception,"get panoramio result error"
photos=jsonresult["photos"]
if len(photos)<toindex-fromindex:
print "warning: can't get photos from server "+str(fromindex)+"--"+str(toindex)
#gp.addwarning("warning: can't get photos from server "+str(fromindex)+"--"+str(toindex))#typeerror:'no type' object is not callable
else:
for photo in photos:
lock.acquire()
lphotos.append(photo)
lock.release()
return jsonresult["count"]#the total of photos of the specified set

#use multithread to retrieve photos, the number of threads=(end-start)/100
#start: 0 in usual
#end: the total of photos
def getphotolist(set,start,end,size):
list=[start]
while list[-1]<end:
list.append(list[-1]+100)
threads=[]
gp.addmessage(str(len(list))+" threads has launched...")
for i in list:
threads.append(threading.Thread(target=retrievephotos,args=(set,i,i+100,size),name=str(i)))
for t in threads:
t.start()
for t in threads:
t.join()#suspend the parent(main) thread until this thread is over

        在运行过程中,发送请求(请求100张照片的信息)后等待服务器响应会花费一些时间,于是就采用Python中的多线程,来同时发送多个请求。Threading是Python的一个标准模块,麻雀虽小,五脏俱全。单线程请求时,5000张照片的结果大约需要8分多钟,多线程请求时需要只需要40多秒即可。请求全部被相应后,即完成了所有照片信息的拼装工作:结果存储在类型为List的lphotos变量中。List是Python中使用最广泛的变量之一,灵活好用,一个顶5个~不过这里有个问题没有解决:在子线程的函数中,调用geoprocessor的addmessage()方法会报错(代码中注释掉的一句),非目前能力所能解决。
        题外话:上述多线程的工作也可以用并行计算来解决,这里有一篇出自Flyingis的关于Python中并行计算的文章,供大家学习。随着多核CPU的普及(ps:硅谷小公司推出100核CPU 性能是英特尔四倍),并行计算将会成为开发人员的新宠,微软.NET Framework 4.0中的Parallel Extention也说明了这一点。至于并行计算和多线程之间的区别,援引科班出身的同学胖胖鼠的话:

两者针对性不同。多线程技术是操作系统中的概念,是软件的概念。指进程中的一部分。以前并行计算主要更侧重硬件CPU,现在泛指同时使用多个计算资源解决问题。我感觉两者都用到了并发性并行性的概念。

个人感觉可简单理解为,并行计算是对细枝末节的多线程技术更优的抽象,与一个CPU里的多线程不同,它能利用多个CPU(不同服务器)资源,回归到网格计算的概念。下面继续主题。
        第二步:保存数据。之后我们便可以利用geoprocessor完成数据处理工作。首先通过调用Toolbox中的工具创建存储数据的FeatureClass,并调用工具添加相应的字段:

#create the 9.3 geoprocessor
gp=arcgisscripting.create(9.3)
gp.OverwriteOutput=True
myuserid=gp.getparameterastext(0)#"305320"
gp.Workspace=gp.getparameterastext(1)
myfile=os.path.basename(gp.getparameterastext(2))

#create 2 result featureclasses
try:
gp.addmessage("creating featureclasses...")
sr=gp.CreateObject("SpatialReference")
sr.Createfromfile(r"E:Program FilesArcGISCoordinate SystemsGeographic Coordinate SystemsWorldWGS 1984.prj")
gp.Createfeatureclass(gp.workspace,myfile,"POINT","","","",sr)
gp.Addfield(myfile,"owner","TEXT","","",100)
gp.Addfield(myfile,"title","TEXT","","",100)
gp.Addfield(myfile,"photourl","TEXT","","",150)
#gp.Createfeatureclass(gp.workspace,xafile,"POINT",myfile,"","",sr)
del sr
except:
gp.adderror("creating featureclasses error")
raise Exception,"creating featureclasses error"

#push the photos into the featureclass
gp.addmessage("dealing my photos")
dealphotos(myuserid,myfile)

        最后两句便是整个功能的入口点了。dealphotos函数如下:

#function to push photos into featureclass
def dealphotos(set,fc):
photoscount=retrievephotos(set,0,1,"medium")
gp.addmessage("start to retrieving photos...")
getphotolist(set,0,photoscount,"medium")
gp.addmessage("retrieving photos completed")
cur=gp.InsertCursor(fc)
descfc=gp.Describe(fc)
print "begin insert features..."
gp.addmessage("begin insert features...")
for photo in lphotos:
row=cur.NewRow()
pnt=gp.CreateObject("POINT")
pnt.x=photo["longitude"]
pnt.y=photo["latitude"]
row.Setvalue(descfc.shapefieldname,pnt)
row.Setvalue("owner",photo["owner_name"])
row.Setvalue("title",photo["photo_title"])http://www.blogger.com/post-edit.g?blogID=21848155&postID=4114083803361112294#
row.Setvalue("photourl",photo["photo_file_url"])
cur.InsertRow(row)
print "insert features completed"
gp.addmessage("insert features completed")
del cur,descfc

        好啦,Python部分结束,添加到ArcMap中,便可成为一个Script Tool,与他人分享了。运行界面:

        运行结果:

        似乎不明显,那就利用空间分析中的Point Density工具抽象一下位置分布信息:

        也可以利用Model,将两部分连接起来,做成一个工具来用:

        有了地理数据,就可以继续在其他平台下进行处理和展示了:

最后总结一下标题:通过Python,不需要一个stand-alone的程序,就可以快速试验一个想法。
btw:附上最近google map tile的获取方法:

public override string GetTileUrl(int level, int row, int col)
{
int servernum = (col + 2 * row) % 4;
string sec2 = "&s="; // after &zoom=...
string secword = "Galileo";
int seclen = ((col * 3) + row) % 8; // corrected from int seclen = ( col + 3 * row ) % 8;
sec2 += secword.Substring(0, seclen);

//google maps map:
//http://mt1.google.com/vt/lyrs=m@114&hl=zh-CN&x=13&y=5&z=4&s=Gali
//http://mt1.google.com/vt/lyrs=m@114&hl=zh-CN&x=13&y=6&z=4&s=Galil
//http://mt1.google.com/vt/lyrs=m@114&hl=zh-CN&x=13&y=8&z=4&s=Galileo
string baseUrl = "http://mt" + servernum.ToString() + ".google.com/vt/lyrs=m@114&hl=zh-CN&x=";
string url = baseUrl + col.ToString() + "&y=" + row.ToString() + "&z=" + level.ToString() + sec2;
return url;

////google maps satallite
////http://khm1.google.com/kh/v=49&x=3&y=5&z=4&s=Galile
//string baseUrl = "http://khm" + servernum.ToString() + ".google.com/kh/v=49&x=";
//string url = baseUrl + col.ToString() + "&y=" + row.ToString() + "&z=" + level.ToString() + sec2;
//return url;
}

【转载】依赖房地产为何总是难逃崩盘?

        四个字:良药苦口!
        转自:时寒冰个人维护博客
        围观连接:点这里

        在中国,那些指出现实中的问题,表露出忧虑情绪的人,被称为“唱衰派”——这还算是客气一点的称呼,不客气的干脆就扣上 “伪专家”、“街头理论家”、“卖国贼”之类的帽子。另一方面,那些拼命唱多的,比如提出“买房就是爱国”的学者,则受到吹捧,好像他们才真正有颗“金子般”的心,是为国家着想的。
        所谓良药苦口利于病,阿谀奉承胜于杀人。那些经常鼓吹房地产救国之徒,不是良心早已喂了恶狗,就是成了权势与既得利益集团的奴才,他们制造出一个虚幻的光环,迷惑决策层出台相关政策,然后,他们再趁乱打劫,牟取私利、暴利!
        古今中外的历史告诉我们,凡是一味唱赞歌的,没有一个是好东西。是房地产救了中国吗?不,是毁了中国——毁了国人的幸福,毁了国人的理想,让整个民族的价值观和理想沉沦、迷失。除了房子,这个民族还剩下什么?
        昨天与一位建筑商一起吃饭,他告诉我说,他退出了,不做建筑了。为什么?过去的楼板是10公分,逐渐变成9公分、8公分、7公分、6.5公分、6公分…… 到6公分(在表面看得见的地方还正常)时,他不敢做了,他恐惧了,怕将来栽倒在这上面!一座大楼倒塌,被视为没有质量问题,乃是压力差之故,而倒塌楼房的业主,竟然被建议去买旁边没有倒塌的楼房!有这样宽松的环境,让开发商自己做好质量,实在是太小看他们的胆量和智慧了。环顾四周,凡是大一点的开发商,还有多少人的妻儿没有拿到国外的绿卡或者移民国外?
        楼房质量越差,房价越涨。为何?连开发商都承认,我国的建筑寿命不到30年,30年以后,现在的楼房重新拆毁再建,拆毁楼房会增加GDP,重建又要增加 GDP,GDP继续增长。房子拆了,或者倒下了,或者不能住人了,人们又要抢着买新房,房价再涨,民众财富再被既得利益集团掠去,如此循环不已,仅仅房地产一个领域,就足以支撑一个国家GDP的繁荣。国外的高科技产品,越来越先进,而价格越来越便宜,中国的房屋越建越差,连站都站不稳了,价格却越来越高。相比之下,外国人是何等之“蠢”,中国人是何等之“聪明”!当国人被一个房地产驱赶成可怜的奴隶,这个民族的精神世界在哪里呢?这个民族的归宿感在哪里?
        与溜须拍马之徒不同,“唱衰派”们大都有着良好的专业背景。比如,人们熟悉的曹建海博士,是中国社会科学院工业经济研究所投资与市场研究室主任,其观点有系统的专业知识和大量数据作为支撑,只是,其研究的结论与主流的经济学家不同而已。与那些沦为了既得利益集团代言人所谓主流专家相比,“唱衰派”要真实得多。
        主流的专家已被证明是颓废的垃圾。2009年全国两会期间,全国政协委员四川科伦实业集团有限公司董事长刘革新在发言中说:“主流经济学家的判断已经证明不行了,需要有一个民间独立的机构和他们进行质询,还要淘汰一部分经济学家……我们企业做的事情和主流经济学家的说法反其道而行就行了。”这是对主流学者的最好注脚。
        “唱衰派”学者提出的观点,或许并未得到验证,但是,这并不意味着他们提出的问题不存在。因为,从量变到质变总需要一个过程,如果能够及时冷静地认识到问题并采取措施解决,就能防患于未然,反之,则可能导致严重后果。
        前段时间,由国家旅游局和中共湖北省委宣传部共同主办的“太极湖论坛2009”上,经济学家、中国人民大学黄卫平教授说:“在这个世界上凡是靠房地产拉动的国家,结果没有不是崩盘的,世界老大美国靠房地产拉动经济结局是崩盘;世界的老二日本靠房地产拉动经济结局是崩盘;世界经济中间早已把中国的房地产列为崩盘之列,这次下滑完全归结为错误的房地产政策造成的。在世界上大概只有德国逃过这一劫,凡是这个地球上靠房地产拉动经济的国家结局都是崩盘,尤其当房地产和金融紧密结合成为一种金融衍生工具时,不崩盘那简直就不是经济。”
        这种观点令房地产大佬们不爽,引来批评是难免的。问题是,学者们的研究基于大量事实与数据的基础之上,当那些用房地产拉动经济的国家相继付出惨烈的代价,中国真的能够幸免吗?命运之神,真的会一再眷顾这个信仰缺失的民族吗?
        我不禁要问:除了房地产,支撑中国经济保持可持续增长的力量还有什么?就目前而言,整个世界上除了中国,再也没有把房地产业作为支柱产业发展的。日本敢把色情业作为支柱产业之一,也不敢与民生息息相关的房地产业作为支柱产业。想到这一点,不能不令人感慨!中国的房地产业是寄生或嫁接在建筑业上的,如果将建筑业与之剥离,开发商还剩下什么?在我国房地产产业链条上,规划由政府有关部门做,设计由设计单位负责,住房由建筑企业建造,开发商扮演的只是“中间人” 或“皮包商”的角色。开发商连自有资金都非常少,他们让建筑商垫资为其盖房,然后,拖欠建筑企业的工资,农民工工资拖欠问题由此而来。房屋还没有建好,开发商就预售房屋,回收资金。因此,中国的开发商经常玩空手套白狼的游戏。也正因为中国的开发商只是一群皮包商或食利阶层,所以,他们才可以不问经营管理,每天像明星一样走穴,游走于论坛与电视节目之间,这本身难道不是皮包商的最好证明吗?除了房地产开发行业,还有哪个行业的老总可以如此逍遥?
        支柱产业意味着一种定位或者引导,如果扮演“皮包商”角色的开发商被作为推动中国经济发展的主导,那么,必然把民众引向投机。道德日益滑坡的现状,就是最好的证明。
        在次贷危机的影响仍在延续的大背景下,再次把房地产摆放到“救世主”的位置上,助推房价上涨,进一步蚕食和透支了民众的购买力,这必然挤压其他领域的消费。另一方面,房地产泡沫的快速累积与金融的紧密嫁接,使得整个经济的系统性风险加大。这是其他国家因此自吞苦果的原因,也是他们因此重视民生,不从房地产领域获取暴利的原因。
        中国需要一个能够替代房地产的支柱产业。
        摩根士丹利亚洲主席史蒂芬·罗奇最近撰文指出:“不断飙升的投资对2009年上半年中国GDP增长的贡献率达到了空前的88%——是过去10年43%均值的两倍。同时,中国各银行的贷款质量多数确实受到今年上半年大量信贷投放的影响——这一趋势可能为新一波银行不良贷款播下了种子。”
        7月31日下午,有记者采访我,问我这些“唱衰派”是不是有什么阴谋。我回答说:“有没有阴谋我不知道,但有一点是肯定的,这些专家讲的隐患的确值得我们重视。”“唱衰派”专家的研究,虽然与主流格格不入,却是综合世界经济历史并结合中国国情得出的结论,应该说,这种研究是系统而严谨的。退一步说,即便 “唱衰派”学者的观点一无是处,那么,听听他们的研究,对照一下现实中存在的问题,并加以解决,难道不是理性的态度吗?
        别等到问题集中释放时候才重视“唱衰派”的观点,到那个时候,可能真的晚了。无论是决策者还是投资者,都需要多一个参照,多一分冷静。千万别再妖魔化“唱衰派”,这容易扼杀或掩盖那些理性的声音,使这个国家失去理性反思的最后机会。
        作为热爱这片土地的一个公民,我为这片多灾多难的土地和同样多灾多难的人民祈祷!
        2009年8月3日

ArcGIS API for Silverlight开发入门(6):图层类型小结

        在用Silverlight API开发的过程中,不论是从客户端提交到服务器端的数据,还是从服务器端返回客户端的数据,都要表现在浏览器中,具体的来说是Map控件里。但根据各自类型的不同,比如数据源,地图服务的类型,是否缓存等,决定了它们将处于某个图层里,前面讲过的GraphicsLayer就是一种图层。清楚地认识这些图层类型,对于处理于服务器与客户端之间的地图数据来说是很重要的。
        所有的图层都是从Layer类型继承而来的,可以参考下载的API中的对象模型图。
Layer
  |–TiledMapServiceLayer
  |       |–ArcGISTiledMapServiceLayer
  |–DynamicLayer
  |       |–DynamicMapServiceLayer
  |                 |–ArcGISDynamicMapServiceLayer
  |                 |–ArcGISImageServiceLayer
  |                 |–GPResultImageLayer
  |–GraphicsLayer
  |       |–FeatureLayer
  |–ElementLayer
        下面就按顺序认识一下这些图层吧,也包括Silverlight API中独有的FeatureLayer。

1、Layer:
        继承自Silverlight中的DependencyObject,并实现了INotifyPropertyChanged接口,是Silverlight API中其他图层的基类。可以把它看成麦子,再好吃的凉皮,泡馍都是由它做出来的;

2、TiledMapServiceLayer:
        继承自Layer,是所有使用了缓存的地图服务的基类。通过它可以在程序中加入经过缓存的,来自不同数据源的地图服务。比如ArcGIS Server的地图服务,Google Map的地图,Virtual Earth的地图等;

3、ArcGISTiledMapServiceLayer:
        继承自TiledMapServiceLayer。像上面说的一样,这个图层扩展了TiledMapServiceLayer,于是支持由ArcGIS Server 9.3版本发布的经过缓存的地图服务;又比如ArcGIS Server 9.2版本发布的缓存地图服务不支持REST方式连接,如果要在93的客户端API中使用的话,就可以通过TiledMapServiceLayer扩展一个比如ArcGISTiledMapServiceLayer92,来支持92Server发布的缓存地图服务;

4、DynamicLayer:
        继承自Layer,是动态地图服务的基类;

5、DynamicMapServiceLayer:
        继承自DynamicLayer,对应于TiledMapServiceLayer,要使用未经过缓存的动态地图服务,就得通过扩展这个图层来实现;

6、ArcGISDynamicMapServiceLayer:
        继承自DynamicMapServiceLayer,针对ArcGIS Server 9.3版本发布的动态地图服务。同理,如果要在客户端API中使用其他动态地图服务,比如OGC的WMS服务,则也需要像这个图层一样,扩展上面的DynamicMapServiceLayer来实现;

7、ArcGISImageServiceLayer:
        继承自DynamicMapServiceLayer,针对ArcGIS Server 9.3版本发布的Image Service,因为影像服务也属于动态的地图服务。在客户端API中,可以通过ArcGISImageServiceLayer的一些属性,方便通过浏览器来展示服务器端的影像数据,比如通过BandIds属性,可以快速调整影像数据显示波段的组合(RGB通道),提供不同结果供用户查看。点击这里,查看一个实例;

8、GPResultImageLayer:
        继承自DynamicMapServiceLayer,针对Geoprocessing服务所产生的结果。可以请求服务器端的GP服务将结果动态生成一张图片,将此图片作为GPResultImageLayer图层直接添加到Map控件中;

9、GraphicsLayer:
        继承自Layer,是图形数据集中展现的地方,在第四讲中已经详细讨论过了;

10、FeatureLayer:
        继承自GraphicsLayer,这也是Silverlight API中的亮点之一,通过它可以完成一个比较炫的功能:

        整个过程在xaml中就可以实现,只需要在Map的Layers中插入以下代码即可:
<esri:ArcGISTiledMapServiceLayer ID="StreetMapLayer" Url="http://server.arcgisonline.com/ArcGIS/rest/services/ESRI_StreetMap_World_2D/MapServer"/>
<esri:FeatureLayer ID="featurelayer"
Url="http://sampleserver1.arcgisonline.com/ArcGIS/rest/services/Specialty/ESRI_StatesCitiesRivers_USA/MapServer/0"
Where="POP1990 > 75000" ClusterFeatures="True" FlareBackground="#99FF0000" FlareForeground="White" MaximumFlareCount="9"
FeatureSymbol="{StaticResource markersymbol}">
<esri:FeatureLayer.OutFields>
<sys:String>CITY_NAME</sys:String>
<sys:String>POP1990</sys:String>
</esri:FeatureLayer.OutFields>
<esri:FeatureLayer.MapTip>
<Grid Background="LightYellow">
<StackPanel Margin="5">
<TextBlock Text="{Binding Converter={StaticResource MyDictionaryConverter},
ConverterParameter=CITY_NAME, Mode=OneWay}" FontWeight="Bold" />
<StackPanel Orientation="Horizontal">
<TextBlock Text="Population (1990): " />
<TextBlock Text="{Binding Converter={StaticResource MyDictionaryConverter},
ConverterParameter=POP1990, Mode=OneWay}" />
</StackPanel>
</StackPanel>
<Border BorderBrush="Black" BorderThickness="1" />
</Grid>
</esri:FeatureLayer.MapTip>
</esri:FeatureLayer>

        可以看出这个FeatureLayer其实是将一个Query查询封装到了一个GraphicsLayer中。通过url指定查询的图层,where指定查询条件(也可以输入geometry指定查询的图形),最关键的是ClusterFeatures=”True”,当一个范围内feature过多时,就将他们“聚合”在一起,以一个更大的符号表示出来,进一步放大时才将它们单独显示出来,如果聚合的目标不超过MaximumFlareCount设置的数目,那么就会出现那个flare动画。在MapTip(继承自GraphicsLayer)里面进行了简单的设置,一个背景为黄色的Grid里显示两行文字,用一个DictionaryConverter类将返回的Graphic.Attributes集合中的两个字段转换成String类型显示出来。顺便提一下,FeatureLayer也可以用于线或面层的查询,但如果继续使用ClusterFeatures的话就没什么意义了。虽然FeatureLayer封装的比较死,只能有此一种效果,但它提供给我们一种思路,可以结合SilverlightRIA的特性,充分发挥自己的想象力做出更炫的效果来;但是,对于需要展现海量(成百上千个)点数据的图层来说,ClusterFeatures是一个非常有用的特性,毕竟将这么多点同时呈现出来性能还是有问题的。如果不使用ClusterFeatures,看起来应该是这样的:

        不用FeatureLayer行吗?
        说到FeatureLayer,还有两个Renderer不得不提一下:UniqueValueRenderer和ClassBreakerRenderer。它们都是依托FeatureLayer的,用于单值专题图的渲染。具体的用法都比较简单,可以查看API中的Concepts。但Samples中的Thematic Rendering例子并没有采用这两种Renderer,而是人为地为每个Graphic设置了不同的Symbol。目前看来虽然这两个Renderer有点鸡肋,但毕竟是现在3种客户端API中提供的唯一现成的Renderer,可以猜想也许下个版本的Silverlight API中会有更加成熟的专题图Renderer直接供我们使用;

11、ElementLayer:
        继承自Layer,它可以用来专门呈现Silverlight中原生的FrameworkElement,比如视频,音频等。虽然在FillSymbol的Fill属性中也能利用Brush类来展现一段视频,但毕竟有些“小气”,在ElementLayer中可以大大方方的放置Silverlight元素。你可能会问,在Map控件之外,Grid等布局元素中不是也能放置Silverlight的东西吗,为什么要放在ElementLayer里呢?其实有个问题经常困扰GIS开发人员,就是想让一些非地理数据元素随着地图范围的变化(放大,缩小,平移)而变化,而无须自己在Extent变化后重新计算客户端坐标,手工改变这些元素的位置。瞧,ElementLayer正解决了这个问题。

        目前Beta版的API中暂时有这么多图层类型,以后也许会继续增加。但万变不离其宗,无非就是从那几个基类中派生出来的。所以,下一节我们就通过一个实例来看看如何扩展基类的MapServiceLayer,来达到使用非ArcGIS Server数据源的目的。

ArcGIS API for Silverlight开发入门(5):任务外包——Tasks

        通过上一节的学习,我们已经知道了如何与GraphicLayer交互,但毕竟GIS不是一个画板,所以这节来看一下如何通过Silverlight API完成GIS中的分析功能。
        GIS之所以是一个通用的工具,就是因为它具有各种各样分析和处理数据的能力。Silverlight API中提供了Task,使我们能够轻松完成常见的分析任务。
        先来考虑一下吃饺子的场景。要想吃饺子,我们需要先去买菜,买肉,回家后在厨房里洗菜,揉面, 拌馅,包饺子,煮饺子,吃饺子,之后别忘了洗碗;另一种情况就是去饭馆,告诉服务员我要吃3两茴香,3两韭菜的饺子,然后等着饺子端到你面前,开吃,走人。
        在ArcGIS Server程序开发中,要完成GIS的分析功能其实和吃饺子是一样的。用ADF编程就像在家里吃饺子,除了架设服务器,所有的工作基本上也都得我们自己在服务器端来完成,要处理的地方比较多;而用客户端API编程相当于去外面吃饺子,我们只要把任务交给相应的Task,之后接受结果就行了,不用做饺子。唯一不同的就是在外面吃完饺子别忘了付钱,而用Task完成分析任务则是免费的。这点也体现在使用客户端API中的Task时,是由ArcGIS Online提供给你的,不需要自己购买AGS软件。
        现在来看看Silverlight API目前给我们提供了那些Task功能:
Query:能够在已经发布的服务数据中,通过属性条件(可以属性字段中进行关系判断,字符查找等),图形条件(与输入的图形相交、包含、相离等),或者是两者的组合,查询出满足条件的数据并返回。相当于Engine中的SpatialFilter,当然也是QueryFilter。
Find:在地图数据的属性字段中查找包含有关键字参数的数据并返回。
Identity:对鼠标当前点击位置上的数据进行辨识并返回结果,可以对多个图层的数据进行辨识。
Address Locator:输入经纬度,返回地址结果(Geocoding);输入一个地方的地址,返回经纬度结果(Reverse Geocoding)。由于国内地图数据保密工作做的相当好,这个Task暂时用不到。
Geometry Service:可以对输入的地理数据进行如缓冲区,动态投影,面积/周长量算等几何操作。
Geoprocessing:能够完成复杂的GIS任务,类似ToolBox中的工具。
        抽象一下,可以看出,Query完全可以完成Identity和Find的工作,但后两者在特定场合下使用起来比Query要方便的多;Geoprocessing完全可以替代Geometry Service,但是在利用REST API编写的程序中,要尽量使用Geometry Service。
        再抽象一下,Silverlight API中的这几个Task和JavaScript/Flex API中的Task是大同小异的,因为其实它们都是AGS 9.3 REST API中暴露出来的操作资源(Operation Resource)见下图:

        后面的代码中实际上也是把输入参数封装起来提交到了REST API的特定Endpoint上。要理解好客户端API中的Task,建议熟读AGS的REST SDK
        Task的用法基本上相同,都遵循这几个步骤:初始化Task,设置Task所需参数,提交任务,等待服务器完成任务后,处理返回的结果;进饭馆,想好你要吃什么饺子,告诉服务员,等饺子做好端上来,开始吃。好了,下面我们就通过一个实例(点击这里,查看实例),来学习一下Query和Geometry两个Task的用法。

        首先选择工具条中的画线工具,在屏幕上画一条曲线,会根据曲线自动生成一个距离100公里的缓冲区显示在地图上,之后开始查询缓冲区图形经过的州(相交),将结果显示在地图上。可以单击每个州查看详细信息。这里假设你已学习了前几节的内容,只讨论Task用法的部分。
1、利用所画的线生成缓冲区。画线利用的是Draw工具中的Freehand,在这个动作完成后会触发Draw的OnDrawCompleted事件,自然可以在这里开始进行缓冲区的工作,用的是Geometry Service里的Buffer。
        初始化Geometry Service。假设已经在Map1中添加了ID为glayerResult的GraphicsLayer,linesymbolred是提前设置好的CartographicLineSymbol:
private void Draw1_OnDrawComplete(object sender, DrawEventArgs args)
{
Draw1.Deactivate();//Freehand动作失效
//将Freehand画的曲线显示在地图上
GraphicsLayer glayer = Map1.Layers["glayerResult"] as GraphicsLayer;
Graphic g = new Graphic();
g.Symbol = linesymbolred;
g.Geometry = args.Geometry;
glayer.Graphics.Add(g);
//初始化Geometry Service
GeometryService geometrytask = new GeometryService("http://sampleserver1.arcgisonline.com/ArcGIS/rest/services/Geometry/GeometryServer");
}

        Geometry Service的初始化使用构造函数来完成的,里面接受一个URL,这个是Geometry Service的REST API Endpoint。顺便说一下,不同于其他服务比如MapService,一个GIS Server只能发布一个GeometryService,并且它的名称必须是Geometry。
        当一个Task完成时会触发Completed事件,失败时也有Failed事件,对这两个事件进行监听:
geometrytask.BufferCompleted += new EventHandler(geometrytask_BufferCompleted);
geometrytask.Failed += new EventHandler(geometrytask_Failed);

        设置Buffer操作所需的参数:
BufferParameters bufferparameters = new BufferParameters();
bufferparameters.Unit = LinearUnit.Kilometer;
//必须指定下面两个spatialreference,否则buffer结果集为空
bufferparameters.BufferSpatialReference = new SpatialReference(3395);
bufferparameters.OutSpatialReference = Map1.SpatialReference;
bufferparameters.Distances.Add(100);
bufferparameters.Features.Add(g);

        BufferParameters是专门用于Buffer的参数;BufferSpatialReference是将要Buffer的图形重新投影到这个坐标系下(常常需要根据地图数据所在地方的情况来设置这个参数),并设置Buffer距离的单位为公里,Buffer的输出一般与地图坐标系一致;Buffer参数有一个Features属性,是List类型,里面的Graphic都将被Buffer。下来将Buffer的任务提交到服务器(可以看出为什么这些动作要叫Task):
geometrytask.BufferAsync(bufferparameters);
        以上代码都放在Draw1_OnDrawComplete函数中。任务提交到服务器后,由Geometry Service接管,计算,完成后会立刻将结果返回给我们,通知我们结果已经完成的方式就是前面绑定的Completed事件。接收到结果后,首先将缓冲区显示出来:
private void geometrytask_BufferCompleted(object sender, GraphicsEventArgs args)
{
if (args.Results.Count>0)
{
GraphicsLayer glayer = Map1.Layers["glayerResult"] as GraphicsLayer;
Graphic g = new Graphic();
g.Symbol = fillsymbolBuffer;
g.Geometry = args.Results[0].Geometry;
glayer.Graphics.Add(g);
}
}

        如图:

2、利用生成缓冲区的缓冲区进行空间查询。要达到我们的目的,就还需要进行一个Query的Task,那么就可以在这里马不停蹄的开始Query的Task。步骤基本都是一样的,初始化,设置参数,提交结果,处理结果:
private void geometrytask_BufferCompleted(object sender, GraphicsEventArgs args)
{
if (args.Results.Count>0)
{
GraphicsLayer glayer = Map1.Layers["glayerResult"] as GraphicsLayer;
Graphic g = new Graphic();
g.Symbol = fillsymbolBuffer;
g.Geometry = args.Results[0].Geometry;
glayer.Graphics.Add(g);
//初始化QueryTask
QueryTask querytask = new QueryTask("http://sampleserver1.arcgisonline.com/ArcGIS/rest/services/Demographics/ESRI_Census_USA/MapServer/5");
//准备接收结果或者处理失败的通知
querytask.ExecuteCompleted += new EventHandler(querytask_ExecuteCompleted);
querytask.Failed += new EventHandler(querytask_Failed);
//设置Query Task所需的参数
Query query = new Query();
query.OutFields.Add("*");//也顺便设置了query.ReturnGeometry=true;
query.Geometry = g.Geometry;
query.SpatialRelationship = SpatialRelationship.esriSpatialRelIntersects;
//向服务器上的对应图层提交任务
querytask.ExecuteAsync(query);
Map1.Cursor = System.Windows.Input.Cursors.Wait;
}

}
        这里的查询实在美国州的图层上进行的,详细信息将QueryTask构造函数里的那个参数输入浏览器查看;query.Geometry是设置需要进行空间查询的图形,就是上面缓冲区的结果;OutFields是查询结果需要返回的字段,这里返回全部字段,如果返回全部字段,则强制设置了ReturnGeometry为true,如果我们不需要处理结果的图形信息,则可以将这个参数设为false,以节省流量,显然这里不是;空间关系可以参考API,与Engine中的完全一致。
        接下来处理QueryTask完成后的结果:
private void querytask_ExecuteCompleted(object sender, QueryEventArgs args)
{
GraphicsLayer graphicslayer = Map1.Layers["glayerResult"] as GraphicsLayer;
FeatureSet featureset = args.FeatureSet;
if (featureset != null && featureset.Features.Count > 0)
{
graphicslayer.ClearGraphics();
listboxResults.Items.Clear();
foreach (Graphic graphic in featureset.Features)
{
graphic.Symbol = fillsymbolresult;
graphicslayer.Graphics.Add(graphic);
}
}
MyMapTip.GraphicsLayer = graphicslayer;
Map1.Cursor = System.Windows.Input.Cursors.Arrow;
}

        上面处理空间查询的结果只是将图形显示了出来,那么对于单击某个州后,显示出其详细信息该怎么办呢?从图一可以看出,用到了Silverlight的DataGrid控件,信息从哪里去呢?记得上面我们设置结果中返回的全部属性字段吗?它们存储在每个Graphic的Attributes属性中。要么绑定到DataGrid里,要么一条条添加……你可能已经发现了这条语句MyMapTip.GraphicsLayer = graphicslayer;,还记得第三节的Widgets吗?那里我们落下了MapTip这个小家伙,现在派上用场了。除了在这里设置MapTip的GraphicsLayer属性外,在xaml中有如下的定义:
<esriWidgets:MapTip x:Name="MyMapTip" BorderBrush="#99000000"
BorderThickness="1" Title="详细信息" VerticalOffset="10"
HorizontalOffset="10" Background="#DDFFFFFF" />

        仅此而已。MapTip会自动找寻自己GraphicsLayer中的Graphic,当鼠标悬停在某个Grpahic上时,会自动读取它的Attributes属性并显示,小玩具又发挥了大作用。
        别忘了万一处理任务失败时的提示:
private void geometrytask_Failed(object sender, TaskFailedEventArgs args)
{
MessageBox.Show("Buffer Error:" + args.Error);
}

private void querytask_Failed(object sender, TaskFailedEventArgs args)
{
MessageBox.Show("Query failed: " + args.Error);
Map1.Cursor = System.Windows.Input.Cursors.Arrow;
GraphicsLayer graphicslayer = Map1.Layers["glayerResult"] as GraphicsLayer;
graphicslayer.ClearGraphics();
}

        本节内容完毕。上面讲的相对简略,要理解各个Task和参数的用法,还是需要熟悉Silverlight API和前面提到的REST API。另外,Geoprocessing Service实际上是最强大Task,如果有自己的GIS Server,完全可以在上面发布自制的Model或者Python脚本,以完成各种GIS分析任务,简单的在线编辑也是可能的。它的用法也万变不离其宗:初始化,设置参数,提交任务,处理结果。不同的是Geoprocessing Service有两种提交任务的方法:同步和异步。前者服务器端处理完任务后会立即将结果发送回客户端;后者将任务提交后会得到服务器端返回的一个JobID,即使任务处理完成也不会立即返回,而是需要你拿这个JobID去询问服务器:完成了吗?完成了吗?完成了吗?如果完成,则可以取回相应的结果。
        前面说到,虽然去外面吃饺子很方便,但是毕竟那是人家做好的,对于老饕来说还需要自己的口感,自己下厨毕竟能控制整个过程的方方面面,哪怕你想做出饺立方也都是有可能的。同样,ADF编程可以调用服务器端的ArcObjects,让你为所欲为,这点是客户端API无论如何也办不到的。
        下一节我们来对Silverlight API中的图层类型做一个小结。

ArcGIS API for Silverlight开发入门(2):一个基础地图实例

        这节在一个地图实例的基础上,来对Silverlight API中的一些基本概念做一个总体了解,顺便熟悉一下Silverlight的开发知识。
        点击这里,直接看效果。


        根据上一节的知识,可以知道这个Silverlight程序里包含了一个Map控件,并且里面至少有一个WorldImagery的图层。那么Page.xaml里的关键代码开起来应该是这样的:
<Grid x:Name="LayoutRoot">
<esri:Map x:Name="Map1">
<esri:Map.Layers>
<esri:ArcGISTiledMapServiceLayer ID="WorldImageLayer" x:Name="WorldImageLayer" Initialized="WorldImageLayer_Initialized"
Url="http://services.arcgisonline.com/ArcGIS/rest/services/ESRI_Imagery_World_2D/MapServer" />
</esri:Map.Layers>
</esri:Map>
</Grid>

所有的布局工作都在一个Grid中进行,给它起个名字叫LayoutRoot。Grid里面放了一个esri:Map元素(Map控件),它继承自Silverlight的Control,所以拥有Width和Height属性,默认是Auto,自动填充整个Grid。Map.Layers是一个集合,可以往里面添加layer,这里的layer指的是ArcGIS Server或其他软件发布的地图服务,目前Silverlight API中支持的能够直接使用的有ArcGISDynamicMapServiceLayer,ArcGISTiledMapServiceLayer,ArcGISImageServiceLayer,分别对应ArcGIS Server发布的动态地图服务,缓存地图服务(两种Map Service)和Image Service,这三种图层是拿来即用的,如果你想加入别的地图服务,比如WMS服务,则需要自己继承相应类型的的Layer;此外还有GraphicsLayer,ElementLayer,Silverlight API特有的FeatureLayer等。这些都会在之后的小节中讲到。强调一下,与ADF开发里MapResourceManager一样,在Map中加入的内容实际上是地图服务,但当做一个layer处理。
        下面就对这个例子中的每一部分来做说明(与上图中的序号相对应)。

1、当地图移动时获取地图范围。
        当地图范围改变后,显示出当前地图范围的边界值。
        这部分的页面布局是这样的:
<Grid x:Name="Gridright" Margin="0,15,20,0" HorizontalAlignment="Right" VerticalAlignment="Stretch">
<!--extent-->
<Canvas Width="215" Height="110" VerticalAlignment="Top">
<Rectangle Style="{StaticResource rectBottom}" />
<Rectangle Style="{StaticResource rectMiddle}" />
<Rectangle Style="{StaticResource rectTop}" />
<TextBlock x:Name="TBextent" Margin="20,15,15,0" Text="范围:" TextWrapping="Wrap" FontWeight="Bold" />
</Canvas>
</Grid>

        有关xaml中详细的布局知识请大家参照其他例子学习,这里稍作讲解。外面的Gridright这个Grid就是页面右边1、2、3、6的父容器,之所以不用StackPanel是因为6需要贴着页面底部,StackPanel中的元素都会flow贴到一起。三个矩形组合便构成了整体轮廓,由于它们都在一个Canvas中,所以会产生压盖效果。最先加入的rectBottom这个矩形便是最底下的阴影效果,中间的矩形是蓝色框,最上面的矩形是白色的文字显示区域。“{ }”里的内容在xaml中称作markup extention,StaticResource是使用在别处已经定义好的资源(resource)来对本元素的一些属性进行自动赋值,这里用来修饰Rectangle的外观。xaml中除了StaticResource这种markup extention之外还有Binding和TemplateBinding两种markup extention,分别用于数据绑定(data binding)和自定义control的外观。上面的StaticResource是在App.xaml中定义的,这样就可以在本工程的任何页面中使用,当然也可以定义为LayoutRoot这个Grid的Resource。贴出来大家一看就明白了:
<Application.Resources>
<Style x:Key="rectBottom" TargetType="Rectangle">
<Setter Property="RadiusX" Value="10" />
<Setter Property="RadiusY" Value="10" />
<Setter Property="Fill" Value="#22000000" />
<Setter Property="Canvas.Left" Value="5" />
<Setter Property="Canvas.Top" Value="5" />
<Setter Property="Width" Value="215" />
<Setter Property="Height" Value="110" />
</Style>
<Style x:Key="rectMiddle" TargetType="Rectangle">
<Setter Property="RadiusX" Value="10" />
<Setter Property="RadiusY" Value="10" />
<Setter Property="Fill" Value="#775C90B2" />
<Setter Property="Canvas.Left" Value="0" />
<Setter Property="Canvas.Top" Value="0" />
<Setter Property="Width" Value="215" />
<Setter Property="Height" Value="110" />
<Setter Property="Stroke" Value="Gray" />
</Style>
<Style x:Key="rectTop" TargetType="Rectangle">
<Setter Property="RadiusX" Value="5" />
<Setter Property="RadiusY" Value="5" />
<Setter Property="Fill" Value="#FFFFFFFF" />
<Setter Property="Canvas.Left" Value="10" />
<Setter Property="Canvas.Top" Value="10" />
<Setter Property="Width" Value="195" />
<Setter Property="Height" Value="90" />
<Setter Property="Stroke" Value="DarkGreen" />
</Style>
</Application.Resources>

        它们就相当于网页中的css。如果不使用StaticResource,那么三个矩形看起来应该是这样的:
<Rectangle RadiusX="10" RadiusY="10" Fill="#22000000" Canvas.Left="5" Canvas.Top="5" Width="215" Height="110" />
<Rectangle RadiusX="10" RadiusY="10" Fill="#775C90B2" Canvas.Left="0" Canvas.Top="0" Width="215" Height="110" Stroke="Gray" />
<Rectangle RadiusX="5" RadiusY="5" Fill="#FFFFFFFF" Canvas.Left="10" Canvas.Top="10" Width="195" Height="90" Stroke="DarkGreen" />

        你猜的没错,在其他矩形框部分也使用到了这些属性。通过实践可以感受到,xaml中的布局在一般使用中比html+css的布局要简单和灵活许多。好了,继续。
        Map控件里面已经封装了一些事件来供我们使用,我们可以在需要的时候捕获它们来进行处理。如果做过ArcGIS产品的二次开发,你应该已经想到我们要捕获的就是Map的ExtentChanged事件;而要在地图移动或者缩放的过程中也实时显示地图范围,则还要对ExtentChanging事件做处理。细心的你可能已经发现,在上面的xaml代码中已经对世界地图这个图层的Initialized事件添加了一个hanlder:WorldImageLayer_Initialized。当然可以像这样一样给Map的这两个事件添加handler,但这里并不这么做,而是在世界地图图层的Initialized事件里来绑定它们(移动地图时出发ExtentChanged事件,网速过慢导致图层并未加入到Map中,则会报错)。来看看Page.xaml.cs中的code-behind代码:
private void WorldImageLayer_Initialized(object sender, EventArgs e)
{
Map1.ExtentChanged += new EventHandler<ESRI.ArcGIS.ExtentEventArgs>(Map1_ExtentChange);
Map1.ExtentChanging += new EventHandler<ESRI.ArcGIS.ExtentEventArgs>(Map1_ExtentChange);
}

        没错,把两个事件绑定到同一个handler即可。再看看Map1_ExtentChange中的代码:
private void Map1_ExtentChange(object sender, ESRI.ArcGIS.ExtentEventArgs e)
{
TBextent.Text = string.Format("地图范围:
MinX:{0}
MinY:{1}
MaxX:{2}
MaxY:{3}",
e.NewExtent.XMin, e.NewExtent.YMin, e.NewExtent.XMax, e.NewExtent.YMax);
}

        很简单吧?顺便提一下,ExtentEventArgs里既然有NewExtent,当然就有OldExtent了,通过比较这两个变量就可以分析出当前进行的是放大、缩小还是平移操作了。其实还有个更简单的办法,查查看Map的Resolution属性吧。
对于Silverlight API中内容,是不是感觉很容易呢(当然你得做够xaml的功课才行)?那么赶快来看第二部分。

2、当鼠标移动时获取鼠标坐标。
        包括屏幕坐标和地图坐标。外观样式方面是这样的:
<!--mouse coords-->
<Canvas Width="215" Height="110" Margin="0,120,0,0" VerticalAlignment="Top">
<Rectangle Style="{StaticResource rectBottom}" />
<Rectangle Style="{StaticResource rectMiddle}" />
<Rectangle Style="{StaticResource rectTop}" />
<StackPanel Orientation="Vertical" Margin="20,15,15,0">
<TextBlock x:Name="TBscreencoords"
HorizontalAlignment="Left" VerticalAlignment="Center" Text="屏幕坐标:" TextWrapping="Wrap" FontWeight="Bold" />
<TextBlock x:Name="TBmapcoords"
HorizontalAlignment="Left" VerticalAlignment="Center" Text="地图坐标:" TextWrapping="Wrap" FontWeight="Bold" />
</StackPanel>
</Canvas>

        那么接下来要捕捉那个事件呢?当然就是MouseMove啦。不过如果查看Silverlight API中的Map类,发现并没有这个事件。但要记住Map是继承自xaml中的Control,Control继承自FrameworkElement,FrameworkElement继承自UIElement,这里就有一个MouseMove事件了。所以Map控件的MouseMove是xaml中而不是Siverlight API中的事件(当然整个Silverlight API都是建立在xaml基础上的)。在esri:Map标签中添加一个MouseMove事件(MouseMove=”Map1_MouseMove”),来看看code-behind代码:
private void Map1_MouseMove(object sender, MouseEventArgs e)
{
if (Map1.Extent != null)
{
System.Windows.Point screenPnt = e.GetPosition(Map1);
TBscreencoords.Text = string.Format("屏幕坐标:
X:{0},Y:{1}", screenPnt.X, screenPnt.Y);

ESRI.ArcGIS.Geometry.MapPoint mapPnt = Map1.ScreenToMap(screenPnt);
TBmapcoords.Text = string.Format("地图坐标:
X:{0}
Y:{1}", Math.Round(mapPnt.X, 4), Math.Round(mapPnt.Y, 4));
}
}

        可以看到Map控件提供了屏幕与地图坐标之间转换的方法,好比开发人员的一座桥梁,用来往返于Silverlight特性与地图之间,非常方便。需要说明的是,这里GetPosition(Map1)获得的屏幕坐标是相对于Map控件的,而不是显示器的左上角。ok,继续来看第三部分。

3、Map里的动画效果。
        当地图放大和平移时都可以看到平滑的效果,这归功于Silverlight的动画功能。Map在封装完动画效果后,给了我们两个属性来对它们进行设置:PanDuration和ZoomDuration,用于设置这两个动作持续的时间。它们都是TimeSpan类型的变量,合理的设置可以带来良好的用户体验。看看这部分的布局:
<!--map animation slider-->
<Canvas Width="215" Height="130" Margin="0,240,0,0" VerticalAlignment="Top">
<Rectangle Style="{StaticResource rectBottom}" Height="130" />
<Rectangle Style="{StaticResource rectMiddle}" Height="130" />
<Rectangle Style="{StaticResource rectTop}" Height="110" />
<StackPanel Orientation="Vertical" Margin="20,15,15,0">
<TextBlock HorizontalAlignment="Left" Text="设置地图缩放动作持续时间:" TextWrapping="Wrap" FontWeight="Bold" />
<TextBlock x:Name="TBzoomdurationvalue" HorizontalAlignment="Left" Text="当前值:" TextWrapping="Wrap" FontWeight="Bold" />
<Slider x:Name="sliderzoomanimation" Orientation="Horizontal" Minimum="0" Maximum="20" SmallChange="1"
LargeChange="5" Cursor="Hand" ValueChanged="slideranimation_ValueChanged" Width="180" />
<TextBlock HorizontalAlignment="Left" Text="设置地图平移动作持续时间:" TextWrapping="Wrap" FontWeight="Bold" />
<TextBlock x:Name="TBpandurationvalue" HorizontalAlignment="Left" Text="当前值:" TextWrapping="Wrap" FontWeight="Bold" />
<Slider x:Name="sliderpananimation" Orientation="Horizontal" Minimum="0" Maximum="20" SmallChange="1"
LargeChange="5" Cursor="Hand" ValueChanged="slideranimation_ValueChanged" Width="180" />
</StackPanel>
</Canvas>

        主要用到了两个slider控件。再看看拖动滑块时的事件代码,为了省事,这两个事件也用了同一个handler:
private void slideranimation_ValueChanged(object sender, RoutedPropertyChangedEventArgs e)
{
Slider s=sender as Slider;
if (s.Name == "sliderzoomanimation")
{
Map1.ZoomDuration = new TimeSpan(0, 0, Convert.ToInt32(sliderzoomanimation.Value));
TBzoomdurationvalue.Text = string.Format("当前值:{0}秒", Convert.ToInt32(sliderzoomanimation.Value));
}
else
{
Map1.PanDuration = new TimeSpan(0, 0, Convert.ToInt32(sliderpananimation.Value));
TBpandurationvalue.Text = string.Format("当前值:{0}秒", Convert.ToInt32(sliderpananimation.Value));
}
}

        对应着地图效果,应该很容易理解。继续第四部分。

4、对地图服务可见性与动态地图服务中图层可见性的控制。
        还是要强调一下,WorldImagery和StreetMap两个能看到的地图实际上都是地图服务,当作layer加入到了Map控件中;而动态地图服务USA中的图层Cities,Rivers,States才是与ArcMap中图层相对的概念。对于WorldImagery和StreetMap之间的切换,主要用到了Silverlight API里Layer的
Visible属性;而动态服务中图层可见性的操作,主要是对ArcGISDynamicMapServiceLayer的VisibleLayers数组做了设置。
        StreetMap这个服务其实一开始就加入了地图(在esri:Map标签中):
<esri:ArcGISTiledMapServiceLayer ID="StreetMapLayer"
Url="http://services.arcgisonline.com/ArcGIS/rest/services/ESRI_StreetMap_World_2D/MapServer" Visible="False" />

        而设置了Visible=”False”。图层不可见时地图不会对它做任何处理,所以不用担心会耗费流量或加重程序负担。
        看看布局部分:
<StackPanel HorizontalAlignment="Left" Margin="20,15,0,0">
<Canvas x:Name="Canvasleft" Width="165" Height="90" HorizontalAlignment="Left" VerticalAlignment="Top">
<Rectangle Style="{StaticResource rectBottom}" Width="165" Height="90" />
<Rectangle Style="{StaticResource rectMiddle}" Fill="#7758FF00" Width="165" Height="90" />
<Rectangle Style="{StaticResource rectTop}" Width="145" Height="70" />
<!--change layer-->
<StackPanel Margin="20,15,15,0">
<TextBlock Text="切换图层:" TextWrapping="Wrap" FontWeight="Bold" />
<StackPanel Orientation="Horizontal">
<ToggleButton x:Name="TBimagery" Content="Imagery" Click="TBimagery_Clicked" Cursor="Hand" />
<ToggleButton x:Name="TBstreetmap" Content="StreetMap" Click="TBstreetmap_Clicked" Cursor="Hand" />
</StackPanel>
<CheckBox Margin="0,5,0,0" x:Name="chkboxDynamicLayer" Content="添加一个动态图层吧" IsChecked="False" Click="chkboxDynamicLayer_Click" Cursor="Hand" />
</StackPanel>
</Canvas>
</StackPanel>

        这里使用了ToggleButton,CheckBox和RadioButton都由它派生而来。Silverlight 2中的ToggleButton不能设置Group(一个Group中自动限定同时只能有一个控件处于激活状态),不如Flex里的ToggleButton来的方便,所以code-behind中多做了些工作。当然这里使用RadioButton也是可以的。
private void TBimagery_Clicked(object sender, RoutedEventArgs e)
{
if (TBstreetmap.IsChecked==true)
{
Map1.Layers["WorldImageLayer"].Visible = true;
Map1.Layers["WorldImageLayer"].Opacity = 0;
TBstreetmap.IsChecked = false;
Storyboard sbworldmapshow = makestoryboard("WorldImageLayer", 0, 1);
Storyboard sbstreetmaphide = makestoryboard("StreetMapLayer", 1, 0);
sbworldmapshow.Begin();
sbstreetmaphide.Begin();
hidelayername = "StreetMapLayer";
timer.Begin();
}
TBimagery.IsChecked = true;
}

private void TBstreetmap_Clicked(object sender, RoutedEventArgs e)
{
if (TBimagery.IsChecked==true)
{
Map1.Layers["StreetMapLayer"].Visible = true;
Map1.Layers["StreetMapLayer"].Opacity = 0;
TBimagery.IsChecked = false;
Storyboard sbstreetmapshow = makestoryboard("StreetMapLayer", 0, 1);
Storyboard sbworldmaphide = makestoryboard("WorldImageLayer", 1, 0);
sbstreetmapshow.Begin();
sbworldmaphide.Begin();
hidelayername = "WorldImageLayer";
timer.Begin();
}
TBstreetmap.IsChecked = true;
}

private void timer_Tick(object sender, EventArgs e)
{
Map1.Layers[hidelayername].Visible = false;
}

public Storyboard makestoryboard(string layername, double from, double to)
{
Storyboard sb = new Storyboard();
ESRI.ArcGIS.ArcGISTiledMapServiceLayer layer = Map1.Layers[layername] as ESRI.ArcGIS.ArcGISTiledMapServiceLayer;
DoubleAnimation doubleAnim = new DoubleAnimation();
doubleAnim.Duration = new TimeSpan(0, 0, 5);
doubleAnim.From = from;
doubleAnim.To = to;
Storyboard.SetTarget(doubleAnim, layer);
Storyboard.SetTargetProperty(doubleAnim, new PropertyPath("Opacity"));
sb.Children.Add(doubleAnim);

return sb;
}

        当切换两个地图服务时能够看到一个渐变的效果,这里用到了Silverlight中的动画,它们都是在StoryBoard里面进行的,以后的小节中会讲Silverlight中的动画,这里不再废话了,有兴趣的朋友可以自己参考帮助学习。hidelayername是这个一个公用的string变量,用来在切换的动画效果完成后设置不可见的图层Visible属性。timer也是一个StoryBoard:
<Storyboard x:Name="timer" Completed="timer_Tick" Duration="0:0:5" />
        这里可以看出把StoryBoard也能巧妙的用作计时器。到了特定时间(5秒)后会自动timer_Tick函数,当然也可以使用.net中的各种timer类。
        下面是添加动态服务的部分。
private void chkboxDynamicLayer_Click(object sender, RoutedEventArgs e)
{
if (chkboxDynamicLayer.IsChecked == true)
{
Map1.Layers.Add(california);
Map1.ZoomTo(california.FullExtent);

if (california.IsInitialized == false)
{
chkboxDynamicLayer.IsEnabled = false;
}
chkboxDynamicLayer.Content = "去掉它";
SVlayers.Visibility = Visibility.Visible;
}
else
{
Map1.Layers.Remove(california);
chkboxDynamicLayer.Content = "添加一个动态图层吧";
SVlayers.Visibility = Visibility.Collapsed;
}
}

private void dynamiclayer_initialized(object s, EventArgs e)
{
//若图层没有初始化好就移除图层,当然会报错了,所以这样做就不会了
chkboxDynamicLayer.IsEnabled = true;
Map1.ZoomTo(california.InitialExtent);
SVlayers.Visibility = Visibility.Visible;
california.ID = "layercalifornia";

ESRI.ArcGIS.ArcGISDynamicMapServiceLayer dynamicServiceLayer = s as ESRI.ArcGIS.ArcGISDynamicMapServiceLayer;
if (dynamicServiceLayer.VisibleLayers == null)
dynamicServiceLayer.VisibleLayers = GetDefaultVisibleLayers(dynamicServiceLayer);
UpdateLayerList(dynamicServiceLayer);
}

        当添加了动态服务后,会自动弹出一个listbox,当然这些也都是在xaml中定义好的(加在上面的Canvas后面):
<ScrollViewer x:Name="SVlayers" Width="165" Visibility="Collapsed" Height="120">
<ListBox x:Name="LayerVisibilityListBox" >
<ListBox.ItemTemplate>
<DataTemplate>
<CheckBox Margin="2" Name="{Binding LayerIndex}" Content="{Binding LayerName}"
Tag="{Binding ServiceName}" IsChecked="{Binding Visible}"
ClickMode="Press" Click="chkboxToggleVilible_Click" />
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</ScrollViewer>

        这里把ListBox放到了ScrollVierwer中,固定了它的高度,当内容过多时可以自动显示纵向滚动条。这里要提一下,ListBox的内容用到了数据绑定(参考xaml中的Data Binding,有OneTime,OneWay和TwoWay三种模式,这里使用的是默认的OneTime),看起来里面只有一个CheckBox,但它相当于一个模板,在code-behind中设置了ListBox.ItemSource之后,根据该属性的内容自动生成多个CheckBox。代码中自定义了一个LayerListData类,它的几个属性分别与上面的CheckBox属性绑定;将一个List赋给了ListBox.ItemSource,则会自动生成ListBox中的内容。通过一个List类型变量,来控制动态服务的可见图层。代码如下:
public class LayerListData
{
public bool Visible { get; set; }
public string ServiceName { get; set; }
public string LayerName { get; set; }
public int LayerIndex { get; set; }
}

private int[] GetDefaultVisibleLayers(ESRI.ArcGIS.ArcGISDynamicMapServiceLayer dynamicService)
{
List<int> visibleLayerIDList = new List<int>();

ESRI.ArcGIS.LayerInfo[] layerInfoArray = dynamicService.Layers;

for (int index = 0; index < layerInfoArray.Length; index++)
{
if (layerInfoArray[index].DefaultVisibility)
visibleLayerIDList.Add(index);
}

return visibleLayerIDList.ToArray();
}

private void UpdateLayerList(ESRI.ArcGIS.ArcGISDynamicMapServiceLayer dynamicServiceLayer)
{
int[] visibleLayerIDs = dynamicServiceLayer.VisibleLayers;

if (visibleLayerIDs == null)
visibleLayerIDs = GetDefaultVisibleLayers(dynamicServiceLayer);

List<LayerListData> visibleLayerList = new List<LayerListData>();

ESRI.ArcGIS.LayerInfo[] layerInfoArray = dynamicServiceLayer.Layers;

for (int index = 0; index < layerInfoArray.Length; index++)
{
visibleLayerList.Add(new LayerListData()
{
Visible = visibleLayerIDs.Contains(index),
ServiceName = dynamicServiceLayer.ID,
LayerName = layerInfoArray[index].Name,
LayerIndex = index
});
}

LayerVisibilityListBox.ItemsSource = visibleLayerList;
}

void chkboxToggleVilible_Click(object sender, RoutedEventArgs e)
{
CheckBox tickedCheckBox = sender as CheckBox;

string serviceName = tickedCheckBox.Tag.ToString();
bool visible = (bool)tickedCheckBox.IsChecked;

int layerIndex = Int32.Parse(tickedCheckBox.Name);

ESRI.ArcGIS.ArcGISDynamicMapServiceLayer dynamicServiceLayer = Map1.Layers[serviceName] as
ESRI.ArcGIS.ArcGISDynamicMapServiceLayer;

List<int> visibleLayerList =
dynamicServiceLayer.VisibleLayers != null
? dynamicServiceLayer.VisibleLayers.ToList() : new List<int>();

if (visible)
{
if (!visibleLayerList.Contains(layerIndex))
visibleLayerList.Add(layerIndex);
}
else
{
if (visibleLayerList.Contains(layerIndex))
visibleLayerList.Remove(layerIndex);
}

dynamicServiceLayer.VisibleLayers = visibleLayerList.ToArray();
}

5、比例尺。
        Silverlight API提供了一个ScaleBar类,可以方便的设置地图比例尺。
<!--scale bar 放在LayoutRoot Grid中-->
<Canvas HorizontalAlignment="Left" VerticalAlignment="Bottom" Margin="10,0,0,20">
<esri:ScaleBar x:Name="scalebar" MapUnit="DecimalDegrees" DisplayUnit="Kilometers" Foreground="Black" FillColor1="White" FillColor2="Blue" />
</Canvas>

        需要在初始化的时候设置scalebar的Map属性,顺便来看看整个页面的初始化工作:
namespace demo_02_extendedmap
{
public partial class Page : UserControl
{
private ESRI.ArcGIS.ArcGISDynamicMapServiceLayer california = new ESRI.ArcGIS.ArcGISDynamicMapServiceLayer();
private string hidelayername;

public Page()
{
InitializeComponent();

scalebar.Map = Map1;
scalebarstoryboard.Begin();
TBzoomdurationvalue.Text = string.Format("当前值:{0}.{1}秒", Map1.ZoomDuration.Seconds, Map1.ZoomDuration.Milliseconds);
TBpandurationvalue.Text = string.Format("当前值:{0}.{1}秒", Map1.PanDuration.Seconds, Map1.PanDuration.Milliseconds);
california.Url = "http://sampleserver1.arcgisonline.com/ArcGIS/rest/services/Specialty/ESRI_StatesCitiesRivers_USA/MapServer";
california.Opacity = 0.5;
california.Initialized += new EventHandler<EventArgs>(dynamiclayer_initialized);

TBimagery.IsChecked = true;
makestoryboard("WorldImageLayer", 0, 1).Begin();
//切换全屏/窗口
Application.Current.Host.Content.FullScreenChanged += new EventHandler(fullscreen_changed);
}
}
}

        scalebarstoryboard是xaml里自定义的一个动画,效果见比例尺旁的单位。

6、地图相关操作。
        Map控件已经内置了一些键盘鼠标事件,但目前不能像Javascript API中那样禁用这些事件。这里还用到了Silverlight程序的一个全屏特性,其实是对Application.Current.Host.Content的一个属性做了设置。直接看代码吧:
<!--operation info-->
<Canvas Width="215" Height="110" Margin="0,0,0,30" VerticalAlignment="Bottom">
<Rectangle Style="{StaticResource rectBottom}" />
<Rectangle Style="{StaticResource rectMiddle}" Fill="#77FF0000" />
<Rectangle Style="{StaticResource rectTop}" />
<TextBlock Margin="20,15,15,0" TextWrapping="Wrap"
Text="地图操作提示: 双击放大 Shift+拖拽:放大到指定范围 Ctrl+Shift+拖拽:缩小到指定范围" />
<ToggleButton x:Name="TBfullscreen" Content="点击切换地图全屏" HorizontalAlignment="Center" Canvas.Left="100" Canvas.Top="15" Height="30" Click="TBfullscreen_Click" />
</Canvas>

        放到Gridright Grid中,
private void TBfullscreen_Click(object sender, RoutedEventArgs e)
{
System.Windows.Interop.Content content = Application.Current.Host.Content;
content.IsFullScreen=!content.IsFullScreen;
}

private void fullscreen_changed(object o,EventArgs e)
{
System.Windows.Interop.Content content=Application.Current.Host.Content;
TBfullscreen.IsChecked = content.IsFullScreen;
}

7、进度条。
        最后还剩下地图中的这个进度条。利用了Map控件内置的一个Progress事件。
<!--progressbar 放在LayoutRoot中-->
<Grid HorizontalAlignment="Center" x:Name="progressGrid" VerticalAlignment="Center" Width="200" Height="20" Margin="5,5,5,5">
<ProgressBar x:Name="MyProgressBar" Minimum="0" Maximum="100" />
<TextBlock x:Name="ProgressValueTextBlock" Text="100%" HorizontalAlignment="Center" VerticalAlignment="Center" />
</Grid>

        在esri:Map标签中加入一个事件:Progress=”Map1_Progress”,
private void Map1_Progress(object sender, ESRI.ArcGIS.ProgressEventArgs e)
{
if (e.Progress < 100)
{
progressGrid.Visibility = Visibility.Visible;
MyProgressBar.Value = e.Progress;
ProgressValueTextBlock.Text = String.Format("正在处理 {0}%", e.Progress);
}
else
{
progressGrid.Visibility = Visibility.Collapsed;
}
}

        好了到此就已经讲完了整个地图功能。尽管想尽可能详细说明每段代码,便于初学的朋友学习,但也不可能面面俱到。没有讲明白的地方大家可以自己思考,查帮助。学习的过程中,不思考,无进步。
        下一节我们来看看Silverlight API中提供的小玩具——Widget。