Skip to content
目录

TileLayer

GIS理论知识(四)之地图的图层(切片/瓦片)概念

TileLayer栅格图层,是每个地图引擎必备的功能,maptalks内部内置了常见坐标投影的瓦片的支持

  • EPSG:3857
  • EPSG:4326
  • EPSG:4490

当我们创建地图或者TileLayer时如果不指定spatialReference时,内部将默认采用EPSG:3857,所以当你的瓦片坐标投影不是默认的切图参数需要配置这个参数的

默认的墨卡托投影切图参数科普

TIP

这里只是做个简单的科普,这些参数主要用来和自定义切图对比,方便查找对应的比例尺对应关系. 切图参数详情

  • 全球墨卡托

  • 切图原点: [-20037508.342787,20037508.342787]

    层级分辨率比例尺
    0156543.03392800014591657527.591555
    178271.51696399994295828763.795777
    239135.75848200009147914381.897889
    319567.87924100001773957190.948944
    49783.93962050000836978595.474472
    54891.96981025000418489297.737236
    62445.9849051250029244648.868618
    71222.9924525625014622324.434309
    8611.49622628125052311162.217155
    9305.748113140625261155581.108577
    10152.87405657031263577790.554289
    1176.43702828515632288895.277144
    1238.21851414257816144447.638572
    1319.1092570712890872223.819286
    149.5546285356445436111.909643
    154.7773142678222718055.954822
    162.3886571339111359027.977411
    171.19432856695556744513.988705
    180.59716428347778372256.994353

墨卡托投影一般常见于互联网,当我们做项目时,用的瓦片好多都是EPSG:4490的,创建地图只需要指定投影为 EPSG:4490即可

js
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即可

js
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:
            '&copy; <a target="_blank" href="http://www.tianditu.cn">Tianditu</a>',
    }),
});

WARNING

注意maptalks体系内图层是有自己的坐标投影配置信息的

  • 地图的坐标系自己根据需要选取一个自己的,一般都是EPSG:3857或者EPSG:4326
  • 图层的坐标系可以和地图不同,比如上面的代码里地图的是EPSG:3857,但是Tilelayer是EPSG:4326
  • 当图层不设置投影坐标信息的时候会自动的去拿地图的投影信息作为自己的坐标投影配置
  • 所以代码层面最好为图层设置自己的坐标信息,尤其时当图层的投影信息和地图不同时,否则会导致一些未知错误,尤其是瓦片图层会导致瓦片加载错乱

TileLayer怎样显示特定的区域

所有的图层都有个setMask方法的,用来设置图层的蒙版效果

js
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时会失效

地图放大后TileLayer加载不到数据一片白怎么办?

TileLayer有个参数叫maxAvailableZoom,用来设置图层的可见最大层级,当设置了改参数后,地图的层级超过这个值时会复用你设置的这个参数层级的数据,比如你设置了18,当地图放大道19时仍然会复用18层级的数据

js
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渲染,所以这个效果会失效. 你可以自定义每个瓦片的处理逻辑来达到同样的效果

js
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里处理

js
//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:
        '&copy; <a href="http://osm.org">OpenStreetMap</a> contributors, &copy; <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,请注意兼容性

TileLayer怎样强制刷新?

由于业务的需要TileLayer的数据可能是动态,业务里期望隔一段时间就去更新TileLayer的内容,尤其是其子类WMSTileLayer我们会用其去加载WMS图层,WMS的数据是不断更新的,这时就需要去强制刷新WMSTileLayer

js
tilelayer.forceReload();
wmsLayer.forceReload();

WARNING

这个方法会清空图层里缓存的所有图片资源,从新加载的,所以要慎用

瓦片加载时自定义headers

TileLayer默认是用Image标签加载

js
const image = new Image();
image.src = url;

Image请求是没法子自定义header的,所以需要开启fetch请求

js
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上下文

js
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"],
    }),
]);

Add GroupTileLayer

WARNING

GroupTileLayer里的所有的TileLayer他们的坐标投影应该一致,不可将不同投影坐标的TileLayer放到一起

获取瓦片的边界范围Extent

TileLayer默认没有提供对应的方法,但是我们可以自己手动添加下对应的方法的,这里我们在TileLayer 的原型下挂载了两个方法

js
//获取投影的范围
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的,因为少了一个坐标转换的步骤
js
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可以用来对瓦片进行偏移

js
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

js
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的整体海拔

js
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:
        '&copy; <a href="http://osm.org">OpenStreetMap</a> contributors, &copy; <a href="https://carto.com/">CARTO</a>',
});

一般设置为负的值

注意把TileLayer的海拔高度设置为负的目的是不要抬高海平面,方便其他的业务图层数据加到地图,否则会要求其他图层也要设置海拔数据,导致业务逻辑变复杂了,这个样子最简单和不容易出错

加载天地图的服务

目前天地图服务提供了两种投影坐标服务

  • EPSG3857 墨卡托
  • EPSG4326 经纬度

详情参阅天地图的文档,请仔细阅读相关服务文档和地址, 已经有好多同学在这里犯错误了,体现在: 明明代码里写的4326投影,图层地址却偏偏指向墨卡托的服务地址

WARNING

太多同学犯这个错误了,所以请仔细点
太多同学犯这个错误了,所以请仔细点
太多同学犯这个错误了,所以请仔细点

加载4326的服务

js
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服务

js
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);

怎样加载自定义切图参数的瓦片

有时我们加载的瓦片服务,其不是按照标准的全球的这个切图参数切图的

json
[
    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 ,调试窗口执行

js
JSON.stringify(map.getSpatialReference()._resolutions);

来查看标准的标准的4326全球切图参数

不是按照标准的全球的这个切图参数切图的怎么加载呢?

宗旨

地图的投影要尽可能的使用全球的标准的切图参数,图层的切图参数要通过一定的方法使其接近全球的切图参数,不要随随便便的改变地图 的 spatialReference,因为地图处于标准的全球切片参数下是我们使用地图最佳的方式

  • 地图为核心,采用标准的全球的切图参数
  • 图层应该尽可能的想地图的切图参数靠拢,而不是地图随着图层走

切图的第一个层级和全球的切图参数接近

Markdown 官方教程 从图片看其是个4326的切图,且第一个切图层级的比例尺和全球的第一个层级切图比列尺(591657527.591555 )非常接近

这时我们只需要自定义TileLayerspatialReference参数

瓦片和地图都有自己的 spatialReference参数,只需要按照瓦片的切图参数来构造 TileLayer的 spatialReference即可

js
//切图第一个参数分辨率
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图层数据做转换的

切图的第一个层级和全球的切图参数差距比较大

这种情况在我们做项目时经常遇到,比如服务是一个县市这种,服务的第一个切图参数和标准的全球切图层数差的不是一点半点

js
//切图第一个参数分辨率
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从而导致矢量切片 等图层不能被正常加载

当然针对这个问题,还有更好的解决方法,我们称之为切图的补位法:

切图的补位法

  • 地图的投影还是尽可能的使用全球的的切图参数,因为这样是我们使用地图最舒服的方式,也方便加载其他的图层服务,比如矢量切片,倾斜图层
  • 瓦片的投影参数通过补位法尽可能的使其和地图一样,这样就可以做到地图投影信息不变了,且可以加载这个瓦片服务
js
//切图参数
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最接近的比例尺为层级的91155581.108577, 故而推断出 前面缺少了0-8这九个层级,看到这里相信你应该知道怎么来判断一个非标准的切图:用自己的切图的第一个比例尺, 从全球的切图参数里去找和你服务第一个切图参数最接近的比例尺,从而确定你的切图的第一个层级在全球切图参数里处于第几个层级,然后把前边缺省的补起来
  • 瓦片的请求函数需要重写,因为构造了补位,导致瓦片的请求层级需要减去补位的层级数,上面的例子是补位了9个层级,那么就减去9,其他情况以此类推

加载GeoServer发布的瓦片服务

geoserver发布的瓦片服务一般有EPSG990913(EPSG3857)和EPSG4326

  • geoserver的 3857切片是标准的全球切图
  • geoserver的 4326切片不是标准的全球切图,其0级是全球的标准切图的1级

标准的4326全球切图参数

json
[
    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 ,调试窗口执行

js
JSON.stringify(map.getSpatialReference()._resolutions);

来查看标准的标准的4326全球切图参数

这个是geoserver默认的切图参数

js
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瓦片

js
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切片不是默认的全球切图参数所以我们要自定义切图参数

js
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发布的瓦片服务

supermapiserver发布的服务和我们平时的常见的arcgis服务还是有点区别的

  • 常见的墨卡托的瓦片和我们常见的互联网瓦片一样的,直接传 x,y,z等参数即可
  • 如果是4326服务的,那和我们平时的就完全不一样了,其要传递个scale参数,scale表示当前地图的缩放比例 而且这个scale这个值需要动态的计算

这个是 supermap iserver 发布的一个例子

从这个服务的界面我们可以看到supermap iserver 支持很多的操作服务的,我们一般常用的有:

  • zxyTileImage 就是我们平时用的互联网墨卡托服务

WARNING

zxyTileImage 服务自带投影转化功能的:

  • 不管你原始发布的服务都是是么坐标投影的,服务端输出的都是我们常见的墨卡托
  • 所以你的项目的主坐标系统是墨卡托的话最好直接使用这个服务,可以省去你大量的折腾坐标系的时间
  • tileImage 就是我们上面说的需要传递scale参数来获取瓦片的服务接口,一般都是4326的服务,当然也可以 是其他的投影获取瓦片的接口,只是我们发布的服务一般都是4326

EPSG3857的方式加载

上面我们也说了 zxyTileImage可以直接输出墨卡托,所以使用起来就方面了

js
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

js
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

下面直接贴代码

js
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:
            '&copy; <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
js
//切图参数
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,因为

js
//获取瓦片的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层级切图的
  • 他们为什么这么做我也不知道

解决方法:

手动构造下瓦片的切图的分辨率即可

js
//这时全球的标准的切图的第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,
};

完整代码:

js
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:
            '&copy; <a href="http://osm.org">OpenStreetMap</a> contributors, &copy; <a href="https://carto.com/">CARTO</a>',
    }),
});

常见问题

加载的瓦片模糊

可能的原因有:

  • 地图默认是开启无极缩放的(seamlessZoom),即地图的缩放层级可以到小数,比如10.4这样,这时加载的瓦片还是 10层级的瓦片,所以就会导致瓦片被缩放一定的倍数导致模糊
  • 浏览器缩放或者高分屏上,这时还用常规的256x256的瓦片就会模糊,假设浏览器缩放到200%,这时瓦片的显示大小就是 512x512了,但是瓦片的原始大小是256x256

解决方法:

  • 关闭地图的无极缩放
js
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:
            "&copy; <a href='http://osm.org'>OpenStreetMap</a> contributors, &copy; <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

one tile url test

其本质就是用的2倍的瓦片大小来当成一倍的瓦片来绘制

tilelayer hq demo

  • 使用矢量切片,因为矢量切片是在前端完成数据的绘制的,所以不存在图片放大模糊的问题, 不管是高分屏还是地图的无极缩放都不会影响瓦片的绘制效果

  • 加载更高级别的瓦片

比如当前地图层级为10,加载11级别的瓦片来填充,假设瓦片大小为256,将瓦片大小缩小一倍(实际上的图片还是256 但是绘制是128)

js
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

Markdown 官方教程

TileLayer 每个瓦片buffer了0.5个像素,这样做的目的是因为在高分屏上会有白色的缝隙,所以内部就buffer了0.5像素 但是当瓦片是透明的时候也带来了新的问题,因为瓦片是透明,相邻的瓦片因为buffer的原因导致了覆盖,从而导致透明 叠加透明,导致瓦片边界处颜色加深了

解决方法:TileLayer提供了bufferPixel选项,你可以设置每个瓦片的buffer像素,业务里根据自己的需要设置合适的值 即可

js
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 参数可以使瓦片的请求分散到不同的服务端主机上去,

js
new maptalks.TileLayer("base", {
    urlTemplate: "https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}.png",
    subdomains: ["a", "b", "c", "d"],
    attribution:
        "&copy; <a href='http://osm.org'>OpenStreetMap</a> contributors, &copy; <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就更能凸显其价值了

  • 总结:不要为了用而用,如果你真的遇到瓶颈才用也不迟,毕竟维护多个服务器高可用成本还是挺高的

This document is generated by mdpress