【问题标题】:Is there a way in OpenLayers to render EPSG:3857 tiles on an EPSG:4326 map?OpenLayers 中有没有办法在 EPSG:4326 地图上渲染 EPSG:3857 瓦片?
【发布时间】:2021-08-31 20:38:24
【问题描述】:

我在EPSG:4326 投影中有一张地图,我想使用EPSG:3857 瓦片集,例如 Mapbox 中的瓦片集。但是,到目前为止,我还不能让它工作。

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

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

我知道有渲染到屏幕外画布和使用 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 瓦片集,但我想解决这个问题。是否可以在 4326 的地图上渲染 3857 的瓦片?

谢谢

【问题讨论】:

  • 证明可以将 EPSG:3857 矢量切片渲染为 EPSG:4326 地图codesandbox.io/s/drag-and-drop-custom-mvt-forked-2jr6n
  • 几年前我在我们的姊妹网站上写了一个关于 gis 的问题(其中一些答案)(链接在右上角的图标上)。简而言之:开放层和传单在 z 上有不同的约定,我不得不对服务器端做一个额外的补丁(但微不足道)。
  • @Mike 是的,将单个图块绘制到 VectorLayer 很容易。但我需要将它与 Tile 图层集成,以便 openlayers 管理瓷砖的加载。如果不是,那么我必须根据视图状态手动管理磁贴加载,所以我不妨像offscreen-canvasmapbox-layer 示例那样绘制到画布上。但我希望它可以在 OpenLayers 中以某种方式使用 VectorTile/ImageTile 图层。
  • @GiacomoCatenazzi 是这个问题吗?:gis.stackexchange.com/questions/180356/…
  • @Matthias:是的,所以你看到你需要在 openlayer 中更正 x,y,z (但它可能取决于后端)。所以:我能够做到(而且我更喜欢大规模),但似乎没有太多支持(或者只是从未测试过)

标签: openlayers openlayers-6 vector-tiles proj4js


【解决方案1】:

如果可以像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>

【讨论】:

  • 谢谢迈克,这是可行的。边缘和角落有少量重叠,所以看起来这会导致透明度问题。此外,如果多边形有描边(如默认样式),这将显示图块的轮廓。仍然有这些限制,它已经足够好了。再次感谢。
  • 当用户浏览地图时,瓷砖似乎会在内存中不断累积,对吗?源在缩放级别之间更改,这使功能消失了一点。是否可以通过某种磁贴管理策略使用一个来源?
  • 这样可以避免重新加载瓷砖。如果内存有问题,您可以在视图缩放级别更改时调用source.clear() 并重置loadedTiles 数组。为了避免边缘效应,您需要将每个图块放在一个单独的图层中,并指定图层范围(视图投影中的图块范围),这将是低效且难以管理的。
猜你喜欢
  • 2017-07-12
  • 2014-04-27
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2022-01-08
  • 2019-07-03
  • 2016-09-28
  • 2021-12-18
相关资源
最近更新 更多