TileLayer
TileLayer栅格图层,是每个地图引擎必备的功能,maptalks内部内置了常见坐标投影的瓦片的支持
- EPSG:3857
- EPSG:4326
- EPSG:4490
当我们创建地图或者TileLayer时如果不指定spatialReference
时,内部将默认采用EPSG:3857
,所以当你的瓦片坐标投影不是默认的切图参数需要配置这个参数的
默认的墨卡托投影切图参数科普
TIP
这里只是做个简单的科普,这些参数主要用来和自定义切图对比,方便查找对应的比例尺对应关系. 切图参数详情
全球墨卡托
切图原点:
[-20037508.342787,20037508.342787]
层级 分辨率 比例尺 0 156543.03392800014 591657527.591555 1 78271.51696399994 295828763.795777 2 39135.75848200009 147914381.897889 3 19567.879241000017 73957190.948944 4 9783.939620500008 36978595.474472 5 4891.969810250004 18489297.737236 6 2445.984905125002 9244648.868618 7 1222.992452562501 4622324.434309 8 611.4962262812505 2311162.217155 9 305.74811314062526 1155581.108577 10 152.87405657031263 577790.554289 11 76.43702828515632 288895.277144 12 38.21851414257816 144447.638572 13 19.10925707128908 72223.819286 14 9.55462853564454 36111.909643 15 4.77731426782227 18055.954822 16 2.388657133911135 9027.977411 17 1.1943285669555674 4513.988705 18 0.5971642834777837 2256.994353
墨卡托投影一般常见于互联网,当我们做项目时,用的瓦片好多都是EPSG:4490
的,创建地图只需要指定投影为 EPSG:4490
即可
var map = new maptalks.Map("map", {
center: [120.84600742, 31.14241977],
zoom: 10,
zoomControl: true,
spatialReference: {
projection: "EPSG:4490",
},
});
TileLayer可以有自己独立的投影
注意maptalks体系内图层是有自己的坐标投影配置信息的
- 图层的坐标系可以和地图不同
- 当图层不设置投影坐标信息的时候会自动的去拿地图的投影信息作为自己的坐标投影配置
- 所以代码层面最好为图层设置自己的坐标信息,尤其时当图层的投影信息和地图不同时,否则会导致一些未知错误,尤其是瓦片图层会导致瓦片加载错乱
怎样加载EPSG:4326的瓦片
将TileLayer的spatialReference
设置为4326即可
var map = new maptalks.Map("map", {
center: [105.08052356963802, 36.04231948670001],
zoom: 4,
minZoom: 1,
maxZoom: 18,
baseLayer: new maptalks.TileLayer("base", {
spatialReference: {
projection: "EPSG:4326",
},
tileSystem: [1, -1, -180, 90],
urlTemplate:
"https://t{s}.tianditu.gov.cn/vec_c/wmts?SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&LAYER=vec&STYLE=default&TILEMATRIXSET=c&FORMAT=tiles&TILEMATRIX={z}&TILEROW={y}&TILECOL={x}&tk=6901643c38b65f1f9770196343cf72b2",
subdomains: ["1", "2", "3", "4", "5"],
attribution:
'© <a target="_blank" href="http://www.tianditu.cn">Tianditu</a>',
}),
});
WARNING
注意maptalks体系内图层是有自己的坐标投影配置信息的
- 地图的坐标系自己根据需要选取一个自己的,一般都是
EPSG:3857
或者EPSG:4326
- 图层的坐标系可以和地图不同,比如上面的代码里地图的是
EPSG:3857
,但是Tilelayer是EPSG:4326
- 当图层不设置投影坐标信息的时候会自动的去拿地图的投影信息作为自己的坐标投影配置
- 所以代码层面最好为图层设置自己的坐标信息,尤其时当图层的投影信息和地图不同时,否则会导致一些未知错误,尤其是瓦片图层会导致瓦片加载错乱
TileLayer怎样显示特定的区域
所有的图层都有个setMask
方法的,用来设置图层的蒙版效果
var map = new maptalks.Map("map", {
center: [121.55939847, 31.19162191],
zoom: 8,
baseLayer: new maptalks.TileLayer("base", {
// debug: true,
urlTemplate:
"https://webrd01.is.autonavi.com/appmaptile?lang=zh_cn&size=1&scale=1&style=8&x={x}&y={y}&z={z}",
subdomains: ["a", "b", "c", "d"],
}),
});
fetch("https://geo.datav.aliyun.com/areas_v3/bound/310000.json")
.then((res) => res.json())
.then((geojson) => {
const polygons = maptalks.GeoJSON.toGeometry(geojson);
map.getBaseLayer().setMask(polygons[0]);
});
TIP
mask支持MutliPolygon的,如果你的蒙层效果是有飞地的,比如上图中上海的效果图,请使用MultiPolygon
mask的实现是依赖于canvas的clip操作,所以当把TileLayer放到GroupGLLayer时会失效
社区了有个插件 maptalks.tileclip 可以用来对瓦片做各种处理:
- clip剪裁
- cssfilter
- 打水印
- 当请求瓦片出错会自动 promise.catch
- 甚至可以和其他的地图引擎结合
地图放大后TileLayer加载不到数据一片白怎么办?
TileLayer有个参数叫maxAvailableZoom
,用来设置图层的可见最大层级,当设置了改参数后,地图的层级超过这个值时会复用你设置的这个参数层级的数据,比如你设置了18
,当地图放大道19时仍然会复用18
层级的数据
var map = new maptalks.Map("map", {
center: [116.45266161, 39.86656647],
zoom: 18,
zoomControl: true,
baseLayer: new maptalks.TileLayer("base", {
// debug: true,
urlTemplate:
"https://webrd01.is.autonavi.com/appmaptile?lang=zh_cn&size=1&scale=1&style=8&x={x}&y={y}&z={z}",
subdomains: ["a", "b", "c", "d"],
maxAvailableZoom: 18,
}),
});
TileLayer放到GroupGLLayer里filter会失效
filter效果依赖canvas的filter特性,当TileLayer放到GroupGLLaye里TileLayer变成了webgl渲染,所以这个效果会失效. 你可以自定义每个瓦片的处理逻辑来达到同样的效果
const baseLayer = new maptalks.TileLayer("base", {
// debug: true,
urlTemplate:
"https://webrd01.is.autonavi.com/appmaptile?lang=zh_cn&size=1&scale=1&style=8&x={x}&y={y}&z={z}",
subdomains: ["a", "b", "c", "d"],
});
baseLayer.on("renderercreate", function (e) {
//load tile image
// img(Image): an Image object
// url(String): the url of the tile
e.renderer.loadTileImage = function (img, url) {
//mocking getting image's base64
//replace it by your own, e.g. load from sqlite database
var remoteImage = new Image();
remoteImage.crossOrigin = "anonymous";
remoteImage.onload = function () {
var base64 = getBase64Image(remoteImage);
img.src = base64;
};
remoteImage.src = url;
};
});
var canvas;
function getCanvas() {
if (canvas) {
return canvas;
}
canvas = document.createElement("canvas");
return canvas;
}
function getBase64Image(img) {
var canvas = getCanvas();
canvas.width = img.width;
canvas.height = img.height;
var ctx = canvas.getContext("2d");
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.save();
ctx.filter = "sepia(100%) invert(90%)";
ctx.drawImage(img, 0, 0);
var dataURL = canvas.toDataURL("image/jpg", 0.7);
return dataURL;
}
var map = new maptalks.Map("map", {
center: [116.45266161, 39.86656647],
zoom: 5,
baseLayer,
});
这个方法就是对每个瓦片做了自定义处理,你可以发挥你的创造力对瓦片做任何操作,当然因为处理逻辑在主线程里,可能会导致主线程卡顿,你也可以在worker里处理
//worker code
const fun1 = function (exports) {
exports.initialize = function () {
console.log("tileimagebitmap init");
};
const canvas = new OffscreenCanvas(1, 1);
const TILESIZE = 256;
exports.onmessage = function (msg, postResponse) {
const url = msg.data.url;
canvas.width = TILESIZE;
canvas.height = TILESIZE;
const ctx = canvas.getContext("2d");
ctx.clearRect(0, 0, canvas.width, canvas.height);
//fetch image
fetch(url)
.then((res) => res.arrayBuffer())
.then((arrayBuffer) => {
const blob = new Blob([arrayBuffer]);
createImageBitmap(blob).then((bitmap) => {
ctx.filter = "sepia(100%) invert(90%)";
ctx.drawImage(bitmap, 0, 0);
const image = canvas.transferToImageBitmap();
postResponse(null, { image }, [image]);
});
})
.catch((error) => {
const image = canvas.transferToImageBitmap();
postResponse(null, { image }, [image]);
});
};
};
const workerKey = "tileimagebitmap";
maptalks.registerWorkerAdapter(workerKey, fun1);
//启动worker
const actor = new maptalks.worker.Actor(workerKey);
var baseLayer = new maptalks.TileLayer("base", {
urlTemplate:
"https://webrd01.is.autonavi.com/appmaptile?lang=zh_cn&size=1&scale=1&style=8&x={x}&y={y}&z={z}",
subdomains: ["a", "b", "c", "d"],
attribution:
'© <a href="http://osm.org">OpenStreetMap</a> contributors, © <a href="https://carto.com/">CARTO</a>',
});
baseLayer.on("renderercreate", function (e) {
//load tile image
// img(Image): an Image object
// url(String): the url of the tile
e.renderer.loadTileBitmap = function (url, tile, callback) {
// console.log(tile);
actor.send({ url }, null, (error, message) => {
// console.log(message);
callback(message.image);
});
};
});
var map = new maptalks.Map("map", {
center: [116.45266161, 39.86656647],
zoom: 5,
baseLayer,
});
WARNING
这个处理方法依赖 OffscreenCanvas,请注意兼容性
社区了有个插件 maptalks.tileclip 可以用来对瓦片做各种处理:
- clip剪裁
- cssfilter
- 打水印
- 当请求瓦片出错会自动 promise.catch
- 甚至可以和其他的地图引擎结合
TileLayer怎样强制刷新?
由于业务的需要TileLayer的数据可能是动态,业务里期望隔一段时间就去更新TileLayer的内容,尤其是其子类WMSTileLayer我们会用其去加载WMS图层,WMS的数据是不断更新的,这时就需要去强制刷新WMSTileLayer
tilelayer.forceReload();
wmsLayer.forceReload();
WARNING
这个方法会清空图层里缓存的所有图片资源,从新加载的,所以要慎用
瓦片加载时自定义headers
TileLayer默认是用Image标签加载
const image = new Image();
image.src = url;
Image请求是没法子自定义header的,所以需要开启fetch请求
new maptalks.TileLayer("tile2", {
urlTemplate:
"https://{s}.basemaps.cartocdn.com/light_nolabels/{z}/{x}/{y}.png",
subdomains: ["a", "b", "c", "d"],
decodeImageInWorker: true,
fetchOptions: {
headers: {
Referer: "http://examples.maptalks.com/",
token: "your token",
//others params
},
//other config
},
});
页面里加载了大量的TileLayer导致webgl环境崩溃
每一个TileLayer实例都会拥有一个webgl上下文,浏览器里webgl的上下文数量是有限的,页面里创建了大量的 TileLayer导致超出浏览器的上限,所以才会导致这个错误,解决方式时将 TileLayer放到GroupTileLayer里,让所有的TileLayer公用一个webgl上下文
new maptalks.GroupTileLayer("base", [
new maptalks.TileLayer("tile2", {
urlTemplate:
"https://{s}.basemaps.cartocdn.com/light_nolabels/{z}/{x}/{y}.png",
subdomains: ["a", "b", "c", "d"],
}),
new maptalks.TileLayer("boudaries", {
urlTemplate:
"https://{s}.basemaps.cartocdn.com/dark_only_labels/{z}/{x}/{y}.png",
subdomains: ["a", "b", "c", "d"],
}),
]);
WARNING
GroupTileLayer里的所有的TileLayer他们的坐标投影应该一致,不可将不同投影坐标的TileLayer放到一起
获取瓦片的边界范围Extent
TileLayer默认没有提供对应的方法,但是我们可以自己手动添加下对应的方法的,这里我们在TileLayer 的原型下挂载了两个方法
//获取投影的范围
maptalks.TileLayer.prototype._getTilePrjExtent = function (x, y, z) {
const map = this.getMap(),
res = map._getResolution(z),
tileConfig = this._getTileConfig(),
tileExtent = tileConfig.getTilePrjExtent(x, y, res);
return tileExtent;
};
//获取经纬度范围
maptalks.TileLayer.prototype._getTileExtent = function (x, y, z) {
const prjExtent = this._getTilePrjExtent(x, y, z);
const map = this.getMap();
const { xmin, ymin, xmax, ymax } = prjExtent;
const pmin = new maptalks.Point(xmin, ymin),
pmax = new maptalks.Point(xmax, ymax);
const projection = map.getProjection();
const min = projection.unproject(pmin),
max = projection.unproject(pmax);
return new maptalks.Extent(min, max);
};
这样我们就可以根据瓦片的行列号获取瓦片对应的范围了Extent
了
WARNING
- 不是所有的投影的都是有经纬度的概念的
- 我们常见的 EPSG3857和EPSG4326有经纬度的概念,如果是平面投影(identity)只有米的坐标,即投影坐标
- EPSG3857也是平面投影的,坐标是米制的,只是大家习惯了输入的是经纬度坐标而已,其实引擎内部将用户输入的经纬度转成了投影坐标在内部参与各种计算
- EPSG4326的投影坐标和经纬度是一样的,所以内部不需要转化,所以EPSG4326的性能是高于EPSGE3857的,因为少了一个坐标转换的步骤
const tileLayer = new maptalks.TileLayer("base", {
// debug: true,
urlTemplate:
"https://webrd01.is.autonavi.com/appmaptile?lang=zh_cn&size=1&scale=1&style=8&x={x}&y={y}&z={z}",
subdomains: ["a", "b", "c", "d"],
debug: true,
});
const extent = tileLayer._getTileExtent(x, y, z);
怎样瓦片纠偏?
有时我们项目加载了高德的瓦片,但是数据是WGS84,导致矢量数据和瓦片图层对应不上,怎样纠偏呢? TileLayer有个配置参数叫offset
可以用来对瓦片进行偏移
var map = new maptalks.Map("map", {
center: [116.45266161, 39.86656647],
zoom: 10,
baseLayer: new maptalks.TileLayer("base", {
// debug: true,
urlTemplate:
"https://webrd01.is.autonavi.com/appmaptile?lang=zh_cn&size=1&scale=1&style=8&x={x}&y={y}&z={z}",
subdomains: ["a", "b", "c", "d"],
offset: function (z) {
const map = this.getMap();
const center = map.getCenter();
//坐标转换的第三方库 https://github.com/hujiulong/gcoord
const c = gcoord.transform(
center.toArray(),
gcoord.AMap,
gcoord.WGS84,
);
const offset = map
.coordToPoint(center, z)
.sub(map.coordToPoint(new maptalks.Coordinate(c), z));
return offset._round().toArray();
},
}),
});
TIP
改例子里演示的矢量数据是标准的WGS84的
关于其他不同坐标系的的转换以此类推,比如天地图的瓦片转成火星的,具体的请参阅gcoord
offset: function (z) {
const map = this.getMap();
const center = map.getCenter();
//坐标转换的第三方库 https://github.com/hujiulong/gcoord
const c = gcoord.transform(center.toArray(), gcoord.WGS84,gcoord.AMap);
const offset = map.coordToPoint(center, z).sub(map.coordToPoint(new maptalks.Coordinate(c), z));
return offset._round().toArray();
},
设置TileLayer的整体海拔
new maptalks.TileLayer("tile", {
altitude: -1000,
urlTemplate:
"https://services.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}",
subdomains: ["a", "b", "c", "d"],
attribution:
'© <a href="http://osm.org">OpenStreetMap</a> contributors, © <a href="https://carto.com/">CARTO</a>',
});
一般设置为负的值
注意把TileLayer的海拔高度设置为负的目的是不要抬高海平面,方便其他的业务图层数据加到地图,否则会要求其他图层也要设置海拔数据,导致业务逻辑变复杂了,这个样子最简单和不容易出错
自定义瓦片请求地址
TileLayer 的urlTemplate 支持函数的
function getTileUrl(x, y, z, domain) {
const url =
"https://ecn.{subdomain}.tiles.virtualearth.net/tiles/a{quadkey}.jpeg?g=14583";
var quadkey = toQuadKey(x, y, z);
const urlTemplate = url;
// let domain = '';
return urlTemplate
.replace("{quadkey}", quadkey)
.replace("{subdomain}", domain);
}
const layer = new maptalks.TileLayer("base", {
//other server url
//https://github.com/digidem/leaflet-bing-layer
urlTemplate: getTileUrl,
subdomains: ["t0", "t1", "t2", "t3"],
maxAvailableZoom: 18,
maskClip: true,
});
const layer = new maptalks.TileLayer("base", {
//other server url
//https://github.com/digidem/leaflet-bing-layer
urlTemplate:
"https://ecn.{subdomain}.tiles.virtualearth.net/tiles/a{quadkey}.jpeg?g=14583",
subdomains: ["t0", "t1", "t2", "t3"],
maxAvailableZoom: 18,
maskClip: true,
});
function toQuadKey(x, y, z) {
var index = "";
for (var i = z; i > 0; i--) {
var b = 0;
var mask = 1 << (i - 1);
if ((x & mask) !== 0) b++;
if ((y & mask) !== 0) b += 2;
index += b.toString();
}
return index;
}
layer.getTileUrl = function (x, y, z) {
var quadkey = toQuadKey(x, y, z);
const urlTemplate = this.options.urlTemplate;
let domain = "";
if (this.options["subdomains"]) {
const subdomains = this.options["subdomains"];
if (Array.isArray(subdomains)) {
const length = subdomains.length;
let s = (x + y) % length;
if (s < 0) {
s = 0;
}
domain = subdomains[s];
}
}
return urlTemplate
.replace("{quadkey}", quadkey)
.replace("{subdomain}", domain);
};
layer.addTo(map);
加载天地图的服务
目前天地图服务提供了两种投影坐标服务
- EPSG3857 墨卡托
- EPSG4326 经纬度
详情参阅天地图的文档,请仔细阅读相关服务文档和地址, 已经有好多同学在这里犯错误了,体现在: 明明代码里写的4326投影,图层地址却偏偏指向墨卡托的服务地址
WARNING
太多同学犯这个错误了,所以请仔细点
太多同学犯这个错误了,所以请仔细点
太多同学犯这个错误了,所以请仔细点
加载4326的服务
const map = new Map("map", {
center: [91.14478, 29.658272],
zoom: 5,
spatialReference: {
projection: "EPSG:4326",
},
});
const baseLayer = new TileLayer("base", {
tileSystem: [1, -1, -180, 90],
urlTemplate:
"http://t0.tianditu.com/DataServer?T=img_c&x={x}&y={y}&l={z}&tk=your token",
spatialReference: {
projection: "EPSG:4326",
},
}).addTo(map);
加载3857服务
const map = new Map("map", {
center: [91.14478, 29.658272],
zoom: 5,
spatialReference: {
projection: "EPSG:3857",
},
});
const baseLayer = new TileLayer("base", {
// tileSystem: [1, -1, -180, 90],
urlTemplate:
"http://t0.tianditu.com/DataServer?T=img_w&x={x}&y={y}&l={z}&tk=your token",
spatialReference: {
projection: "EPSG:3857",
},
}).addTo(map);
怎样加载自定义切图参数的瓦片
WARNING
当我们自己发布切片服务时要尽可能的使用常用的全球切图参数,不要随便的去自定义切图参数, 因为使用常见的全球切图参数是我们使用地图最佳的方式,方便和矢量切片,3dtile等图层结合. 当且仅当在我们对接第三方提供的服务时才会用到这里介绍的方法(因为我们没有第三方服务的控制权限,我们是被动的,不得已而为之)。
总之:不要给自己找麻烦上强度,实在没事情干可以刷刷抖音看看小姐姐啥的
有时我们加载的瓦片服务,其不是按照标准的全球的这个切图参数切图的
- 标准的全球墨卡托切图参数 切图参数详情
- 标准的4326全球切图参数
[
1.40625, 0.703125, 0.3515625, 0.17578125, 0.087890625, 0.0439453125,
0.02197265625, 0.010986328125, 0.0054931640625, 0.00274658203125,
0.001373291015625, 0.0006866455078125, 0.00034332275390625,
0.000171661376953125, 0.0000858306884765625, 0.00004291534423828125,
0.000021457672119140625, 0.000010728836059570312, 0.000005364418029785156,
0.000002682209014892578, 0.000001341104507446289, 6.705522537231445e-7,
3.3527612686157227e-7
]
打开这个例子(天地图)https://maptalks.org/examples/en/tilelayer-projection/epsg4326/raw/index.html ,调试窗口执行
JSON.stringify(map.getSpatialReference()._resolutions);
来查看标准的标准的4326全球切图参数
不是按照标准的全球的这个切图参数切图的怎么加载呢?
宗旨
地图的投影要尽可能的使用全球的标准的切图参数,图层的切图参数要通过一定的方法使其接近全球的切图参数,不要随随便便的改变地图 的 spatialReference
,因为地图处于标准的全球切片参数下是我们使用地图最佳的方式
- 地图为核心,采用标准的全球的切图参数
- 图层应该尽可能的想地图的切图参数靠拢,而不是地图随着图层走
切图的第一个层级和全球的切图参数接近
从图片看其是个4326的切图,且第一个切图层级的比例尺和全球的第一个层级切图比列尺(591657527.591555 )非常接近
这时我们只需要自定义TileLayer
的 spatialReference
参数
瓦片和地图都有自己的 spatialReference
参数,只需要按照瓦片的切图参数来构造 TileLayer的 spatialReference
即可
//切图第一个参数分辨率
const RES = 1.4078260157100582;
//原点
const ORIGIN = [-180, 90];
//切图的分辨率集合
const resolutions = [];
//一共18个层级
let i = 0;
while (i <= 17) {
resolutions.push(RES / Math.pow(2, i));
i++;
}
const spatialReference = {
projection: "EPSG:4326",
resolutions: resolutions,
};
var map = new maptalks.Map("map", {
center: [120.84600742, 31.14241977],
zoom: 0,
zoomControl: true,
spatialReference: {
projection: "EPSG:4326", //or 3857
},
});
const tileLayer = new maptalks.TileLayer("tilelayer,", {
urlTemplate: "xxxxxx/MapServer/tile/{z}/{y}/{x}",
tileSystem: [1, -1].concat(ORIGIN),
spatialReference,
});
map.setBaseLayer(tileLayer);
WARNING
注意地图的spatialReference
是标准的4326,但是Tilelayer的spatialReference
不是的, 我们自定义了TileLayer里的resolutions
,地图内部会自动的吧TileLayer图层数据做转换的
切图的第一个层级和全球的切图参数差距比较大
这种情况在我们做项目时经常遇到,比如服务是一个县市这种,服务的第一个切图参数和标准的全球切图层数差的不是一点半点
//切图第一个参数分辨率
const RES = 0.002749664687500373;
//原点
const ORIGIN = [-400, 400];
//切图的分辨率集合
const resolutions = [];
//一共10个层级
let i = 0;
while (i <= 9) {
resolutions.push(RES / Math.pow(2, i));
i++;
}
//for tilelayer
const spatialReference = {
projection: "EPSG:4326",
resolutions: resolutions,
};
var map = new maptalks.Map("map", {
center: [120.84600742, 31.14241977],
zoom: 0,
zoomControl: true,
});
function switchSp() {
//设置地图的投影为自定义的
map.setSpatialReference(spatialReference);
const tileLayer = new maptalks.TileLayer("tilelayer,", {
urlTemplate:
"http://180.108.205.111:6080/arcgis/rest/services/CYLMap_PCZH/MapServer/tile/{z}/{y}/{x}",
tileSystem: [1, -1].concat(ORIGIN),
spatialReference,
});
map.setBaseLayer(tileLayer);
}
switchSp();
关于切图参数,一般瓦片服务的供应商都会给出来的,如果服务是arcgis或者supermap等发布的,在服务里接口里都会给出来的,如果服务里看不到请咨询服务的提供商.
DANGER
这里我们使地图的spatialReference
和TileLayer的spatialReference
相等了 (setSpatialReference方法设置了,明显违背了上边说的宗旨),这样会有一些问题,比如在地图添加矢量切片图层等 这些图层将不能被正常加载,因为像矢量切片图层他们的切图参数是固定的,但是我们改变了地图的spatialReference
从而导致矢量切片 等图层不能被正常加载
当然针对这个问题,还有更好的解决方法,我们称之为切图的补位法
:
切图的补位法
- 地图的投影还是尽可能的使用全球的的切图参数,因为这样是我们使用地图最舒服的方式,也方便加载其他的图层服务,比如矢量切片,倾斜图层
- 瓦片的投影参数通过补位法尽可能的使其和地图一样,这样就可以做到地图投影信息不变了,且可以加载这个瓦片服务
//切图参数
const RES = 0.002749664687500373;
//原点
const ORIGIN = [-400, 400];
let resolutions = [];
let i = 0;
//一共10个层级
while (i <= 9) {
resolutions.push(RES / Math.pow(2, i));
i++;
}
const offsetRes = [];
//前面缺失的层级,一共有9级,补起来,让其切图参数尽可能的和地图一样
const zoomOffset = 10;
i = 1;
while (i < zoomOffset) {
const res = RES * Math.pow(2, zoomOffset - i);
offsetRes.push(res);
i++;
}
resolutions = offsetRes.concat(resolutions);
console.log(resolutions);
//构造的spatialReference,使其尽可能的和全球的切图参数一样的
const spatialReference = {
projection: "EPSG:4326",
resolutions: resolutions,
};
var map = new maptalks.Map("map", {
center: [120.84600742, 31.14241977],
zoom: 10,
zoomControl: true,
//地图还是用标准的切图参数
spatialReference: {
projection: "EPSG:4326",
},
});
function switchSp() {
console.log(map.getSpatialReference()._resolutions);
const tileLayer = new maptalks.TileLayer("tilelayer,", {
urlTemplate:
"http://180.108.205.111:6080/arcgis/rest/services/CYLMap_PCZH/MapServer/tile/{z}/{y}/{x}",
tileSystem: [1, -1].concat(ORIGIN),
spatialReference,
debug: true,
});
//重写瓦片的请求函数
tileLayer.getTileUrl = function (x, y, z) {
//replace with your own
//e.g. return a url pointing to your sqlite database
z -= 9;
if (z < 0) {
//z<0时返回自定义的图片
return "./../assets/image/default-image1.png";
}
return maptalks.TileLayer.prototype.getTileUrl.call(this, x, y, z);
};
map.setBaseLayer(tileLayer);
}
switchSp();
在这个过程中,有两个地方需要注意:
- 怎样判定图层需要往前补多少个层级:通过切图的比例尺来判定,以这个例子切图参数里第一个层级的比例尺为
1155583
, 然后我们从 上面的的全球的切图参数里可以看多,和这个1155583
最接近的比例尺为层级的9
的1155581.108577
, 故而推断出 前面缺少了0-8
这九个层级,看到这里相信你应该知道怎么来判断一个非标准的切图:用自己的切图的第一个比例尺, 从全球的切图参数里去找和你服务第一个切图参数最接近的比例尺,从而确定你的切图的第一个层级在全球切图参数里处于第几个层级,然后把前边缺省的补起来 - 瓦片的请求函数需要重写,因为构造了补位,导致瓦片的请求层级需要减去补位的层级数,上面的例子是补位了9个层级,那么就减去9,其他情况以此类推
加载GeoServer发布的瓦片服务
geoserver发布的瓦片服务一般有EPSG990913
(EPSG3857
)和EPSG4326
- geoserver的 3857切片是标准的全球切图
- geoserver的 4326切片不是标准的全球切图,其0级是全球的标准切图的1级
标准的4326全球切图参数
[
1.40625, 0.703125, 0.3515625, 0.17578125, 0.087890625, 0.0439453125,
0.02197265625, 0.010986328125, 0.0054931640625, 0.00274658203125,
0.001373291015625, 0.0006866455078125, 0.00034332275390625,
0.000171661376953125, 0.0000858306884765625, 0.00004291534423828125,
0.000021457672119140625, 0.000010728836059570312, 0.000005364418029785156,
0.000002682209014892578, 0.000001341104507446289, 6.705522537231445e-7,
3.3527612686157227e-7
]
打开这个例子(天地图)https://maptalks.org/examples/en/tilelayer-projection/epsg4326/raw/index.html ,调试窗口执行
JSON.stringify(map.getSpatialReference()._resolutions);
来查看标准的标准的4326全球切图参数
这个是geoserver默认的切图参数
var resolutions = [
0.703125, 0.3515625, 0.17578125, 0.087890625, 0.0439453125, 0.02197265625,
0.010986328125, 0.0054931640625, 0.00274658203125, 0.001373291015625,
6.866455078125e-4, 3.4332275390625e-4, 1.71661376953125e-4,
8.58306884765625e-5, 4.291534423828125e-5, 2.1457672119140625e-5,
1.0728836059570312e-5, 5.364418029785156e-6, 2.682209014892578e-6,
1.341104507446289e-6, 6.705522537231445e-7, 3.3527612686157227e-7,
];
很明显其前面少了个层级对不对?
加载 3857瓦片
function test3857() {
const url = `
http://localhost/geoserver/deyihu/gwc/service/wmts?REQUEST=GetTile&SERVICE=WMTS&VERSION=1.0.0&LAYER=deyihu:jiangsu-area&STYLE=&TILEMATRIX=EPSG:900913:{z}&TILEMATRIXSET=EPSG:900913&format=image%2Fpng&TILECOL={x}&TILEROW={y}
`;
const vtLayer = new maptalks.TileLayer("vt", {
debug: true,
urlTemplate: url,
});
groupGLLayer.addLayer(vtLayer);
}
加载4326的瓦片
因为geoserver 4326切片不是默认的全球切图参数所以我们要自定义切图参数
function test4326() {
map.setSpatialReference({
projection: "EPSG:4326",
});
const RES = 0.703125;
const ORIGIN = [-180, 90];
const resolutions = [];
let i = 0;
while (i <= 18) {
resolutions.push(RES / Math.pow(2, i));
i++;
}
const spatialReference = {
projection: "EPSG:4326",
resolutions: resolutions,
};
const url = `
http://localhost/geoserver/deyihu/gwc/service/wmts?REQUEST=GetTile&SERVICE=WMTS&VERSION=1.0.0&LAYER=deyihu:jiangsu-area&STYLE=&TILEMATRIX=EPSG:4326:{z}&TILEMATRIXSET=EPSG:4326&FORMAT=image%2Fpng&TILECOL={x}&TILEROW={y}
`;
const vtLayer = new maptalks.TileLayer("vt", {
debug: true,
urlTemplate: url,
tileSystem: [1, -1].concat(ORIGIN),
spatialReference,
});
groupGLLayer.addLayer(vtLayer);
}
WARNING
注意地图还是标准的全球4326切片,TileLayer我们自定义了spatialReference
加载supermap发布的瓦片服务
supermap
的iserver
发布的服务和我们平时的常见的arcgis
服务还是有点区别的
- 常见的墨卡托的瓦片和我们常见的互联网瓦片一样的,直接传
x
,y
,z
等参数即可 - 如果是4326服务的,那和我们平时的就完全不一样了,其要传递个
scale
参数,scale
表示当前地图的缩放比例 而且这个scale
这个值需要动态的计算
从这个服务的界面我们可以看到supermap iserver 支持很多的操作服务的,我们一般常用的有:
zxyTileImage
就是我们平时用的互联网墨卡托服务
WARNING
zxyTileImage 服务自带投影转化功能的:
- 不管你原始发布的服务都是是么坐标投影的,服务端输出的都是我们常见的墨卡托
- 所以你的项目的主坐标系统是墨卡托的话最好直接使用这个服务,可以省去你大量的折腾坐标系的时间
tileImage
就是我们上面说的需要传递scale
参数来获取瓦片的服务接口,一般都是4326的服务,当然也可以 是其他的投影获取瓦片的接口,只是我们发布的服务一般都是4326
EPSG3857的方式加载
上面我们也说了 zxyTileImage
可以直接输出墨卡托,所以使用起来就方面了
const url = `
http://support.supermap.com.cn:8090/iserver/services/map-world/rest/maps/World/zxyTileImage.png?z={z}&x={x}&y={y}&width=256&height=256
`;
const vtLayer = new maptalks.TileLayer("vt", {
debug: true,
urlTemplate: url,
spatialReference: {
projection: "EPSG:3857",
},
});
groupGLLayer.addLayer(vtLayer);
WARNING
注意:即使你的原始服务不是3857的,以这种方式加载瓦片输出的永远都是墨卡托,所以不要去瞎折腾坐标投影的问题, 也是我们推荐的做法
EPSG4326的方式加载
有的同学可能说:我的项目里主坐标系就是4326
的,不用supermap iclient
怎样来加载呢?
WARNING
注意我们这里讨论的前提是:项目里的主坐标系是4326,即地图的空间投影是4326
const map = new maptalks.Map("map", {
center: [120.58443009853363, 31.077306186678225],
zoom: 3,
bearing: 0,
pitch: 0,
centerCross: true,
spatialReference: {
projection: "EPSG:4326",
},
});
因为iserver
发布的服务属于非主流,和我们常见的直接通过直接传 x
,y
,z
来请求不同,需要我们动态传递个 scale
参数,所以计算scale
就成为我们核心难点
scale
怎样计算我也不懂,我是通过翻看iclient
的源码来获取的😜 iClient-JavaScript
下面直接贴代码
function test4326() {
// A complete customized TileLayer
// Radius of the earth
var earchRadiusInMeters = 6378137;
var inchPerMeter = 1 / 0.0254;
var meterPerMapUnit = (Math.PI * 2 * earchRadiusInMeters) / 360;
function replaceURL(url, x, y, scale) {
var str = ["x", x, "y", y, "scale", scale];
for (var i = 0, len = str.length; i < len; i += 2) {
url = url.replace("{" + str[i] + "}", str[i + 1]);
}
return url;
}
function resolutionToScale(resolution, dpi) {
var scale = resolution * dpi * inchPerMeter * meterPerMapUnit;
scale = 1 / scale;
return scale;
}
//切图参数
var parmas = {
zooms: 18,
//第一个比例尺
// http://support.supermap.com.cn:8090/iserver/services/map-world/rest/maps/World.leaflet
firstRes: 1.4062499999999996,
origin: [-180, 90],
maxBounds: [
[-180, -90],
[180, 90],
],
};
var url =
"http://support.supermap.com.cn:8090/iserver/services/map-world/rest/maps/World/tileImage.png?scale={scale}&x={x}&y={y}&width=256&height=256&layersID=&tileversion=&transparent=false&prjCoordSys=%7B%22epsgCode%22%3A4326%7D";
var res = [];
for (var i = 0; i <= parmas.zooms; i++) {
res.push(parmas.firstRes / Math.pow(2, i));
}
var crs = {
projection: "EPSG:4326",
resolutions: res,
// fullExtent: {
// top: 42.31,
// left: 114.59,
// bottom: 37.44232891378436,
// right: 119.45767108621564
// }
};
var tileLayer = new maptalks.TileLayer("base", {
debug: true,
repeatWorld: true,
urlTemplate: url,
spatialReference: crs,
subdomains: ["a", "b", "c", "d"],
attribution:
'© <a href="https://www.supermap.com/cn/">supermap</a> contributors',
tileSystem: [1, -1].concat(parmas.origin), // tile system
});
// 重写瓦片获取的方法
tileLayer.getTileUrl = function (x, y, z) {
this.scales = this.scales || {};
if (this.scales[z]) {
return replaceURL(this.options.urlTemplate, x, y, this.scales[z]);
}
var crs = this.getMap().getSpatialReference().getProjection();
var bounds = this._getTileExtent(x, y, z);
var min = bounds.getMin(),
max = bounds.getMax();
var ne = crs.project(max);
var sw = crs.project(min);
var tileSize = this.options.tileSize[0];
var resolution = Math.max(
Math.abs(ne.x - sw.x) / tileSize,
Math.abs(ne.y - sw.y) / tileSize,
);
var scale = resolutionToScale(resolution, 96);
this.scales[z] = scale;
return replaceURL(this.options.urlTemplate, x, y, this.scales[z]);
};
//获取瓦片的bbox
tileLayer._getTileExtent = function (x, y, z) {
var map = this.getMap(),
res = map._getResolution(z),
tileConfig = this._getTileConfig(),
tileExtent = tileConfig.getTilePrjExtent(x, y, res);
return tileExtent;
};
tileLayer.addTo(groupGLLayer);
}
代码里有几点需要注意:
- 切图参数第一个分辨率
firstRes
//切图参数
var parmas = {
zooms: 18,
//第一个比例尺
// http://support.supermap.com.cn:8090/iserver/services/map-world/rest/maps/World.leaflet
firstRes: 1.4062499999999996,
origin: [-180, 90],
maxBounds: [
[-180, -90],
[180, 90],
],
};
第一个切图的分辨率我们从在线的iserver
里查看
输出的代码里会把这些切图参数给出来的
- 整个服务的边界范围
iserver
服务界面都会给出来的
至于其他的逻辑基本都不用动
WARNING
注意需要把地图的空间投影也设置为4326,因为
//获取瓦片的bbox
tileLayer._getTileExtent = function (x, y, z) {
var map = this.getMap(),
res = map._getResolution(z),
tileConfig = this._getTileConfig(),
tileExtent = tileConfig.getTilePrjExtent(x, y, res);
return tileExtent;
};
要动态的获取每个瓦片的bbox来,如果不是4326的那么获取的范围就不是经纬度了
WARNING
如果瓦片服务不是标准的全球切图,请参阅改篇幅里的 怎样加载自定义切图参数的瓦片
加载cesiumlab切的瓦片集合
cesiumlabe切得瓦片和GeoServer一样的,缺少了第0级的数据
- 虽然切的数据包有0级,但是不是标准的全球切图的0级,其其实是从标准的1层级切图的
- 他们为什么这么做我也不知道
解决方法:
手动构造下瓦片的切图的分辨率即可
//这时全球的标准的切图的第0级的切图参数
// const RES = 1.40625;
//这个是cesiumlabe的第0级切图参数
const RES = 0.703125;
//原点
const ORIGIN = [-180, 90];
//切图的分辨率集合
const resolutions = [];
//一共10个层级
let i = 0;
while (i <= 10) {
resolutions.push(RES / Math.pow(2, i));
i++;
}
//for tilelayer
const spatialReference = {
projection: "EPSG:4326",
resolutions: resolutions,
};
完整代码:
const RES = 0.703125;
//原点
const ORIGIN = [-180, 90];
//切图的分辨率集合
const resolutions = [];
//一共10个层级
let i = 0;
while (i <= 10) {
resolutions.push(RES / Math.pow(2, i));
i++;
}
//for tilelayer
const spatialReference = {
projection: "EPSG:4326",
resolutions: resolutions,
};
const map = new maptalks.Map("map", {
zoomControl: true,
center: [105.7, 33.7],
zoom: 10,
spatialReference: {
projection: "EPSG:4326",
},
baseLayer: new maptalks.TileLayer("base", {
tileSystem: [1, -1].concat(ORIGIN),
spatialReference,
urlTemplate: "./chengxian18/{z}/{x}/{y}.png",
subdomains: ["a", "b", "c", "d"],
attribution:
'© <a href="http://osm.org">OpenStreetMap</a> contributors, © <a href="https://carto.com/">CARTO</a>',
}),
});
加载Bing地图瓦片
Bing的瓦片有点特殊,不是简单的x,y,z这种请求方式,需要对x,y,z进行编码下请求.
这里我直接给代码,具体的详情请参阅 leaflet-bing-layer
var map = new maptalks.Map("map", {
center: [121.44224697, 31.12893667],
zoom: 9.309009504457512,
pitch: 0,
bearing: 0,
// seamlessZoom: false,
// cameraInfiniteFar: true,
zoomControl: true,
// debugSky: true
});
const layer = new maptalks.TileLayer("base", {
//other server url
//https://github.com/digidem/leaflet-bing-layer
urlTemplate:
"https://ecn.{subdomain}.tiles.virtualearth.net/tiles/a{quadkey}.jpeg?g=14583",
subdomains: ["t0", "t1", "t2", "t3"],
maxAvailableZoom: 18,
maskClip: true,
});
function toQuadKey(x, y, z) {
var index = "";
for (var i = z; i > 0; i--) {
var b = 0;
var mask = 1 << (i - 1);
if ((x & mask) !== 0) b++;
if ((y & mask) !== 0) b += 2;
index += b.toString();
}
return index;
}
layer.getTileUrl = function (x, y, z) {
var quadkey = toQuadKey(x, y, z);
const urlTemplate = this.options.urlTemplate;
let domain = "";
if (this.options["subdomains"]) {
const subdomains = this.options["subdomains"];
if (Array.isArray(subdomains)) {
const length = subdomains.length;
let s = (x + y) % length;
if (s < 0) {
s = 0;
}
domain = subdomains[s];
}
}
return urlTemplate
.replace("{quadkey}", quadkey)
.replace("{subdomain}", domain);
};
layer.addTo(map);
加载船讯网图层
船讯网图层是EPSG3395的,其和我们常见的EPSG3857还是有区别的,maptalks内部不直接 支持 EPSG3395的,所以需要我们利用proj4自定义下投影信息
// https://epsg.io/3395
var proj3395 =
"+proj=merc +lon_0=0 +k=1 +x_0=0 +y_0=0 +datum=WGS84 +units=m +no_defs +type=crs";
var proj = proj4("WGS84", proj3395);
// define a custom projection object
var projection = {
code: "EPSG:3395", // code of the projection
project: function (c) {
// from wgs84 to EPSG:3395
var pc = proj.forward(c.toArray());
return new maptalks.Coordinate(pc);
},
unproject: function (pc) {
// from EPSG:3395 to wgs84
var c = proj.inverse(pc.toArray());
return new maptalks.Coordinate(c);
},
// tell projection how to measure
// for cartesian coordinates change this to:
// measure: 'identity'
measure: "EPSG:4326",
};
const baseLayer = new maptalks.TileLayer("base", {
urlTemplate:
"https://api.shipxy.com/apicall/GetMTile?k=1F6D701272402D1E7D8D316CCE519123&x={x}&y={y}&z={z}",
subdomains: ["a", "b", "c", "d"],
// decodeImageInWorker: true,
// fetchOptions: {
// headers: {
// Referer: "https://api.shipxy.com/",
// // token: "your token",
// //others params
// },
// //other config
// },
// attribution: '© <a href="http://osm.org">OpenStreetMap</a> contributors, © <a href="https://carto.com/">CARTO</a>',
spatialReference: {
projection: projection, // geo projection, defined by proj4js
resolutions: [
// map's zoom levels and resolutions
156543.03392804097, 78271.51696402048, 9135.75848201024, 19567.87924100512,
9783.93962050256, 4891.96981025128, 2445.98490512564, 1222.99245256282,
611.49622628141, 305.748113140705, 152.8740565703525, 76.43702828517625,
38.21851414258813, 19.109257071294063, 9.554628535647032, 4.777314267823516,
2.388657133911758, 1.194328566955879, 0.5971642834779395, 0.29858214173896974,
],
fullExtent: {
// map's full extent
top: 18764656.23,
left: -20037508.34,
bottom: -12496570.74,
right: 20037508.34,
},
},
});
常见问题
加载的瓦片模糊
可能的原因有:
- 地图默认是开启无极缩放的(seamlessZoom),即地图的缩放层级可以到小数,比如10.4这样,这时加载的瓦片还是 10层级的瓦片,所以就会导致瓦片被缩放一定的倍数导致模糊
- 浏览器缩放或者高分屏上,这时还用常规的256x256的瓦片就会模糊,假设浏览器缩放到200%,这时瓦片的显示大小就是 512x512了,但是瓦片的原始大小是256x256
解决方法:
- 关闭地图的无极缩放
const map = new maptalks.Map("map", {
center: [8.22, 53.14],
zoom: 13,
zoomControl: true,
seamlessZoom: false,
// devicePixelRatio: 4,
baseLayer: new maptalks.TileLayer("base", {
urlTemplate:
"https://maps.omniscale.net/v2/mapsosc-b667cf5a/style.default/{z}/{x}/{y}.png?hq=true",
subdomains: ["a", "b", "c", "d"],
attribution:
"© <a href='http://osm.org'>OpenStreetMap</a> contributors, © <a href='https://carto.com/'>CARTO</a>",
}),
});
- 使用高清的瓦片源,其原理就是用512x512的瓦片当做256x256的瓦片来绘制,来解决高分屏上的模糊问题,其他倍数的缩放以此类推, 比如缩放了4倍,就需要用1024x1024的瓦片来作为数据源了, 比如这地址就是2倍的高分屏瓦片服务地址:
https://maps.omniscale.net/v2/mapsosc-b667cf5a/style.default/{z}/{x}/{y}.png?hq=true
其本质就是用的2倍的瓦片大小来当成一倍的瓦片来绘制
使用矢量切片,因为矢量切片是在前端完成数据的绘制的,所以不存在图片放大模糊的问题, 不管是高分屏还是地图的无极缩放都不会影响瓦片的绘制效果
加载更高级别的瓦片
比如当前地图层级为10,加载11级别的瓦片来填充,假设瓦片大小为256,将瓦片大小缩小一倍(实际上的图片还是256 但是绘制是128)
var map = new maptalks.Map("map", {
center: [120, 31.498568],
zoom: 9,
seamlessZoom: true,
pitch: 0,
baseLayer: new maptalks.TileLayer("base", {
debug: true,
tileSize: 128,
zoomOffset: 1,
urlTemplate:
"https://webrd01.is.autonavi.com/appmaptile?lang=zh_cn&size=1&scale=1&style=8&x={x}&y={y}&z={z}",
subdomains: ["a", "b", "c", "d"],
}),
});
WARNING
这个方法会导致瓦片的数据量急剧膨胀,性能会下降很多,请小心使用
瓦片之间有缝隙
WARNING
该特性要求maptalks版本 version>v1.0.0-rc.25
TileLayer 每个瓦片buffer了0.5个像素,这样做的目的是因为在高分屏上会有白色的缝隙,所以内部就buffer了0.5像素 但是当瓦片是透明的时候也带来了新的问题,因为瓦片是透明,相邻的瓦片因为buffer的原因导致了覆盖,从而导致透明 叠加透明,导致瓦片边界处颜色加深了
解决方法:TileLayer提供了bufferPixel
选项,你可以设置每个瓦片的buffer像素,业务里根据自己的需要设置合适的值 即可
const layer=new maptalks.TileLayer('base', {
bufferPixel: 0,
repeatWorld: false,
urlTemplate: 'xxxxxxxxxxxxxxxxxx’,
subdomains: ["a", "b", "c", "d"],
attribution: '© <a href="http://osm.org">OpenStreetMap</a> contributors, © <a href="https://carto.com/">CARTO</a> '
})
subdomains 参数的价值和意义
subdomains的价值主要为了服务端负载均衡和突破前端网络请求并发限制
因为瓦片是个高频请求,一个TileLayer单位时间内会产生大量的请求,一般一个屏幕下一个TileLayer 会产生几十个请求,如果的你的地图比较的大,请求数量会更多,地图越大瓦片越多当然请求越多
subdomains 参数可以使瓦片的请求分散到不同的服务端主机上去,
new maptalks.TileLayer("base", {
urlTemplate: "https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}.png",
subdomains: ["a", "b", "c", "d"],
attribution:
"© <a href='http://osm.org'>OpenStreetMap</a> contributors, © <a href='https://carto.com/'>CARTO</a>",
});
以上面的代码为例:
瓦片请求会负担到
https://a.basemaps.cartocdn.com
https://b.basemaps.cartocdn.com
https://c.basemaps.cartocdn.com
https://d.basemaps.cartocdn.com
四台主机上,可以减轻服务端单台服务器的压力,互联网常用的抗高并发的手段之一
另外浏览器里针对一个主机的请求并发是有数量限制,以chrome为例我记得一般是是6个(可能不准确,如有问题请纠错) 现在用了四个主机,那就意味着可以拥有用24个瓦片请求并发了,可以使瓦片更加快速的出图了
该技术一般用于互联网业务,因为互联网用户量大,tilelayer本来就是高频请求场景, 加上用户量多,如果用单台服务器压力就比较大了
如果平时做项目的自己搭建服务的,一般用不上,你的业务么有那么多用户压力,毕竟多台服务器 意味真更大的经济成本和维护成本
TileVectorLayer矢量瓦片图层,因为请求是数据切片而不是图片了(数据体积比图片要大多了),那么对服务端压力就更加大了, subdomains就更能凸显其价值了
总结:不要为了用而用,如果你真的遇到瓶颈才用也不迟,毕竟维护多个服务器高可用成本还是挺高的