網(wǎng)絡(luò)地圖服務(wù)(WMS)
??????? 網(wǎng)絡(luò)地圖服務(wù)(WMS)利用具有地理空間位置信息的數(shù)據(jù)制作地圖狼忱。其中將地圖定義為地理數(shù)據(jù)可視的表現(xiàn)危号。能夠根據(jù)用戶的請(qǐng)求返回相應(yīng)的地圖(包括PNG,GIF,JPEG等柵格形式或者是SVG和WEB CGM等矢量形式)队腐。WMS支持網(wǎng)絡(luò)協(xié)議HTTP,所支持的操作是由URL定義的薪前。
??????? 先來(lái)分析一波服務(wù)鏈接:
一般情況給我們的是這樣的: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
參數(shù):
service=WMS
version=1.1.0 : 版本(1.1...和1.3...在計(jì)算上有區(qū)別)
request=GetMap
layers=zgwz_lzyz_f08 :圖層數(shù)組(這個(gè)參數(shù)如果錯(cuò)誤則不顯示)
styles=
bbox=-1844.9489742784644,0.0,35699.99999999942,34437.74999999997 : 盒子(顯示區(qū)域)一般是需要計(jì)算
width=768
height=704
srs=EPSG:3857 :坐標(biāo)類型 :3857同900913為偽墨卡托投影润努,也被稱為球體墨卡托坐標(biāo);4326為WGS84經(jīng)緯度坐標(biāo)
format=application/openlayers :格式示括,iOS用這個(gè)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=
一、高德地圖:
???????通過(guò)高德地圖 MATileOverlay 接口垛膝,添加 WMS 服務(wù)到地圖上
???????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//周長(zhǎng)的一半 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,此方法默認(rèn)填充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 加載被請(qǐng)求的tile,并以tile數(shù)據(jù)或加載tile失敗error訪問(wèn)回調(diào)block;默認(rèn)實(shí)現(xiàn)為首先用URLForTilePath去獲取URL,然后用異步NSURLConnection加載tile
* @param path tile path
* @param result 用來(lái)傳入tile數(shù)據(jù)或加載tile失敗的error訪問(wèn)的回調(diào)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()
}
/// 取消請(qǐng)求瓦片,當(dāng)?shù)貓D顯示區(qū)域發(fā)生變化時(shí)吼拥,會(huì)取消顯示區(qū)域外的瓦片的下載, 當(dāng)disableOffScreenTileLoading=YES時(shí)會(huì)被調(diào)用倚聚。since 5.3.0
/// - Parameter path: path
override func cancelLoadOfTile(at path: MATileOverlayPath) {
super.cancelLoadOfTile(at: path)
}
/**
* 根據(jù)瓦片的x/y等級(jí)返回瓦片范圍
*
* @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)"
}
/**
* 根據(jù)像素、等級(jí)算出坐標(biāo)
*
* @param p p
* @param zoom z
* @return double
*/
func pixels2Meters(_ p: Int, zoom: Int) -> Double {
return Double(p) * resolution(zoom) - originShift
}
/**
* 計(jì)算分辨率
*
* @param zoom z
* @return double
*/
func resolution(_ zoom: Int) -> Double {
return initialResolution / (pow(2.0, Double(zoom)))
}
}
???????2.在地圖控制器MapController調(diào)用
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//停止不在顯示區(qū)域的瓦片下載
mapView.add(tileOverlay)
view.addSubview(mapView)
}
}
???????3.異常處理
?????????(1)WMS服務(wù)地圖沒(méi)有坐標(biāo):會(huì)導(dǎo)致圖層不能顯示在正確的位置凿可,這時(shí)候圖層會(huì)顯示在地圖的(0惑折,0)坐標(biāo)位置(非洲),排查的方法就是將地圖中心設(shè)置在(0枯跑,0)坐標(biāo)惨驶,然后放大。解決辦法就是制圖工程師給你個(gè)帶坐標(biāo)的圖敛助。
?????????(2)圖層疊加了很多次:第一出現(xiàn)這種情況有可能是我們自己對(duì)圖層做的緩存引起的粗卜。可以刪除緩存試試纳击。
?????????(3)出現(xiàn)柵格:這種情況有可能發(fā)布的圖有問(wèn)題续扔,或者bbox參數(shù)計(jì)算出現(xiàn)了問(wèn)題。出現(xiàn)這種情況的原因很多焕数,歡迎補(bǔ)充纱昧。
?????????(4)坐標(biāo)系不同、WMS版本1.1和1.3:如果WMS服務(wù)給的是84坐標(biāo)系百匆,疊加到高德上會(huì)出現(xiàn)偏移砌些,可以讓制圖工程師出一個(gè)高德坐標(biāo)系的圖,也可以我們自己在代碼里做加匈,會(huì)有一點(diǎn)計(jì)算量存璃,代碼如下:
???????????在第1步自定義類WMSTileOverlay中的方法func titleBoundsBy(x: Int, y: Int, zoom: Int) -> String?更改
/**
* 根據(jù)瓦片的x/y等級(jí)返回瓦片范圍
*
* @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)"
}
???????????改為
/**
* 根據(jù)瓦片的x/y等級(jí)返回瓦片范圍
*
* @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)
//轉(zhuǎn)換成經(jīng)緯度
minX = meters2Lon(minX)
minY = meters2Lat(minY)
maxX = meters2Lon(maxX)
maxY = meters2Lat(maxY)
//轉(zhuǎn)換目標(biāo)經(jīng)緯度為高德坐標(biāo)系。
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
//轉(zhuǎn)換成墨卡托
minX = lon2Meters(minX)
minY = lat2Meters(minY)
maxX = lon2Meters(maxX)
maxY = lat2Meters(maxY)
//有博客提到1.1版本和1.3版本有區(qū)別雕拼,沒(méi)有嘗試過(guò)纵东,如果你遇到了歡迎補(bǔ)充
result = "\(minX),\(minY),\(maxX),\(maxY)" //1.1
//result = "\(minX),\(minY),\(maxX),\(maxY)"http://1.3
return result
}
//////添加坐標(biāo)轉(zhuǎn)換相應(yīng)的方法
/**
* X米轉(zhuǎn)經(jīng)緯度
*/
func meters2Lon(_ mx: Double) -> Double {
let lon = mx * DEGREE_PER_METER
return lon
}
/**
* Y米轉(zhuǎn)經(jīng)緯度
*/
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經(jīng)緯度轉(zhuǎn)米
*/
func lon2Meters(_ lon: Double) -> Double {
let mx = lon * METER_PER_DEGREE
return mx
}
/**
* Y經(jīng)緯度轉(zhuǎn)米
*/
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地圖:
?????????????建議:做室內(nèi)地圖的用Mapbox啥寇,因?yàn)镸apbox縮放級(jí)別可以達(dá)到1米的效果偎球。
???????1.Mapbox的WMS服務(wù)和添加?xùn)鸥駡D像是一樣的洒扎,甚至不需要我們做修改,唯一需要注意的是參數(shù):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]
}
}
}