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>
我在 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>