转载请注明出处
以前在Esri的博客上看到了一篇用Silverlight+Balder实现TerrainMap的文章,实现的功能是将指定的二维投影地理范围转换成三维地形图,这是链接地址http://maps.esri.com/sldemos/terrainmap/default.html,感觉很有意思,最近在看WebGL,所以就想用WebGL重新进行实现,其中用ArcGIS JS API获取图片和高程数据,用WebGL进行三维显示。由于对WebGL框架不是很熟悉,所以开始的时候就想用原生的WebGL进行开发,后来发现越写越多,干脆萌生了一个将这些代码封装成自己的WebGL图形库的想法,取名World.js,我现在设置的World.js的版本是World0.3.5,World0.3.5的源码链接地址http://blog.csdn.net/sunqunsunqun/article/details/7885639,做这个TerrainMap的一个主要目的就是熟习WebGL,正好顺便把常用的WebGL代码封装成框架,以便增加代码的复用率。
现在这个Demo还有诸多不足,需要以后改正。
下图是二维界面:
下面是用WebGL实现的TerrainMap:
下面是Demo的组织结构:
World0.3.5的源码链接地址http://blog.csdn.net/sunqunsunqun/article/details/7885639。
以下是CanvasEventHandle.js代码:
var bMouseDown = false; var handleMouseMove; var previousX=-1; var previousY=-1; var MODE = "ROTATE"; function onMouseDown(evt){ previousX=evt.layerX||evt.offsetX; previousY=evt.layerY||evt.offsetY; bMouseDown = true; handleMouseMove = dojo.connect(dojo.byId("canvasId"),"onmousemove","onMouseMove"); } function onMouseMove(evt){ var currentX=evt.layerX||evt.offsetX; var currentY=evt.layerY||evt.offsetY; if(MODE == "PAN"){ if(bMouseDown){ onPanMouseMove(currentX,currentY); } } else if(MODE == "ROTATE"){ if(bMouseDown){ onRotateMouseMove(currentX,currentY); } } previousX = currentX; previousY = currentY; } function onRotateMouseMove(currentX,currentY){ if(previousX > 0 && previousY > 0){ var changeX = currentX - previousX; var changeY = currentY - previousY; var horCameraAngle = World.canvas.width / World.canvas.height * camera.fov; var changeHorAngle = changeX / World.canvas.width * horCameraAngle; var changeVerAngle = changeY / World.canvas.height * camera.fov; camera.worldRotateY(-changeHorAngle*Math.PI/180); var lightDir = camera.getLightDirection(); //if(Math.abs(lightDir.z)>0.01){ var plumbVector = getPlumbVector(lightDir,false); camera.worldRotateByVector(-changeVerAngle*Math.PI/180,plumbVector); //} } } function onPanMouseMove(currentX,currentY){ var position = camera.getPosition(); var target = camera.getTarget(); //左右平移 if(currentX != previousX){ var bLeft = currentX < previousX ? true:false; var plumbVector = getPlumbVector(camera.getLightDirection(), bLeft); position.x += plumbVector.x; position.z += plumbVector.z; target.x += plumbVector.x; target.z += plumbVector.z; } //前后平移 if(currentY != previousY){ var bForward = currentY < previousY ? true:false; var forwardVector = getForwardVector(camera.getLightDirection(), bForward); position.x += forwardVector.x; position.y += forwardVector.y; position.z += forwardVector.z; target.x += forwardVector.x; target.y += forwardVector.y; target.z += forwardVector.z; } camera.look(position,target); } function onMouseWheel(evt){ var scale = 0.0; if (evt.wheelDelta ){ if(evt.wheelDelta > 0){ scale = 0.9; } else if(evt.wheelDelta < 0){ scale = 1.1; } } else if(evt.detail){ if(evt.detail < 0){ scale = 0.9; } else if(evt.detail > 0){ scale = 1.1; } } var distance = camera.getViewFrustumDistance(); camera.setViewFrustumDistance(distance*scale,true); } function onMouseUp(evt){ bMouseDown = false; dojo.disconnect(handleMouseMove); previousX = -1; previousY = -1; } function getPlumbVector(direction,bLeft){ direction.y = 0; direction.normalize(); var plumbVector = new World.Vector(-direction.z,0,direction.x); plumbVector.normalize(); return plumbVector; } function getForwardVector(direction,bForward){ direction.y = 0; direction.normalize(); if (bForward){ direction.x *= -1; direction.z *= -1; } return direction; }
下面是前端代码:
<!DOCTYPE HTML> <html> <head> <title>WebGL Terrain Map</title> <meta http-equiv="Content-Type" content="text/html"/> <meta name="charset" content="utf-8"/> <!--<link rel="stylesheet" type="text/css" href="http://serverapi.arcgisonline.com/jsapi/arcgis/3.1/js/dojo/dijit/themes/claro/claro.css">--> <link rel="stylesheet" type="text/css" href="http://localhost/arcgis_js_api/library/3.1/jsapi/js/dojo/dijit/themes/claro/claro.css"> <style type="text/css"> html, body { margin: 0; padding: 0; width: 100%; height: 100% ;font-family:"Times New Roman",Georgia,Serif;} div { margin: 0; padding: 0 } .head{width:100%; height:25px; background-color:#5998DD;color:#ffffff;font-size:13px; line-height:25px;border-top-left-radius:5px;border-top-right-radius:5px;-webkit-border-top-left-radius:5px; -webkit-border-top-right-radius:5px;-moz-border-radius-topleft:5px;-moz-border-radius-topright:5px;} .tip{font-size:8px;display:block;height:8px;margin-left:3px;margin-top:9px;} </style> <!--<script src='http://serverapi.arcgisonline.com/jsapi/arcgis/?v=3.1' data-dojo-config='parseOnLoad: true'></script>--> <script src='http://localhost/arcgis_js_api/library/3.1/jsapi/' data-dojo-config='parseOnLoad: true'></script> <script type="text/javascript" src="World0.3.5.js"></script> <script type="text/javascript" src="CanvasEventHandler.js"></script> <script type="text/javascript"> dojo.require("esri.map"); dojo.require("esri.tasks.geometry"); dojo.require("dojo.parser"); dojo.require("dijit.form.HorizontalSlider"); dojo.require("dijit.layout.BorderContainer"); dojo.require("dijit.layout.ContentPane"); var elevationData = [],imageName = ""; var vertexShaderContent,fragmentShaderContent,camera,scene,renderer,heightMap; var map,dynamicLayer,bMap2D = true,previousExtent = null;; var mapServiceUrl = "http://services.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer"; var streetMapUrl = "http://services.arcgisonline.com/ArcGIS/rest/services/World_Street_Map/MapServer"; var topoMapUrl = "http://services.arcgisonline.com/ArcGIS/rest/services/World_Topo_Map/MapServer"; var squareCenterExtent; var geometryServiceUrl = "http://tasks.arcgisonline.com/ArcGIS/rest/services/Geometry/GeometryServer"; var getElevationDataUrl = "http://sampleserver4.arcgisonline.com/ArcGIS/rest/services/Elevation/ESRI_Elevation_World/MapServer/exts/ElevationsSOE/ElevationLayers/1/GetElevationData"; function getShaderContent(shaderType){ var shaderUrl; if(shaderType == "VERTEX_SHADER"){ shaderUrl = "VertexShader.txt"; } else if(shaderType == "FRAGMENT_SHADER"){ shaderUrl = "FragmentShader.txt"; } var xhrArgs = { url:shaderUrl, handleAs:"text", sync:true, preventCache:true, load:function(data){ if(shaderType == "VERTEX_SHADER"){ vertexShaderContent = data; } else if(shaderType == "FRAGMENT_SHADER"){ fragmentShaderContent = data; } }, error:function(error){ alert("获取ShaderContent出错!"); } }; dojo.xhrGet(xhrArgs); } function getDefaultElevationData(dataName){ var xhrArgs = { url:"ElevationData/"+dataName, handleAs:"text", sync:true, preventCache:true, load:function(data){ elevationData = []; var strElevationArray = data.split(','); for(var i=0;i<strElevationArray.length;i++){ elevationData.push(parseFloat(strElevationArray[i])); } }, error:function(error){ alert("加载默认高程数据出错!") } }; dojo.xhrGet(xhrArgs); } function startWebGL(){ getShaderContent("VERTEX_SHADER"); getShaderContent("FRAGMENT_SHADER"); //getDefaultElevationData("50X50.txt"); renderer = new World.WebGLRenderer(dojo.byId("canvasId"),vertexShaderContent,fragmentShaderContent); //World.enableAmbientLight(); //World.enableParallelLlight(new World.Vector(0,-30,-50),new World.Vertice(0,0,0.8)); World.disableAmbientLight(); World.disableParallelLlight(); camera = new World.PerspectiveCamera(45,1,1.0,100.0); camera.look(new World.Vertice(0,30,50),new World.Vertice(0,-30,-50)); scene = new World.Scene(); //heightMap = new World.HeightMap(rowCount,columnCount,elevationData,"MapImages/"+"terrain512.jpg"); //scene.add(heightMap); renderer.bindScene(scene); renderer.bindCamera(camera); renderer.setIfAutoRefresh(false); } function judgeExtentEqual(ext1,ext2){ if(ext1 && ext2){ var sr1 = ext1.spatialReference; var sr2 = ext2.spatialReference; function judgeNumberEqual(a,b){ var c = Math.abs(a-b); if(c <= 100) return true; } if(judgeNumberEqual(ext1.xmin,ext2.xmin) && judgeNumberEqual(ext1.ymin,ext2.ymin) && judgeNumberEqual(ext1.xmax,ext2.xmax) && judgeNumberEqual(ext1.ymax,ext2.ymax) && sr1.wkid == sr2.wkid){ return true; } } return false; } function getSquareCenterExtent(){ var offset = 0; if (map.extent.getHeight() < map.extent.getWidth()) { offset = map.extent.getHeight() / 2; } else { offset = map.extent.getWidth() / 2; } var p = map.extent.getCenter(); var squareExtent = esri.geometry.Extent(p.x - offset, p.y - offset, p.x + offset, p.y + offset, map.extent.spatialReference) return squareExtent; } function showTerrain3D(bUpdate,row,column,elevations,mapImageName){ if(bUpdate == true){ scene.remove(heightMap); var scale = parseFloat(dojo.byId("labelStretch").innerHTML); heightMap = new World.HeightMap(row, column, elevations, "MapImages/"+mapImageName,scale); scene.add(heightMap); } renderer.setIfAutoRefresh(true); dojo.byId("mapId").style.display = "none"; dojo.byId("canvasId").style.display = "block"; dojo.byId("iSpring").style.visibility = "visible"; dojo.byId("btnSwitch").innerHTML = "转换为2D视图"; dojo.byId("btnSwitch").disabled = false; dijit.byId("sliderStretch")._setDisabledAttr(false); previousExtent = map.extent; bMap2D = false; } function startSwitch(bSwitchTo3D){ squareCenterExtent = getSquareCenterExtent(); if(bSwitchTo3D == true){ setIfDisableControls(true); var currentExtent = map.extent; //第一个判断的顺序要放在首位 if(judgeExtentEqual(currentExtent,previousExtent)){ showTerrain3D(false); } else{ var imageSize = dojo.byId("selectImageSize").value; getMapImageUrl(dynamicLayer,squareCenterExtent,imageSize,imageSize); } } else{ renderer.setIfAutoRefresh(false); setIfDisableControls(false); dojo.byId("canvasId").style.display = "none"; dojo.byId("iSpring").style.visibility = "hidden"; dojo.byId("mapId").style.display = "block"; dojo.byId("btnSwitch").innerHTML = "转换为3D视图"; dijit.byId("sliderStretch")._setDisabledAttr(true); bMap2D = true; } } function setIfDisableControls(bDisable){ dojo.byId("btnSwitch").disabled = bDisable; dojo.byId("btnFullExtent").disabled = bDisable; dojo.byId("radioSatelite").disabled = bDisable; dojo.byId("radioStreet").disabled = bDisable; dojo.byId("radioTopo").disabled = bDisable; dijit.byId("sliderGridSize")._setDisabledAttr(bDisable); dojo.byId("selectImageSize").disabled = bDisable; } function getMapImageUrl(dynamicLayer,extent,imageWidth,imageHeight){ var imageParameters = new esri.layers.ImageParameters(); imageParameters.bbox = extent; imageParameters.format = "jpeg"; imageParameters.width = imageWidth; imageParameters.height = imageHeight; imageParameters.imageSpatialReference = map.spatialReference; dynamicLayer.exportMapImage(imageParameters,function(mapImage){ tryStoreMapImage(mapImage.href); }); } function tryStoreMapImage(imageUrl){ var xhrArgs = { url:"proxy.ashx?requestType=getImage&imageUrl="+imageUrl, handleAs:"text", sync:false, preventCache:true, load:function(data){ imageName = data; //alert("存储图像完成!"); var gridSize = Math.round(dijit.byId("sliderGridSize").value); getCurrentElevationData(gridSize,gridSize); }, error:function(error){ alert("存储图像出错!"); startSwitch(false); } }; dojo.xhrGet(xhrArgs); } function getCurrentElevationData(rows,columns){ var extent = squareCenterExtent; var xhrArgs = { url:"proxy.ashx?requestType=getElevation", content:{ Rows:rows, Columns:columns, xmin:extent.xmin, ymin:extent.ymin, xmax:extent.xmax, ymax:extent.ymax, wkid:extent.spatialReference.wkid }, handleAs:"text", sync:false, preventCache:true, load:function(data){ //alert("获取高程数据完成!"); succeedGetElevationData(data); }, error:function(error){ alert("获取高程出错!"); startSwitch(false); } }; dojo.xhrGet(xhrArgs); } function succeedGetElevationData(data){ var info = data.split(";"); var row = parseFloat(info[0]);//一定要将string转换成float var column = parseFloat(info[1]);//一定要将string转换成float var strElevations = info[2]; elevationData = []; var strElevationArray = strElevations.split(','); for(var i=0;i<strElevationArray.length;i++){ elevationData.push(parseFloat(strElevationArray[i])); } showTerrain3D(true,row,column,elevationData,imageName); } function initLayout(){ var sliderGridSize = new dijit.form.HorizontalSlider({ name: "sliderGridSize", value: 50, minimum: 1, maximum: 100, intermediateChanges: true, style: "width:175px;float:left;", onChange:function(value){ dojo.byId("labelGridSize").innerHTML = Math.round(value); } }, "sliderGridSize"); var sliderStretch = new dijit.form.HorizontalSlider({ name:"sliderStretch", value:1, minimum:0, maximum:4, intermediateChanges:true, style:"width:175px;float:left;", onChange:function(value){ var scale = Math.round(value*10)/10;//var scale = Math.round(value); dojo.byId("labelStretch").innerHTML = scale; heightMap.heightScale = scale; } },"sliderStretch"); sliderStretch._setDisabledAttr(true);//开始的时候要禁用拉伸滑块 var clientWidth = document.body.clientWidth < 1024 ? 1024:document.body.clientWidth; var clientHeight = document.body.clientHeight < 600 ? 600:document.body.clientHeight; var height = clientHeight - 10 - 10;// var viewerWidth = clientWidth - 220 - 10;// document.getElementById("controls").style.height = height+"px"; document.getElementById("controlsContent").style.height = (height - 25) + "px"; document.getElementById("viewer").style.height = height+"px"; document.getElementById("mapId").style.height = (height-25)+"px"; document.getElementById("canvasId").height = height-25; document.getElementById("viewer").style.width = viewerWidth + "px"; document.getElementById("mapId").style.width = viewerWidth + "px"; document.getElementById("canvasId").width = viewerWidth; } function initMap(){ map = new esri.Map("mapId"); var basemap = new esri.layers.ArcGISTiledMapServiceLayer(mapServiceUrl); map.addLayer(basemap); dynamicLayer = new esri.layers.ArcGISDynamicMapServiceLayer(mapServiceUrl,{imageFormat:"jpg"}); dojo.connect(dynamicLayer,'onError',function(){ alert("载入动态图层出错!"); }); dojo.connect(map, 'onLoad',function(theMap){ dojo.connect(dojo.byId('mapId'),'onresize',map, map.resize); dojo.connect(theMap, "onMouseDown",function(evt){ console.log(evt.mapPoint); }); }); } function initEvents(){ dojo.connect(dojo.byId("canvasId"),"mousedown",onMouseDown); dojo.connect(dojo.byId("canvasId"),"onmouseup",onMouseUp); dojo.connect(dojo.byId("canvasId"),'mousewheel',onMouseWheel); dojo.connect(dojo.byId("canvasId"),'DOMMouseScroll',onMouseWheel); } function initAll(){ initLayout(); initEvents(); initMap(); startWebGL(); } function showTest3D(){ getDefaultElevationData("Test.txt"); showTerrain3D(true,50,50,elevationData,"Test.jpg"); } dojo.addOnLoad(initAll); </script> </head> <body class="claro" style="background-color:#D8DCE0;" onselectstart="return false;"> <div id="main"> <div id="controls" style="width:200px;height:650px;position:absolute;left:10px;top:10px;"> <div class="head" style="text-align:left;"> Controls</div> <div id="controlsContent" style="height:624px;position:relative;background-color:#ffffff;"> <div style="width:100%;height:40px;padding-top:5px;background-color:LightBlue;"> <button id="btnSwitch" style="display:block;height:30px;margin-left:auto;margin-right:auto;" onclick="javascript:bMap2D==true?startSwitch(true):startSwitch(false);">转换为3D视图</button> </div> <span class="tip">导航</span> <image src="Images/horizontal.png" /> <button id="btnFullExtent" style="display:block;height:30px;" onclick="map.setExtent(getFullExtent());">全图</button> <span class="tip">地图</span> <image src="Images/horizontal.png" /> <input type="radio" id="radioSatelite" name="radioMap"/>Satelite<br/> <input type="radio" id="radioStreet" name="radioMap"/>Street<br/> <input type="radio" id="radioTopo" name="radioMap"/>Topo<br/> <span class="tip">高程格网大小</span> <image src="Images/horizontal.png" /> <div id="sliderGridSize"></div><label id="labelGridSize" style="float:left;">50</label></br> <span class="tip">图像大小</span> <image src="Images/horizontal.png" /> <select id="selectImageSize" style="display:block;width:190px;margin:0 auto;"> <option value="64">64</option> <option value="128">128</option> <option value="256">256</option> <option value="512" >512</option> <option value="1024" selected>1024</option> <option value="2048">2048</option> </select> <a href="http://cn.khronos.org/" target="_blank" style="display:block;position:absolute;bottom:0px;"> <div style="width:116px;height:77px;background-image:url(Images/WebGL.png);"></div> </a> <span class="tip">拉伸系数</span> <image src="Images/horizontal.png" /> <div id="sliderStretch"></div><label id="labelStretch" style="float:left;">1</label> </div> </div> <div id="viewer" style="width:1040px;height:650px;position:absolute;left:220px;top:10px;"> <div class="head"> Viewer <a href="http://weibo.com/iispring" target="_blank" style="float:right;text-decoration:none;color:#ffffff;margin-right:7px;text-align:center;line-height:25px;">About iSpring</a> </div> <div id="mapId" style="width:1040px;height:625px;background-color:#666666;"></div> <canvas id="canvasId" width="1040" height="625" style="display:none;"></canvas> <a id="iSpring" style="visibility:hidden;diaplay:block;position:absolute;bottom:0px;right:0px;" href="http://weibo.com/iispring" target="_blank"> <div style="width:81px;height:50px;background-image:url(Images/iSpring.png);"></div> </a> </div> </div> </body> </html>
下面是所使用的代理proxy.ashx
<%@ WebHandler Language="C#" Class="proxy" %> using System; using System.Web; using System.IO; using System.Drawing; using System.Text; public class proxy : IHttpHandler { public void ProcessRequest(HttpContext context) { string requestType = context.Request["requestType"]; if (requestType == "getImage") { string url = context.Request["imageUrl"]; System.Net.WebRequest request = System.Net.WebRequest.Create(new Uri(url)); request.Method = context.Request.HttpMethod; request.ContentType = "application/x-www-form-urlencoded"; System.Net.WebResponse response = request.GetResponse(); Stream stream = response.GetResponseStream(); Image img = Image.FromStream(stream); int index = url.LastIndexOf('/'); string imageName = url.Remove(0, index + 1); string baseDirectory = System.AppDomain.CurrentDomain.BaseDirectory; string physicPath = baseDirectory + "\\MapImages\\" + imageName; img.Save(physicPath); context.Response.Write(imageName); context.Response.End(); } else if (requestType == "getElevation") { string Rows = context.Request["Rows"]; string Columns = context.Request["Columns"]; string xmin = context.Request["xmin"].Trim(); ; string ymin = context.Request["ymin"].Trim(); ; string xmax = context.Request["xmax"].Trim(); string ymax = context.Request["ymax"].Trim(); string wkid = context.Request["wkid"]; string baseUrl = "http://sampleserver4.arcgisonline.com/ArcGIS/rest/services/Elevation/ESRI_Elevation_World/MapServer/exts/ElevationsSOE/ElevationLayers/1/GetElevationData"; string paras = "?Extent=%7B%22xmin%22%3A" + xmin + "%2C%0D%0A%22ymin%22%3A" + ymin + "%2C%0D%0A%22xmax%22%3A" + xmax + "%2C%0D%0A%22ymax%22%3A" + ymax + "%2C%0D%0A%22spatialReference%22%3A%7B%22wkid%22%3A" + wkid + "%7D%7D&Rows=" + Rows + "&Columns=" + Columns + "&f=pjson"; string url = baseUrl + paras; System.Net.WebRequest request = System.Net.WebRequest.Create(new Uri(url)); request.Method = context.Request.HttpMethod; request.ContentType = "application/x-www-form-urlencoded"; System.Net.WebResponse response = request.GetResponse(); Stream stream = response.GetResponseStream(); StreamReader sr = new StreamReader(stream); string strResponse = sr.ReadToEnd(); int index1 = strResponse.LastIndexOf('[') + 1; string str = strResponse.Remove(0, index1); int index2 = str.LastIndexOf(']'); string result = str.Remove(index2); result = result.Replace("\r\n", "").Replace(" ", "");//类似于这样32767,32767,-3175,384,1983,-208 //假设高程6500对应着地球投影面长宽的1/10 float width = int.Parse(Rows); string[] results = result.Split(','); StringBuilder LastOutput = new StringBuilder(); LastOutput.Append(Rows + ";" + Columns + ";"); for (int i = 0; i < results.Length; i++) { string strElevation = results[i]; int Elevation = int.Parse(strElevation); if (Elevation == 32767) { Elevation = 0; } float handledElevation = width / 10 * Elevation / 6500; string strHandledElevation = handledElevation.ToString(); if (i != results.Length - 1) { LastOutput.Append(strHandledElevation + ","); } else { LastOutput.Append(strHandledElevation); } } context.Response.ContentType = "text/plain"; context.Response.Write(LastOutput.ToString()); context.Response.End(); } } public bool IsReusable { get { return false; } } }