OpenLayers 有没有办法在 EPSG:4326 地图上渲染 EPSG:3857 瓦片?

Is there a way in OpenLayers to render EPSG:3857 tiles on an EPSG:4326 map?

我在 EPSG:4326 投影中有一张地图,我想使用 EPSG:3857 地图集,例如来自 Mapbox 的地图集。但是,到目前为止我还无法让它发挥作用。

this discussion 中,ahocevar 解释说不支持或计划不支持或计划矢量切片的任意重投影,因为矢量裁剪会很复杂并且会引入来自弯曲裁剪路径的伪影。 3857 和 4326 彼此是正方形,但支持两者之间重新投影的代码会使库复杂化。

ahocevar 还提到了一种涉及来自隐藏地图的图像图块的解决方法。然而,这没有意义,因为在任何情况下,3857 个图块都不会与 4326 个图块网格对齐,因为再多的变换也无法改变图块边界所在的位置。

我知道有渲染到屏幕外 canvas 和使用 Mapbox GL 的示例,但这两个都不理想,因为它们在库中运行(例如,它不适用于 map.getFeaturesAtPixel).我想知道 openlayers 本身是否有办法做到这一点。

这是一次尝试:https://codesandbox.io/s/mvt-3857-to-4326-attempt-qsf07

相关代码如下:

const mapboxSource = new VectorTileSource({
        attributions: '© <a href="https://www.mapbox.com/map-feedback/">Mapbox</a> ' +
            '© <a href="https://www.openstreetmap.org/copyright">' +
            'OpenStreetMap contributors</a>',
        projection: 'EPSG:4326',
        tileUrlFunction: (tileCoord) => {
            // Use the tile coordinate as a pseudo URL for caching purposes
            return JSON.stringify(tileCoord);
        },
        tileLoadFunction: async (tile, urlToken) => {
            const tileCoord = JSON.parse(urlToken);
            console.log('tileCoord', tileCoord);
            const [z, x, y] = tileCoord;

            const tileUrl = url
                .replace('{z}', String(z))
                .replace('{x}', String(x))
                .replace('{y}', String(y))
                .replace('{a-d}', 'abcd'.substr(((x << z) + y) % 4, 1))
            ;

            try {
                const response = await fetch(tileUrl);
                if (!response.ok) throw new Error();
                const arrayBuffer = await response.arrayBuffer();

                // Transform the vector tile's arrayBuffer into features and add them to the tile.
                const {layers} = new VectorTile(new Protobuf(arrayBuffer));
                const geojsonFeatures = [];
                Object.keys(layers).forEach((layerName) => {
                    const layer = layers[layerName];
                    for (let i = 0, len = layer.length; i < len; i++) {
                        const geojson = layer.feature(i).toGeoJSON(x, y, z);
                        geojson.properties.layer = layerName;
                        geojsonFeatures.push(geojson);
                    }
                });

                const features = geojsonFormat.readFeatures({
                    type: 'FeatureCollection',
                    features: geojsonFeatures,
                });
                tile.setFeatures(features);
            } catch (e) {
                console.log(e);
                debugger;
                tile.setState(TileState.ERROR);
            }
        },

这只会正确加载 z:0。放大后,MVT 图层不再与 OSM 底图对齐。

我还尝试使用 toContext 直接绘制到 TileImages,但无法弄清楚从坐标到图块像素的映射,而且光栅重投影看起来非常模糊。所以我放弃了,只使用了Maptiler的4326 tileset,但我想解决这个问题。是否可以在 4326 的地图上渲染 3857 个图块?

谢谢

如果可以像 https://codesandbox.io/s/drag-and-drop-custom-mvt-forked-2jr6n 那样对一个图块执行此操作,则可以对视图范围内的所有图块执行此操作。为了在重新投影后保持样式化的矢量格式,它需要是一个矢量层(使用 MVT featureClass: Feature 选项来克服坐标到平铺像素的问题)因为平铺网格必须在缩放级别内具有相同的平铺大小,这将如果您尝试将一个投影中的网格用作另一个投影中的图块,则情况并非如此。

<!doctype html>
<html lang="en">
  <head>
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/openlayers/openlayers.github.io@master/en/v6.6.1/css/ol.css" type="text/css">
    <style>
      html, body, .map {
        margin: 0;
        padding: 0;
        width: 100%;
        height: 100%;
      }
    </style>
    <script src="https://cdn.jsdelivr.net/gh/openlayers/openlayers.github.io@master/en/v6.6.1/build/ol.js"></script>
  </head>
  <body>
    <div id="map" class="map"></div>
    <script type="text/javascript">

let map;
const format = new ol.format.MVT({ featureClass: ol.Feature });

const vectorTileSource = new ol.source.VectorTile({
  format: format,
  url:
    "https://basemaps.arcgis.com/v1/arcgis/rest/services/World_Basemap/VectorTileServer/tile/{z}/{y}/{x}.pbf"
});
const tileGrid = vectorTileSource.getTileGrid();

const vectorLayer = new ol.layer.Vector();
const vectorSources = [];

function loader(zoom) {
  const loadedTiles = [];
  return function (extent, resolution, projection) {
    const tileProjection = vectorTileSource.getProjection();
    const maxExtent = ol.proj.transformExtent(
      tileProjection.getExtent(),
      tileProjection,
      projection
    );
    const safeExtent = ol.extent.getIntersection(extent, maxExtent);
    const gridExtent = ol.proj.transformExtent(safeExtent, projection, tileProjection);
    tileGrid.forEachTileCoord(gridExtent, zoom, function (tileCoord) {
      const key = tileCoord.toString();
      if (loadedTiles.indexOf(key) < 0) {
        loadedTiles.push(key);
        fetch(vectorTileSource.getTileUrlFunction()(tileCoord))
          .then(function (response) {
            return response.arrayBuffer();
          })
          .then(function (result) {
            const features = format.readFeatures(result, {
              extent: tileGrid.getTileCoordExtent(tileCoord),
              featureProjection: tileProjection
            });
            features.forEach(function (feature) {
              feature.getGeometry().transform(tileProjection, projection);
            });
            vectorSources[zoom].addFeatures(features);
          })
          .catch(function () {
            loadedTiles.splice(loadedTiles.indexOf(key), 1);
          });
      }
    });
  };
}

tileGrid.getResolutions().forEach(function (resolutiom, zoom) {
  vectorSources.push(
    new ol.source.Vector({
      loader: loader(zoom),
      strategy: ol.loadingstrategy.bbox
    })
  );
});

map = new ol.Map({
  target: "map",
  view: new ol.View({
    center: [0, 0],
    zoom: 2,
    projection: "EPSG:4326"
  }),
  layers: [
    new ol.layer.Tile({
      source: new ol.source.OSM()
    }),
    vectorLayer
  ]
});

let zoom;
function setSource() {
  const newZoom = Math.round(map.getView().getZoom());
  if (newZoom !== zoom && newZoom < vectorSources.length) {
    zoom = newZoom;
    vectorLayer.setSource(vectorSources[zoom]);
  }
}

map.getView().on("change:resolution", setSource);

setSource();

    </script>
  </body>
</html>