网络地图服务(WMS)
网络地图服务(WMS)利用具有地理空间位置信息的数据制作地图。其中将地图定义为地理数据可视的表现。能够根据用户的请求返回相应的地图(包括PNG,GIF,JPEG等栅格形式或者是SVG和WEB CGM等矢量形式)。WMS支持网络协议HTTP,所支持的操作是由URL定义的。
先来分析一波服务链接:
一般情况给我们的是这样的:http://... .../geoserver/xf/wms?service=WMS&version=1.1.0&request=GetMap&layers=zgwz_lzyz_f08&styles=&bbox=-1844.9489742784644,0.0,35699.99999999942,34437.74999999997&width=768&height=704&srs=EPSG:3857&format=application/openlayers
参数:
service=WMS
version=1.1.0 : 版本(1.1...和1.3...在计算上有区别)
request=GetMap
layers=zgwz_lzyz_f08 :图层数组(这个参数如果错误则不显示)
styles=
bbox=-1844.9489742784644,0.0,35699.99999999942,34437.74999999997 : 盒子(显示区域)一般是需要计算
width=768
height=704
srs=EPSG:3857 :坐标类型 :3857同900913为伪墨卡托投影,也被称为球体墨卡托坐标;4326为WGS84经纬度坐标
format=application/openlayers :格式,iOS用这个format=image/png
TRANSPARENT=TRUE :透明 true:可以看到高德底图,false看不到高德底图
==>http://... .../geoserver/xf/wms?SERVICE=WMS&VERSION=1.1.0&REQUEST=GetMap&LAYERS=zgwz_lzyz_f08_gd&TRANSPARENT=TRUE&STYLES=&WIDTH=762&HEIGHT=768&srs=EPSG:3857&FORMAT=image/png&BBOX=
一、高德地图:
通过高德地图 MATileOverlay 接口,添加 WMS 服务到地图上
1.自定义类WMSTileOverlay继承自MATileOverlay
import UIKit
class WMSTileOverlay: MATileOverlay {
var rootURL = ""
var titleSize = 0
var initialResolution = 0.0
var originShift = 0.0
var HALF_PI = 0.0
var RAD_PER_DEGREE = 0.0
var HALF_RAD_PER_DEGREE = 0.0
var METER_PER_DEGREE = 0.0
var DEGREE_PER_METER = 0.0
/// 初始化
/// - Parameter initRootURL: 在线地图路径&TRANSPARENT=TRUE&FORMAT=image/png&BBOX=
init?(rootURL initRootURL: String?) {
super.init()
rootURL = initRootURL ?? ""
titleSize = 256
initialResolution = 156543.03392804062////2*Math.PI*6378137/titleSize
originShift = 20037508.342789244//周长的一半 2*Math.PI*6378137/2.0
HALF_PI = .pi / 2.0
RAD_PER_DEGREE = .pi/180.0
HALF_RAD_PER_DEGREE = .pi/360.0
METER_PER_DEGREE = originShift/180.0//一度多少米
DEGREE_PER_METER = 180.0/originShift//一米多少度
}
/**
* @brief 以tile path生成URL。用于加载tile,此方法默认填充URLTemplate
* @param path tile path
* @return 以tile path生成tileOverlay
*/
override func url(forTilePath path: MATileOverlayPath) -> URL {
let strURL = "\(rootURL)\(titleBoundsBy(x: path.x, y: path.y, zoom: path.z) ?? "")"
let url = URL(string: strURL)
return url!
}
/**
* @brief 加载被请求的tile,并以tile数据或加载tile失败error访问回调block;默认实现为首先用URLForTilePath去获取URL,然后用异步NSURLConnection加载tile
* @param path tile path
* @param result 用来传入tile数据或加载tile失败的error访问的回调block
*/
override func loadTile(at path: MATileOverlayPath, result: @escaping(Data?, Error?) -> Void) {
let url = self.url(forTilePath: path)
let request = NSMutableURLRequest(url: url)
request.httpMethod = "GET"
let session = URLSession.shared
session.dataTask(with: request as URLRequest, completionHandler: {(data, response, error) in
if error != nil {
#if DEBUG
print("Error downloading tile")
#endif
result(nil, error)
}
else {
result(data, nil)
}
}).resume()
}
/// 取消请求瓦片,当地图显示区域发生变化时,会取消显示区域外的瓦片的下载, 当disableOffScreenTileLoading=YES时会被调用。since 5.3.0
/// - Parameter path: path
override func cancelLoadOfTile(at path: MATileOverlayPath) {
super.cancelLoadOfTile(at: path)
}
/**
* 根据瓦片的x/y等级返回瓦片范围
*
* @param tx
* @param ty
* @param zoom
* @return url
*/
func titleBoundsBy(x: Int, y: Int, zoom: Int) -> String? {
let minX = pixels2Meters(x * titleSize, zoom: zoom)
let maxY = -pixels2Meters(y * titleSize, zoom: zoom)
let maxX = pixels2Meters(((x + 1) * titleSize), zoom: zoom)
let minY = -pixels2Meters(((y + 1) * titleSize), zoom: zoom)
return "\(minX),\(minY),\(maxX),\(maxY)"
}
/**
* 根据像素、等级算出坐标
*
* @param p p
* @param zoom z
* @return double
*/
func pixels2Meters(_ p: Int, zoom: Int) -> Double {
return Double(p) * resolution(zoom) - originShift
}
/**
* 计算分辨率
*
* @param zoom z
* @return double
*/
func resolution(_ zoom: Int) -> Double {
return initialResolution / (pow(2.0, Double(zoom)))
}
}
2.在地图控制器MapController调用
import UIKit
class MapController: UIViewController {
let tileOverlay = WMSTileOverlay.init(rootURL: "http://... .../geoserver/haitu/wms?SERVICE=WMS&VERSION=1.1.0&REQUEST=GetMap&LAYERS=haitu:gis_t_landuse_a&TRANSPARENT=TRUE&STYLES=&srs=EPSG:3857&FORMAT=image/png&BBOX=")
override func viewDidLoad() {
super.viewDidLoad()
let map = MAMapView(frame: frame: view.bounds)
mapView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
mapView.setCenter(CLLocationCoordinate2D(latitude: 39.97000000, longitude: 116.3300000), zoomLevel: 20, animated: false)
tileOverlay?.disableOffScreenTileLoading = true//停止不在显示区域的瓦片下载
mapView.add(tileOverlay)
view.addSubview(mapView)
}
}
3.异常处理
(1)WMS服务地图没有坐标:会导致图层不能显示在正确的位置,这时候图层会显示在地图的(0,0)坐标位置(非洲),排查的方法就是将地图中心设置在(0,0)坐标,然后放大。解决办法就是制图工程师给你个带坐标的图。
(2)图层叠加了很多次:第一出现这种情况有可能是我们自己对图层做的缓存引起的。可以删除缓存试试。
(3)出现栅格:这种情况有可能发布的图有问题,或者bbox参数计算出现了问题。出现这种情况的原因很多,欢迎补充。
(4)坐标系不同、WMS版本1.1和1.3:如果WMS服务给的是84坐标系,叠加到高德上会出现偏移,可以让制图工程师出一个高德坐标系的图,也可以我们自己在代码里做,会有一点计算量,代码如下:
在第1步自定义类WMSTileOverlay中的方法func titleBoundsBy(x: Int, y: Int, zoom: Int) -> String?更改
/**
* 根据瓦片的x/y等级返回瓦片范围
*
* @param tx
* @param ty
* @param zoom
* @return url
*/
func titleBoundsBy(x: Int, y: Int, zoom: Int) -> String? {
let minX = pixels2Meters(x * titleSize, zoom: zoom)
let maxY = -pixels2Meters(y * titleSize, zoom: zoom)
let maxX = pixels2Meters(((x + 1) * titleSize), zoom: zoom)
let minY = -pixels2Meters(((y + 1) * titleSize), zoom: zoom)
return "\(minX),\(minY),\(maxX),\(maxY)"
}
改为
/**
* 根据瓦片的x/y等级返回瓦片范围
*
* @param tx
* @param ty
* @param zoom
* @return url
*/
func titleBoundsBy(x: Int, y: Int, zoom: Int) -> String? {
var minX = pixels2Meters(x * titleSize, zoom: zoom)
var maxY = -pixels2Meters(y * titleSize, zoom: zoom)
var maxX = pixels2Meters(((x + 1) * titleSize), zoom: zoom)
var minY = -pixels2Meters(((y + 1) * titleSize), zoom: zoom)
//转换成经纬度
minX = meters2Lon(minX)
minY = meters2Lat(minY)
maxX = meters2Lon(maxX)
maxY = meters2Lat(maxY)
//转换目标经纬度为高德坐标系。
let amapcoord = AMapCoordinateConvert(CLLocationCoordinate2DMake(CLLocationDegrees(minY), CLLocationDegrees(minX)), type)
minY = amapcoord.latitude
minX = amapcoord.longitude
let maxAmapcoord = AMapCoordinateConvert(CLLocationCoordinate2DMake(CLLocationDegrees(maxY), CLLocationDegrees(maxX)), type)
maxY = maxAmapcoord.latitude
maxX = maxAmapcoord.longitude
//转换成墨卡托
minX = lon2Meters(minX)
minY = lat2Meters(minY)
maxX = lon2Meters(maxX)
maxY = lat2Meters(maxY)
//有博客提到1.1版本和1.3版本有区别,没有尝试过,如果你遇到了欢迎补充
result = "\(minX),\(minY),\(maxX),\(maxY)" //1.1
//result = "\(minX),\(minY),\(maxX),\(maxY)"//1.3
return result
}
//////添加坐标转换相应的方法
/**
* X米转经纬度
*/
func meters2Lon(_ mx: Double) -> Double {
let lon = mx * DEGREE_PER_METER
return lon
}
/**
* Y米转经纬度
*/
func meters2Lat(_ my: Double) -> Double {
var lat = my * DEGREE_PER_METER
lat = 180.0 / .pi * (2 * atan(exp(lat * RAD_PER_DEGREE)) - HALF_PI)
return lat
}
/**
* X经纬度转米
*/
func lon2Meters(_ lon: Double) -> Double {
let mx = lon * METER_PER_DEGREE
return mx
}
/**
* Y经纬度转米
*/
func lat2Meters(_ lat: Double) -> Double {
var my = log(tan((90 + lat) * HALF_RAD_PER_DEGREE))/(RAD_PER_DEGREE)
my = my * METER_PER_DEGREE
return my
}
二、Mapbox地图:
建议:做室内地图的用Mapbox,因为Mapbox缩放级别可以达到1米的效果。
1.Mapbox的WMS服务和添加栅格图像是一样的,甚至不需要我们做修改,唯一需要注意的是参数:bbox={bbox-epsg-3857}
import UIKit
import Mapbox
class ViewController: UIViewController, MGLMapViewDelegate {
var mapView: MGLMapView!
var rasterLayer: MGLRasterStyleLayer?
override func viewDidLoad() {
super.viewDidLoad()
mapView = MGLMapView(frame: view.bounds)
mapView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
mapView.setCenter(CLLocationCoordinate2D(latitude: 39.97000000, longitude: 116.3300000), zoomLevel: 20, animated: false)
mapView.delegate = self
view.addSubview(mapView)
// Add a UISlider that will control the raster layer’s opacity.
addSlider()
}
func mapView(_ mapView: MGLMapView, didFinishLoading style: MGLStyle) {
// Add a new raster source and layer.
let source = MGLRasterTileSource(identifier: "stamen-watercolor", tileURLTemplates: ["http://.../geoserver/xf/wms?service=WMS&version=1.1.0&request=GetMap&layers=zgwz_lzyz_f08_3857&styles=&bbox={bbox-epsg-3857}&width=768&height=704&srs=EPSG:3857&format=image/png"], options: [ .tileSize: 256 ])
let rasterLayer = MGLRasterStyleLayer(identifier: "stamen-watercolor", source: source)
style.addSource(source)
style.addLayer(rasterLayer)
self.rasterLayer = rasterLayer
}
@objc func updateLayerOpacity(_ sender: UISlider) {
rasterLayer?.rasterOpacity = NSExpression(forConstantValue: sender.value as NSNumber)
}
func addSlider() {
let padding: CGFloat = 10
let slider = UISlider(frame: CGRect(x: padding, y: self.view.frame.size.height - 44 - 30, width: self.view.frame.size.width - padding * 2, height: 44))
slider.minimumValue = 0
slider.maximumValue = 1
slider.value = 1
slider.isContinuous = false
slider.addTarget(self, action: #selector(updateLayerOpacity), for: .valueChanged)
view.insertSubview(slider, aboveSubview: mapView)
if #available(iOS 11.0, *) {
let safeArea = view.safeAreaLayoutGuide
slider.translatesAutoresizingMaskIntoConstraints = false
let constraints = [
slider.bottomAnchor.constraint(equalTo: safeArea.bottomAnchor, constant: -mapView.logoView.bounds.height - 10),
slider.widthAnchor.constraint(equalToConstant: self.view.frame.size.width - padding * 2),
slider.centerXAnchor.constraint(equalTo: safeArea.centerXAnchor)
]
NSLayoutConstraint.activate(constraints)
} else {
slider.autoresizingMask = [.flexibleTopMargin, .flexibleLeftMargin, .flexibleRightMargin]
}
}
}