项目用到了GIS地图,在浏览器进行展示。起初使用了在线的高德地图。高德官网api丰富,且都是中文,很好用,也很方便。但是随着需求的变更,项目环境也从互联网变成了内网环境。所以高德地图就不能再用了,于是,要切换成离线地图。
由于才疏学浅,一直对地图引擎,图层,BIGMAP,瓦片,矢量等等一系列GIS概念模棱两可。如何换成离线GIS地图,一时间竟迷茫的无从下手。
本文记录一下从迷茫,到了解,再到搭建起一套离线地图,并发布所需图层的探索过程,以免以后忘记。由于用GIS只是简单的展示,所以,各个技术点用的都不是很深入,都是点到为止的技术研究。
看了一些GIS的专业知识,里面从地图数据的采集,制作到分析,再到电脑上的展示和保存都有涉及。作为一个java程序员,并不需要了解这些专业的GIS概念和理论。这里只记录几个对接下来的探索有用的概念:
矢量数据:
专业的写法这里不再体现。个人理解矢量数据就是点,线,面形成的文件。最常见的是.shp文件,这种文件就是根据真是地理数据形成的文件数据。作为java程序员而已,无需知道如何制作.shp文件。只需要拿到.shp文件,能发布成图层展示就可以了。下面就是一个.shp文件形成的图层预览,是两条线组成的:
栅栏数据:
栅栏数据个人理解就是图片格式的数据。最常见的是tif格式的文件。
瓦片/切片:
这个概念,还是有必要体现一下官方的描述的:
瓦片地图金字塔模型是一种多分辨率层次模型,在统一的空间参照下,根据用户需要以不同分辨率进行存储与显示,形成分辨率由粗到细、数据量由小到大的金字塔结构。所表示的地理范围不变,金字塔越往底层所表示的地图信息越详细,比例尺越大。
首先确定地图服务平台提供的缩放级别的数量N,把缩放级别最高、地图比例尺最大的地图图片作为金字塔的底层,即第0层,并对其进行分块,从地图图片的左上角开始,从左至右、从上至下进行切割,分割成相同大小(比如256x256像素)的正方形地图瓦片,形成第0层瓦片矩阵,在第0层地图图片的基础上,按第2x2像素合成一个像素的方法生成第1层地图图片,并对其进行分块,分割成与下一层相同大小的正方形地图瓦片,形成第1层瓦片矩阵,采用同样的方法生成第2层瓦片矩阵…直到第N-1层。
个人理解就是,把栅栏数据的tif图片分成无数个小的图片,每个分辨率都会生成自己的小图片。这些小图片就是切片。这样我们在缩放地图的时候,就可以加载不同的切片,展现不同的精度了。
平时耳闻最多的就是BIGMAP,也不清楚它是干啥的,就知道与地图有关系。经过一番研究,才知道,BIGMAP主要是用于下载地图栅栏数据的。当然,BIGMAP提供了很多功能,但是大部分人用BIGMAP是用于下载地图数据的。但是BIGMAP是收费的,试用版下载的地图有水印,肯定不能用到项目中,所以,只能找其他的地图下载工具了。
最终,选定了太乐地图下载工具,下面,就记录一下使用太乐下载工具的过程。首先,为啥会用到地图下载工具呢?因为地图厂商只提供了项目所需的矢量数据,即.shp文件。.shp文件加载出来的图层就是一些线条和点,总不能让用户只看一些线条吧?所以,需要以电子地图作为底图图层,给用户展示。因此,需要用地图下载器,来下载电子地图。下载下来的数据是栅栏格式的,即图片类型的数据。
太乐地图下载器提供了几个地图厂家的数据源,因为项目中用到的是暗黑色调的地图背景,所以找到了ArGIS提供的地图有暗色调的,因此,切换成ArGIS的街道(午夜蓝)风格。(这里需要考虑地图的坐标系,坐标系这个问题还没研究透彻,选择的ArGIS地图的数据正好与之前高德的地图位置能对应上,也算歪打正着吧,百度地图是单独的坐标系,和高德的位置就对应不上)。
选择要下载的级别,进行下载:
下载完成后,太乐地图下载器可以将数据转成tif格式,将其转成tif格式文件:
至此,电子地图数据下载完毕。需要注意的是,选择高级别的地图数据后,太乐地图下载器会转成多个tif文件。因为数据太大了,所以太乐地图下载器会转成多个小的tif文件。每个tif文件都是地图的一部分。这几个tif文件拼凑起来是下载的完整的地图。
其他的地图下载器是否会根据文件大小切分tif文件不得而知。
地图引擎,从应用层层来看,就是一套提供了驱动和管理地理数据,实现渲染、查询等功能的一套函数库,所有的应用层软件只需要调用地图引擎提供的功能接口就能较容易的完成其功能。常见的地图引擎有Google Maps API、百度地图API、高德地图API、HereMap等。
离线的地图引擎有超图、ArGIS、geoserver等。之所以选择geoserver,是因为geoserver是开源的地图引擎,其相当于是一个开源版的ArGIS地图引擎(破解版的ArcGis可以留评论获取哦)。geoserver是用java实现的一个地图引擎服务,运行在tomcat里。
关于地图引擎中提供的几种地图服务,博主研究了一下也没太研究明白,对于java程序员而言,最常用的应该就是wms服务,能给访问并展示栅栏图层和矢量图层,并做一些简单操作就足够了,所以这里不再深入研究各种服务类型的区别,几种服务类型的区别可参考文章:常见地图服务(WMS、WFS、WCS、TMS、WMTS)
接下来,就是把上述用地图下载器下载的tif文件和地图厂商提供的.shp文件制作成一个图层,供项目去访问了。
这里先强调一下地图引擎的作用。就是通过矢量或栅栏数据,发布图层,然后供客户端去访问这个图层,进行操作或展示。
发布图层也遇到了很多坑,尝试了很多次,才发布成功。这里不再记录错误的做法,直接记录最终正确的做法。
下载安装geoserver操作省略。本次使用2.21.0版本。登录geoserver,如下图:
第一步:
首先,添加工作空间。作为java程序员,工作空间的概念很好理解,按java的思路来理解即可。
填写名称和URI。其中,名称用作唯一标识,命名要有意义。第二个URI可以随便填写,也是用于标识作用,只要每次工作空间填写的不一样即可,具体填什么在本次项目中并无实际意义。
这里,因为我要两个图层,一个是厂家提供的.shp文件,一个是自己下载的tif文件,所以,创建两个工作空间。
第二步:
然后,点击左侧菜单的存储仓库(转换成中文后的叫法),添加新的存储仓库。
如上图所示,1是添加多个.shp文件,2是添加单个.shp文件,3是添加tif文件。
首先制作矢量图层。因为地图厂家提供了多个.shp文件,所以选择上图的1号进行文件加载。
选择新建的工作空间,为数据源命名,然后在连接参数里,选择地图厂家提供的.shp文件的文件夹。
.shp文件要放入geoserver安装目录的data\data中,如下图所示:
点击保存,会跳转到发布图层的界面。因为是加载了多个.shp文件。一个.shp文件就是一个矢量图层,所以会有多个矢量图层需要发布。
第三步:
完成第二步操作后,跳到下面界面:
首先对第一个图层进行发布,点击发布按钮,进行图层发布:
发布矢量图层时,需要有两点要注意:
一个是数据tab卡中,坐标系和边界的计算:
坐标系这里,没有研究太明白,就用图层发布时默认选择的。然后边框计算上面的点从数据中计算,下面的点Compute from native bounds生成边界即可。
第二个需要注意的是在发布的tab卡中,WMS设置,都设置成line选项。否则矢量图层发布会有问题,在图层预览界面,点击预览时,可能会直接让你下载文件。这里选成line,避免出现这种错误:
点击保存,图层发布完成。
因为有多个.shp文件,上述才发布了一个图层,其他的图层如何发布呢?第三步的第一张图,那个发布列表,博主找了很长时间,才找到。在左侧的图层菜单,点击添加新的资源按钮,跳到如下界面:
选择刚才新建的工作空间:发布的仓库名称,选择完成后,就又进入了第三步的第一张图:
然后重复上面步骤,将图层全部发布完成即可。
第四步:
图层预览,对刚才的图层进行预览。点击左侧图层预览按钮,然后选择刚发布的图层,进行预览:
点击OpenLayers按钮,如果出现下载界面,就是第三步操作没有选择line类型。如果图层发布成功,可以呈现预览界面。
第五步:
通过第四步的预览发现,厂家提供的多个.shp文件,共同组成一个整体。我们项目里需要的是一个整体的效果。厂家提供了十几个.shp文件,我们总不能项目里引入十几个图层吧,这样太麻烦了。geoserver提供了图层组合的功能。下面对这十几个图层进行组合,形成一个组合图层。
点击菜单左侧图层组菜单,进入界面后,点击添加新图层组,在添加界面的中间部分,可以添加图层:
将刚才的图层一一添加,组成图层组。需要注意的是,在上图的图层列表中,下面的图层会在图层组的上方。列表上面的图层,会在图层组的下方。也就是说一个图层如果想在最上部程序,不被其他图层覆盖,那就将这个图层移动到上图图层列表的最下方的位置。
图层组合完毕后,点击上面的生成边界按钮,生成组合图层的边界,然后点击保存即可。
组合图层同样可以在图层预览中预览。通过预览,查看图层是否有被覆盖的情况,如果有,那么就编辑图层组,调整上图中图层列表位置,解决图层覆盖的问题。
第六步:
发布图层组后,进行图层组的预览,发现预览的效果和项目想要的效果不一样。预览的效果默认是蓝线条,项目想要的效果是白线条,且需要对线条之间的面进行颜色的填充。下面记录一下如何修改矢量图层的样式。这也是为什么矢量图层和栅栏图层分开发布的原因。因为矢量图层可以设置样式,而栅栏数据是图片,无法设置样式,所以就分开两个工作空间发布了。
点击菜单左侧的样式按钮,进入样式界面,然后点击添加样式,进行样式的添加。
这里需要注意的是,geoserver的样式默认是SLD格式。但是作为一名java程序员而言,学习SLD语法无疑是一个巨大的成本。geoserver还提供了css样式来设置,所以,我选择用css样式来调试图层样式。
需要往geoserver服务里添加css样式的插件,然后重启geoserver服务,才会生效,具体添加插件的步骤可参考官网,这里不再赘述,注意下载插件时要与自己的geoserver版本保持一致。安装好插件,重启geoserver服务后,在添加新的样式的界面,就会出现css选项:
填写样式名称,选择工作空间,在Generate a default style选项中,因为我要改变线条的颜色,所以选择line,然后点击Generate按钮,生成css代码,如下图:
点击保存。这样,线条样式就制作好了。
因为还要对面进行颜色的填充,所以,还需要定义面的css样式。再新建一个样式,Generate a default style选项选择polygon,点击Generate按钮,生成面的样式代码:
第七步:
对.shp文件发布的图层,进行样式的修改。注意修改的是单个.shp文件的图层,单个图层样式修改后,图层组的样式自动就修改了。
点击左侧图层菜单,找到刚刚发布的图层,进行编辑,在发布的tab卡中,之前选择的line,那么现在就要选新建的样式:
这里需要注意的是,在第六步中,我们定义了两个样式。一个是线条的样式,一个是面的样式。在地图厂家提供的这十几个.shp文件中,有线条的图层,也有面的图层。具体怎么看哪个是线条的图层,哪个是面的图层,博主不会看,所以只能一个图层一个图层的去试。先设置成面的样式,然后去预览,如果面的填充效果不对,那说明这是一个线条图层,就将样式改成线条的样式。如果面的样式预览的正确,那么这个图层就是面的图层。一个图层一个图层的试完后,最终得到了想要的图层效果。
注意:
geoserver的样式提供了选择器功能。比如可以选择某部分填充颜色为红色,另一部分填充颜色为绿色。本项目中填充颜色都是一样的,所以不需要选择器。如果遇到填充不同颜色的需求,可以使用选择器功能。具体功能官网有讲解,这里不再深究。
第一步:
新建工作空间,同上。
第二步:
这里选择3,加载tif文件。先将地图下载器下载的tif文件复制到geoserver安装目录的data/data目录下,然后选择数据源为这个tif文件,点击保存。
第三步:
发布图层:
与矢量数据发布图层一样,坐标系默认,点击按钮,生成图层边界。与矢量数据不同的是,栅栏数据发布图层时,不需要选择发布tab卡,而是选择缓存tab卡:
在此tab的底部:
首先需要选择坐标系,博主对坐标系研究不深,大部分地图都是选择EPSG:4326坐标系,所以这里就选了这个坐标系,然后 选择缩放等级。最小缩放是10,最大是21,选择这两个等级,然后点击保存。
第四步:
进行图层预览,可以看到,就是我们刚才下载的电子地图,而且可以缩放。下面着重解释一下第三步中缩放级别的选择是什么意思。
当我们预览了图层,并放大,缩小电子地图图层后,看geoserver安装目录下data目录下的gwc文件夹:
这些都是栅栏图层的数据,我们点开一个看里面的文件:
可以看到,在地图下载器下载地图数据时,下载的是16级别的数据,所以文件夹命令是L16。但是在L16文件夹里,却有多个级别的数据。其实,图层在新建的时候,L16文件夹里只有16级别的数据。当我们预览图层的时候,进行地图的放大和缩小操作,geoserver会自动生成其他等级的数据。所以就有了多个等级的数据。我们点开某个等级进去查看,如下图:
都是png格式的图片,这就是geoserver为我们自动切割成的瓦片。这样的好处就是我们再次缩放地图时,可以很快的加载不同级别的瓦片数据。当然,第一次缩放时会相对慢点,因为第一次要生成这些瓦片数据。
在第三步中,我们选择的缩放级别是10到21,geoserver就会生成10到21级别的瓦片数据。当缩放级别小于10时,地图就隐藏成空白了,因为我们设置的最低级别是10。
需要注意的是,geoserver生成了多个级别的数据,但是都是基于地图下载器下载的tif文件的精度去生成瓦片的。也就是说,geoserver生成的瓦片,最高精度是地图下载器的精度。比如下载的16级别的数据,那么geoserver生成的18级别的数据,也不会比16更高精。
至此,一个tif文件的栅栏图层发布完成。
第五步:
上面提到,博主下载的是高级别的地图数据,在用太乐下载器转tif文件时,自动切分成了多个tif文件。所以通过上述步骤发布的栅栏图层,只是地图的一部分。我们需要重复上面的步骤,将多个tif文件都进行发布。
每个tif文件都是一个图层,又需要对多个图层进行组合,形成组合图层。
与矢量图层组一样,添加图层列表,最后生成边界。因为这几个图层拼接共同形成一张完整地图,所以这里无需考虑图层顺序。因为是栅栏图层,所以在缓存tab卡里,也要配置切分瓦片等级:
这样,通过组合图层,就拼接成了一个完整地图
上述的栅栏图层的制作,是单个精度的数据,做成了多个等级。这样做的弊端就是,在地图缩放时,地图的精度都是固定的元素。比如我们下载的16等级的数据,我们在放大到16级别时,看到的地图是这样的:
可以看到,显示了村落的名称。但是当我们把级别调到10时,地图是这样的:
还是显示那几个村落,但是字很小了,用户看不清楚。
这就是因为地图下载的精度就是16级别。缩小10后,展示的内容还是16精度的内容,所以字就会变小。
最优的效果应该是,地图放大后,显示村落的名字,地图缩小后,应该显示县的名字,城市的名字,省的名字。也就是地图展示的内容随着放大缩小动态变化。这种效果怎么实现呢?
第一步:
要实现这种效果,在地图下载器下载数据时,就要选择多个级别的数据下载:
下载完成后,可以转换成多个级别的tif数据。把这些tif文件复制到geoserver安装目录的data/data文件夹下。
下面开始加载多层级的这些tif文件。新建存储仓库,发布栅栏图层,步骤和上面的一样,这里不再赘述。需要注意的是,发布多层级精度的图层时,缩放范围就不需要写10到21了,10层级的数据,min/max都写10即可。11层级的数据,min/max都写11即可,如下图:
多精度的tif图层都逐个发布。
第二步:
图层发布成功后,点击左侧菜单的切换图层菜单,找到刚才新发布的多精度图层:
点击Seed/Truncate按钮,对栅栏图层进行瓦片切分:
上图中注释的几个选项,选择完成后,开始切分瓦片:
切分成功后,在geoserver安装目录的data/gwc文件夹下,可以找到刚刚切分的瓦片数据:
这里多说一句,上图中11等级的数据是我们手动切分好的。如果我们此时预览这个L11的图层,并对地图进行放大缩小,那么在这个文件夹里,会自动生成其他级别的瓦片数据,但是都是基于L11这个级别,去生成的其他级别的数据,精度都是L11的,这里我们制作的是多精度的切换,所以我们只要L11的数据。
第三步:
将第二步生成的L11的数据,复制到L15的数据里面。因为我们最开始制作的是L15精度的数据,并依照L15精度数据,生成了L10~L21多级别的数据。这样生成的问题就是精度不变,缩放到10的时候,村落名字太小,看不见了。所以现在就要把对应放大层级的数据,放到L15里,这样,再切换的时候,就是对应层级的精度地图了,就可以动态展示县,市和省了。
上图是L15级别发布的数据(注意是组合图层的数据,因为L15生成了多个tif文件)。可以看到里面已经有了L11级别的数据,但是它是基于L15的地图生成的,所以把它删掉,然后把刚刚生成的L11的数据,复制到这里来。
依次把每个图层的瓦片都切割好,然后复制到L15组合图层的文件夹下面,这样,就实现了多精度数据的动态加载效果。因为这里最高精度就是15,所以15以上的精度图层,还是延用15精度图层生成的就可以。
下面可以看多精度图层的效果:
使用openlayers的api进行geoserver图层的调用。OpenLayers 是一个专为Web GIS 客户端开发提供的JavaScript 类库包,用于实现标准格式发布的地图数据访问。其不光能访问geoserver的图层,只要符合格式的矢量图层和栅栏图层,都可以用openlayers进行调用。
openlayers具体的api这里不过多记录,因为发布了两个图层,一个栅栏图层,一个矢量图层,所以在openlayers初始化地图时,也需要加载两个图层,代码如下:
function initMap(){
map = new ol.Map({
target: 'map',
layers: [
/*new ol.layer.Tile({
source: new ol.source.OSM()
})*/
],
view: new ol.View({
projection: 'EPSG:4326',
center: [115.698695,40.740556],
zoom: 11.2
})
});
initRoadLayer();
initBaseLayer();
}
/**
* 矢量图层样式
*/
function layerStyle(){
style = new ol.style.Style({
fill: new ol.style.Fill({ //矢量图层填充颜色,以及透明度
color: 'rgba(255, 255, 255, 0.6)'
}),
stroke: new ol.style.Stroke({ //边界样式
color: '#319FD3'
}),
text: new ol.style.Text({ //文本样式
font: '12px Calibri,sans-serif',
fill: new ol.style.Fill({
color: '#000'
}),
stroke: new ol.style.Stroke({
color: '#fff'
})
})
});
}
/**
* 添加底图图层
*/
function initBaseLayer(){
var wmsLayer = new ol.layer.Image({
visible: true,
source: new ol.source.ImageWMS({
ratio: 1,
url:'http://192.168.2.43:8099/geoserver/ycztdz/wms',
params: {
'FORMAT': 'image/png',
'VERSION': '1.1.0',
'LAYERS': 'ycztdz:ycztdz',
'STYLES': '',
}
})
});
map.addLayer(wmsLayer);
}
/**
* 添加高速图层
*/
function initRoadLayer(){
layerStyle();
var tiled = new ol.layer.Tile({
source: new ol.source.TileWMS({
url: 'http://192.168.2.43:8099/geoserver/gwc/service/wms?',
params: {
'FORMAT': "image/png",
'VERSION': '1.1.0',
tiled: true,
STYLES: '',
/*LAYERS: 'final:L10'*/
LAYERS: 'yanchonggaosu:L15-compoiste'
//tilesOrigin: -124.73142200000001 + "," + 24.955967
}
})
});
map.addLayer(tiled);
}
跨域问题解决:
上述代码可以看到,需要往geoserver服务发送请求,这就涉及到了跨域。geoserver服务对跨域问题做了后台的处理,需要我们配置,开启geoserver的跨域处理,参考文章:跨域问题及常见解决方法-Geoserver解决跨域