网站整体将分为太湖简介、自然风光、人文风韵、旅游导览这四个部分组成。
太湖简介部分将介绍太湖全域的经济发展、自然风貌,让游客对太湖及其周边地区形成一个整体的直观认识,了解自己的旅游需求。
下一个页面为自然风光页面,将重点介绍太湖全域的自然景点,将主要的自然风光景点通过地图可视化的方法进行罗列,让游客了解太湖全域的自然景点,给游客在进行行程规划时提供参考。同时也会重点介绍部分的重要旅游景点,提供详细的景区介绍给游客方便参考。
再下一个页面为人文荟萃页面,重点介绍太湖全域的人文景点,整体内容展现方式与自然风光页面类似,功能也同样类似。
最后一个页面为旅游导览页面,提供给游客旅行规划的全部信息。从准备出发时的天气气候数据,到达时的交通信息,游览时的路径规划、景点选择等功能。
-
1. 页面整体设计
-
页面上方设计了导航栏来进行四个页面的切换,最上方为网站的标题“太湖全域导览”。
页面整体设计时使用了深蓝色和白色两种背景色,每部分使用一种背景色,在不同部分之间使用不同的背景色实现不同部分之间的区分,同时页面中使用的部分字体使用的深蓝色也使用了同种的色号,实现网站颜色的统一。同时在每个可视化的地图中均添加了控制放大缩小的控件和比例尺,也可视化的地图的样式也更加统一。
在每个页面的最后设计了页脚进行点缀。
-
2. 简介界面开发
-
太湖简介页面是网站的主页面,所以在此处除了导航栏还使用了太湖的图片来直观展现一下太湖的景色,展现的图片是S58沪常高速太湖隧道正上方的景色,图中红色的灯塔状的建筑物为隧道的通风口。同时页面中也使用了部分的诗词来展现太湖的人文底蕴与历史厚重感。
下方的第一部分为一条欢迎导语部分。在这里插入了“太湖美”的音乐播放功能和图片走马灯播放的功能。走马灯更能的实现引入了bootstrap框架,版本为4.6,使用了其中的轮播组件的功能,并进行了适当的修改融入整体的网站风格。音乐播放的功能使用了HTML中的音频元素,并设置属性为加载音频控件、自动播放。
界面实现的代码如下:
-
<div class="welcome"> <script src="https://cdn.jsdelivr.net/npm/jquery@3.5.1/dist/jquery.slim.min.js" integrity="sha384-DfXdz2htPH0lsSSs5nCTpuj/zy4C+OGpamoFVy38MVBnE+IbbVYUew+OrCXaRkfj" crossorigin="anonymous"></script> <!-- <script src="js/bootstrap.bundle.min.js"></script>--> <script src="https://cdn.jsdelivr.net/npm/popper.js@1.16.1/dist/umd/popper.min.js" integrity="sha384-9/reFTGAW83EW2RDu2S0VKaIzap3H66lZH81PoYlFhbGU+6BZp6G7niu735Sk7lN" crossorigin="anonymous"></script> <script src="js/bootstrap.min.js"></script> <!-- container --> <div class="container"> <div class="welcome-info"> <h3>Hello !</h3> <h5>欢迎来到太湖全域旅游导览网站 !</h5> <audio src="music/群星-太湖美.mp3" id="aud" autoplay="autoplay" controls="controls" preload="auto" class="music_taihu"></audio> </div> <div id="carouselExampleIndicators" class="carousel slide" data-ride="carousel"> <ol class="carousel-indicators"> <li data-target="#carouselExampleIndicators" data-slide-to="0" class="active"></li> <li data-target="#carouselExampleIndicators" data-slide-to="1"></li> <li data-target="#carouselExampleIndicators" data-slide-to="2"></li> </ol> <div class="carousel-inner"> <div class="carousel-item active"> <img src="images/DSC06176.JPG" class="d-block w-100" alt="照片1"> </div> <div class="carousel-item"> <img src="images/IMG_8758.jpg" class="d-block w-100" alt="照片2"> </div> <div class="carousel-item"> <img src="images/IMG_5326.jpg" class="d-block w-100" alt="照片3"> </div> <button class="carousel-control-prev" type="button" data-target="#carouselExampleIndicators" data-slide="prev"> <span class="carousel-control-prev-icon" aria-hidden="true"></span> <span class="sr-only">Previous</span> </button> <button class="carousel-control-next" type="button" data-target="#carouselExampleIndicators" data-slide="next"> <span class="carousel-control-next-icon" aria-hidden="true"></span> <span class="sr-only">Next</span> </button> </div> </div> </div> <!-- //container --> </div>
-
接下来的部分将介绍太湖周边的苏州、无锡、常州、湖州四个地级市的基础情况,包括人口和经济发展状况。GDP与人口的数据均来自于2022年《中国城市统计年鉴》,通过行政区划的面数据来可视化人口的分级统计图,通过饼状图来可视化四市的三大产业的GDP占比,通过饼状图的大小来间接可视化GDP的多少。
同时人口和三产的GDP的具体数字均可通过移动鼠标到行政区划或饼状图上来进行显示,提供游客与地图交互的可能,增加游客浏览网页的趣味性。同时本部的背景色采用了深蓝色,字体颜色采用了白色,既能够让背景与字体形成对比,清晰显示文字,又能够通过背景颜色让本部分和其他的部分实现区分,下一部分的背景色将采用白色来本部分进行区分,同时网页中利用的深蓝色均为这种色号能够实现统一。
-
同时页面中也提供给了游客切换背景地图的选项,将默认的遥感影像切换为正常的矢量地图。
本页面的下一部分将介绍太湖全域的自然情况。主要通过地图来可视化太湖全域的高程,通过91卫图助手下载的高程数据结合turf生成等值面后再结合四个地级市的边界数据进行裁剪后得到可视化的结果,最后填充颜色并添加图例。
同时为了游客能够更清晰地观赏到可视化的DEM数据,添加了sideBySide控件实现在两个地图中实现拖动,控制两个地图在显示区域内按不同比例出现,让右侧显示要素更少的地图显示在地图上能够更清晰展现DEM数据。同样不同区域的地形数据也能够通过移动鼠标显示的数据框来显示。
社会情况及地形情况部分的主要实现代码如下,下方代码主要实现了人口显示控件的添加和饼状图的添加:
-
1. //增加标题及各城市人口信息显示控件 2. var info = L.control(); 3. info.onAdd = function (inMap) { 4. this._div = L.DomUtil.create('div', 'info'); //创建一个类名为"info"的div元素 5. this.update(); 6. return this._div; 7. }; 8. // 更新内容 9. info.update = function (props) { 10. this._div.innerHTML = '<h4>太湖周边城市人口分布</h4>' + (props ? 11. '<b>' + props.市 + '</b><br />' + cityData[props.市] + ' 万人' 12. : '鼠标移动到各个城市即可查看'); 13. }; 14. info.addTo(inMap); 15. 16. //。。。 17. //gdp饼状图 18. var arr=[]; //用于存储绑定的数据 19. geojsonLayer_taihuaround.on('data:loaded',function(data){ 20. geojsonLayer_taihuaround.eachLayer(function (layer){ 21. arr.push({ 22. name: layer.feature.properties.市, 23. latlng: layer.getBounds().getCenter() //获取各省中心,作为注记和图表的锚点 24. }) 25. }); 26. d3.json("data/太湖周边GDP.json").then(function(data){ //读取json文件里面的数据 27. for (var i =0; i < arr.length; i++) { 28. for(var key in data){ 29. if (arr[i].name == key) { 30. arr[i].first = data[key]["第一产业GDP"]; 31. arr[i].second = data[key]["第二产业GDP"]; 32. arr[i].third = data[key]["第三产业GDP"]; 33. } 34. } 35. } 36. var pie = d3.pie(); 37. //返回一个由10种不同颜色组成的数组 38. var color = d3.scaleOrdinal(d3.schemeCategory10); 39. 40. var pieOverlay = L.d3SvgOverlay(function(sel,proj) { 41. //绘制饼状/环状图 42. var arrBinding=[]; //绑定饼状/环状图的数组 43. var selselArcs= sel.selectAll("g.arc"); 44. for(var j=0;j<arr.length;j++){ 45. arrBinding.push(arr[j].first); 46. arrBinding.push(arr[j].second); 47. arrBinding.push(arr[j].third); 48. var sum=arr[j].first+arr[j].second+arr[j].third; 49. var outerRadius = Math.sqrt(sum/30); 50. var innerRadius = Math.sqrt(sum/100); 51. var arc = d3.arc() 52. .innerRadius(innerRadius) 53. .outerRadius(outerRadius); 54. //绘制扇形 55. var arcs = selArcs.data(pie(arrBinding)) 56. .enter() 57. .append("g") 58. .attr("class", "arc") 59. .attr("transform", function (d) { //饼状图圆心坐标 60. return "translate(" + proj.latLngToLayerPoint(arr[j].latlng).x + "," + proj.latLngToLayerPoint(arr[j].latlng).y + ")" 61. }); 62. //通过路径绘制弧 63. arcs.append("path") 64. .attr("fill", function(d, i) { 65. return color(i); //设置填充颜色 66. }) 67. .attr("d", arc) 68. //pointer-events与leaflet.css里面样式设置冲突,导致鼠标提示失效,故增加以下语句 69. .attr("style",'pointer-events: visiblepainted'); 70. arcs.append("title") //鼠标提示 71. .text(function(d,i) { 72. return "第"+(i+1)+"产业GDP:"+d.value+data["单位"]; 73. }); 74. arrBinding=[]; 75. } 76. 77. //添加注记 78. var proName = sel.selectAll("text").data(arr, function (d) { 79. return d.first; 80. }); 81. proName.enter() 82. .append("text") //绘制文本 83. .text(function (d) { //设置文本内容 84. return d.name; 85. }) 86. .attr('x', function (d) { 87. return proj.latLngToLayerPoint(d.latlng).x-18; 88. }) 89. .attr('y', function (d) { 90. return proj.latLngToLayerPoint(d.latlng).y + 5; 91. }) 92. .attr('stroke', 'black'); 93. }); 94. pieOverlay.addTo(inMap); 95. }); 96. });
3. 自然界面开发
-
自然风光界面首先展示太湖全域主要的自然风光景点,通过合适的图标的来定点。同时在可视化实践的过程中,发现部分的可视化点数据出现过于密集的情况,所以引入了PruneCluster库来对点数据进行聚类显示,在显示小比例尺的地图时将密集的部分仅显示点数据的个数,地图放大到可以显示时就显示风光景点的图标,代码实现的流程图如下图所示:
自然风光介绍的可视化成果如下图所示,仅西部景点密度较小的部分在1:5000000的比例尺时显示了自然风光景点的图标,其他区域均显示了聚类的效果,显示了该区域的景点数量,聚类实现的代码实现如下:
var geojsonLayer = new L.GeoJSON.AJAX("data/nature.geojson"); //加载位于data文件夹内的geojson数据 var testData = []; geojsonLayer.on('data:loaded', function (e) { const features = e.target.toGeoJSON().features; for (const feature of features) { const name = feature.properties.名称; const coordinates = feature.geometry.coordinates; var lat = coordinates[0]; var lng = coordinates[1]; testData.push([lng,lat,name]); } var leafletView = new PruneClusterForLeaflet(); for (var i = 0, l = testData.length; i < l; ++i) { leafletView.RegisterMarker(new PruneCluster.Marker(testData[i][0], testData[i][1], {title: testData[i][2]})); } leafletView.PrepareLeafletMarker = function (marker, data) { if (marker.getPopup()) { marker.setPopupContent(data.title); } else { marker.bindPopup(data.title); } }; natureMap_1.addLayer(leafletView); });
自然风光界面如下图所示:
放大地图后即可显示具体地图的图标,效果如下图所示:
当游客点击聚类的数字时地图能够放大到该区域显示自然风光经典的图标,同时地图也提供了弹出框让用户能够看到可视化的景点的名称。左上角的图标让用户能够直接返回至初始化显示时的视图,即能够全览太湖的地图视角上。
自然风光界面接下来介绍了太湖鼋头渚景区,对景区内重要的景点进行了显示并推荐了一条游览路线。
景点的显示方式如上,但是在弹出框的显示不同,此处使用了bindTooltip来显示景点名称,游客仅需将鼠标移动到图标上就能够显示景点名称不需要点击才能弹出。
推荐路线的可视化使用了AntPath方法来可视化,实现类似于蚂蚁移动的移动效果,设置颜色为红色让推荐路线更醒目。
-
4. 人文界面开发
-
人文荟萃的开发过程与前文类似,同样使用了PruneCluster库来对点数据进行聚类显示,添加了弹出框和回到初始化视图的按钮。
人文荟萃界面的下一部分介绍了太湖周边最重要的历史古城——苏州,通过在地图上叠加历史地图让游客感受到穿越历史而来的厚重感,感受到苏州古城的历史的悠久。通过引入Leaflet.ImageOverlay.Rotated插件并指定地图左上、左下、右上三个投影点完成老地图的投影,再修改老地图的不透明度为0.4让地图能够显现,同时地图地图也选择了尽量朴素的地图,让老地图的文字也能够清晰可见。
老地图投影的实现代码如下方所示:
<!--地图_2--> <script> const customMap_2 = L.map('custom_map_2', { center: [31.31, 120.62], zoom: 15, zoomControl: false, }); L.tileLayer('http://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}.png', { maxZoom: 19, attribution: '© <a href="http://www.openstreetmap.org/copyright">OpenStreetMap</a>' }).addTo(customMap_2); L.control.zoom({ zoomInTitle: '放大', zoomOutTitle: '缩小' }).addTo(customMap_2); L.control.scale({ maxWidth : 200, //最大宽度 metric : true, //是否显示米制长度单位 imperial : false, //是否显示英制长度单位 updateWhenIdle : true, //是否在移动地图结束后更新 }).addTo(customMap_2); // 地图投影 // 顶点坐标 var topleft = L.latLng(31.3327302852, 120.5528504655), bottomleft = L.latLng(31.2792367080, 120.5594824607), topright = L.latLng(31.3407466441, 120.6412190876); var overlay = L.imageOverlay.rotated("images/1938年《最新苏州地图》.jpg", topleft, topright, bottomleft, { opacity: 0.4, interactive: true, }); overlay.addTo(customMap_2); </script>
5. 旅游导览页面开发
旅游导览主要分为出发、到达、游览、规划这四个部分。
出发部分主要聚焦于太湖全域的气候和天气的介绍,在游客出发时或是出发前提供天气和气候信息,让游客能够对游览时的天气能够有所准备,及时应对可能出现的极端天气和气候现象,以免耽误游客的行程。
通过地图可视化了太湖全域的天气状况,调用了windy的API展现了降雨雷暴、风、气温、云四种气象的情况,每个一段时间进行切换,下方显示了10月21日周六的雨雷暴的情况:
下方地图显示了10月21日周六的云量的情况:
下方地图显示了10月21日周六的温度的情况,图例显示在了地图的最下方:
下方地图显示了10月21日周六的风的情况,图例显示在了地图的最下方:
到达部分主要聚焦于达到太湖周边的交通方法,主要介绍了飞机、铁路、高速这三种方法,地图主要通过migrationLayer和Canvas-Flowmap-Layer插件来进行流向图的可视化,数据来源于无锡硕放国际机场和常州奔牛国际机场官网的航班信息通过整理得到的真实数据。可视化效果如下图所示, 游客点击图中的大圆就能够看到流向太湖周边一个机场的流向图,直观展示航班航向信息。
用户点击另一个机场,此处为常州奔牛国际机场即可看到奔牛机场的流向图,进行流向图终点的切换。
游览部分通过可视化景点密度的热力图来给游客更直观的太湖全域景点密度的印象,能够方便游客合理分配不同区域的游览时间。热力图的可视化使用了leaflet-heat插件,数据来源于2022年爬取的周边四市的高德地图的POI数据,通过筛选大类为“旅游景点”的POI数据共计11706条,通过heatLayer方法添加到地图上形成热力图。
游览部分的第二章地图可视化了推荐旅游路线,通过读取设定好的重要景点数据排序后,通过高德请求最佳行驶路径后再提取每一段最佳路径的途径点数据合并成同一个数组后再通过motion库中的seq方法进行路径的绘制。
推荐路线实现后的成果如下图所示,蓝色的为推荐路线,红色小车在页面载入成功后开始缓慢运动,双击小车用户能够让小车的运动暂停,小车能够完整运动到西北方向的终点,带领游客完成环绕太湖一圈的旅行,为游客的旅行规划提供便利。
同时也通过弹出框来为用户提供了交互,能够提供景点的具体名称。
推荐景点及路线部分的实现代码如下方所示:
var trackRoute = []; // 初始化为一个空数组存储所有需要途径的点的坐标 const travel_point = new L.GeoJSON.AJAX('data/travel_point.geojson',{ onEachFeature: function (feature, layer) { layer.bindTooltip(feature.properties.name, { sticky: true }); } }); travel_point.addTo(travelMap_3); travel_point.bindTooltip(function (layer) { //设置提示框 var feat=layer.feature; //获取选中的要素 return feat.properties.name; //获取要素的名称作为提示框显示内容 },{ sticky:true, //提示框随鼠标移动而移动,默认为false,提示框显示在地物要素中心 }).addTo(travelMap_3); travel_point.on('data:loaded', function (e) { const features = e.target.toGeoJSON().features; for (const feature of features) { const coordinates = feature.geometry.coordinates; trackRoute.push({ lat: coordinates[1], lng: coordinates[0] }); } var latlngArr=[]; //用于存储起止点经纬度坐标 var layerGroup=L.layerGroup().addTo(travelMap_3);//用于存储起止点图标 var key="2c664e69cf8b5f8dff7bff7a34392a82"; //高德key var path; //轨迹线 var number = 0; var urls = []; for (let i = 0; i < trackRoute.length - 1; i++) { // var marker = L.marker(trackRoute[i]); // layerGroup.addLayer(marker); //起止点图标 var lat_1 = L.Util.formatNum(trackRoute[i].lat, 6); var lng_1 = L.Util.formatNum(trackRoute[i].lng, 6); latlngArr.push([lat_1, lng_1]); //存储起止点坐标,传递给高德地图路径规划服务 // var marker = L.marker(trackRoute[i + 1]); // layerGroup.addLayer(marker); var lat_2 = L.Util.formatNum(trackRoute[i + 1].lat, 6); var lng_2 = L.Util.formatNum(trackRoute[i + 1].lng, 6); latlngArr.push([lat_2, lng_2]); //存储起止点坐标,传递给高德地图路径规划服务 // myMap.fitBounds([latlngArr[0],latlngArr[1]]); var para = "destination=" + latlngArr[1][1] + "," + latlngArr[1][0] + "&" + "origin=" + latlngArr[0][1] + "," + latlngArr[0][0] + "&output=JSON"; var url = "https://restapi.amap.com/v3/direction/driving?"; urlurl = url + para + "&key=" + key; //构建高德地图步行路径规划服务URL urls.push(url); latlngArr=[]; } requestDataSequentially(urls); async function requestDataSequentially(urls) { const results = []; // 用于存储数据的数组 for (let i = 0; i < urls.length; i++) { const data = await fetchUrl(urls[i]); // 使用await等待每个请求完成 results.push(data); } const mergedData = [].concat(...results); // // 最终进行绘制 // path = L.polyline(mergedData, { // color: 'red', // weight:8, // snakingSpeed: 200, //蛇行速度,像素/秒 // }).addTo(map).snakeIn(); var seqGroup = L.motion.seq([ L.motion.polyline(mergedData, { color: "blue" }, { easing: L.Motion.Ease.easeInOutQuad }, { removeOnEnd: true, icon: L.divIcon({html: "<i class='fa fa-truck fa-2x fa-flip-horizontal' aria-hidden='true'></i>", iconSize: L.point(27.5, 24)}) }).motionDuration(80000), ]).addTo(travelMap_3); seqGroup.on("click", function(){ seqGroup.motionStart(); }); seqGroup.on("dblclick", function(e){ seqGroup.motionToggle(); }); setTimeout(function () { seqGroup.motionStart(); }, 1000); } function fetchUrl(url) { return new Promise(function(resolve, reject) { var polyline=""; $.get(url,function(data){ //访问高德地图步行路径规划服务 number += 1; console.log("Request successful!" + number); var latlngs=[]; //存储轨迹中所有途径点 var paths=data.route.paths; //获得高德地图步行路径 for(var i=0;i<paths.length;i++){//步行路径可能由好几段路径组成 var steps=paths[i].steps; //获取步行结果列表 for(var j=0;j<steps.length;j++){//获取每段步行方案的路径线节点坐标 if(polyline!=""){ polylinepolyline=polyline+";"+steps[j].polyline; }else{ polylinepolyline=polyline+steps[j].polyline; } } } if(polyline!=""){ var nodeArr=polyline.split(";"); //将节点字符串数组转换成数值数组 for(var k=0;k<nodeArr.length;k++){ var ele=nodeArr[k].split(",").map(Number).reverse(); // latlngs.push(ele); latlngs.push({ lng: ele[1], lat: ele[0] }); } resolve(latlngs); latlngs = []; } }); }); } });
规划部分提供给用户绘制的功能,游客能够在地图上随意绘制,绘制完成后还能够将图片下载下来,并且也提供了全屏和鹰眼图的功能方便游客的绘制。
全屏功能使用Control.FullScreen插件,通过将fullscreenControl选项设置为true并设置enterFullscreen和exitFullscreen的功能实现进入全屏和推出全屏。
鹰眼图功能通过引入Control.MiniMap插件,并添加miniMap变量设置属性实现添加。
绘制功能通过引入leaflet.draw-src插件,通过添加drawnItems、drawControl到地图实现绘制图标的添加。
导出图片功能通过引入bundle插件,通过easyPrint方法设置属性后添加至地图,实现导出地图为图片的功能。
同时在本部分也使用了高德地图API进行导航规划,当用户使图标Marker来标记坐标点时,通过调用高德路径规划API请求路径数据在图中显示最优路径,并且此处也提供了步行和驾驶两种不同的出行路径规划方式供用户选择,用户可以点击地图右侧的图标进行选择。用户在使用时需要先点击右侧图标选择出行的方式,才能在地图上显示最优的路径,否则会出现报错。代码实现流程图如下所示:
如下图所示,红色显示为步行路径,蓝色显示为驾驶路径,蓝色路径主要通过了高速,红色路径主要经过普通道路。