使用 Mapbox GL JS 绘制半径为 miles/meters 的圆
Drawing a circle with the radius in miles/meters with Mapbox GL JS
我正在将地图从使用 mapbox.js 转换为 mapbox-gl.js ,并且无法绘制一个使用英里或米作为半径而不是像素的圆。这个特定的圆用于显示从中心点到任何方向的距离区域。
以前我可以使用以下内容,然后将其添加到图层组:
// 500 miles = 804672 meters
L.circle(L.latLng(41.0804, -85.1392), 804672, {
stroke: false,
fill: true,
fillOpacity: 0.6,
fillColor: "#5b94c6",
className: "circle_500"
});
我发现在 Mapbox GL 中唯一的 documentation 是这样的:
map.addSource("source_circle_500", {
"type": "geojson",
"data": {
"type": "FeatureCollection",
"features": [{
"type": "Feature",
"geometry": {
"type": "Point",
"coordinates": [-85.1392, 41.0804]
}
}]
}
});
map.addLayer({
"id": "circle500",
"type": "circle",
"source": "source_circle_500",
"layout": {
"visibility": "none"
},
"paint": {
"circle-radius": 804672,
"circle-color": "#5b94c6",
"circle-opacity": 0.6
}
});
但这会以像素为单位呈现圆圈,不随缩放比例缩放。目前有没有一种方法可以使用 Mapbox GL 来渲染一个基于距离和缩放比例的圆(或多个)图层?
我目前使用的是 Mapbox GL v0.19.0。
此功能未内置于 GL JS 中,但您可以使用 functions 来模拟它。
<!DOCTYPE html>
<html>
<head>
<meta charset='utf-8' />
<title></title>
<meta name='viewport' content='initial-scale=1,maximum-scale=1,user-scalable=no' />
<script src='https://api.tiles.mapbox.com/mapbox-gl-js/v0.19.0/mapbox-gl.js'></script>
<link href='https://api.tiles.mapbox.com/mapbox-gl-js/v0.19.0/mapbox-gl.css' rel='stylesheet' />
<style>
body {
margin: 0;
padding: 0;
}
#map {
position: absolute;
top: 0;
bottom: 0;
width: 100%;
}
</style>
</head>
<body>
<div id='map'></div>
<script>
mapboxgl.accessToken = 'pk.eyJ1IjoibHVjYXN3b2oiLCJhIjoiNWtUX3JhdyJ9.WtCTtw6n20XV2DwwJHkGqQ';
var map = new mapboxgl.Map({
container: 'map',
style: 'mapbox://styles/mapbox/streets-v8',
center: [-74.50, 40],
zoom: 9,
minZoom: 5,
maxZoom: 15
});
map.on('load', function() {
map.addSource("source_circle_500", {
"type": "geojson",
"data": {
"type": "FeatureCollection",
"features": [{
"type": "Feature",
"geometry": {
"type": "Point",
"coordinates": [-74.50, 40]
}
}]
}
});
map.addLayer({
"id": "circle500",
"type": "circle",
"source": "source_circle_500",
"paint": {
"circle-radius": {
stops: [
[5, 1],
[15, 1024]
],
base: 2
},
"circle-color": "red",
"circle-opacity": 0.6
}
});
});
</script>
</body>
</html>
重要提示:
- 确定特定真实世界测量的函数参数并不简单。它们随要素的经度/纬度而变化。
- 由于平铺数据的性质和我们为 WebGL 打包数据的方式,大于 1024 像素的圆无法正确呈现
详细说明 ,我想出了一种估计参数的方法,以便根据特定的度量大小绘制圆。
地图支持 0 到 20 之间的缩放级别。假设我们定义半径如下:
"circle-radius": {
stops: [
[0, 0],
[20, RADIUS]
],
base: 2
}
由于我们为最小缩放级别 (0) 和最大缩放级别 (20) 定义了一个值,因此地图将在所有缩放级别呈现圆。对于介于两者之间的所有缩放级别,它会产生(大约)RADIUS/2^(20-zoom)
的半径。因此,如果我们将 RADIUS
设置为与我们的度量值匹配的正确像素大小,我们将获得所有缩放级别的正确半径。
所以我们基本上是在将米转换为缩放级别 20 的像素大小的转换因子之后。当然,这个因子取决于纬度。如果我们在最大缩放级别 20 处测量赤道处水平线的长度并除以这条线跨越的像素数,我们得到一个系数 ~0.075m/px(每像素米)。应用 1 / cos(phi)
的墨卡托纬度比例因子,我们获得了任何纬度的正确米像素比:
const metersToPixelsAtMaxZoom = (meters, latitude) =>
meters / 0.075 / Math.cos(latitude * Math.PI / 180)
因此,将 RADIUS
设置为 metersToPixelsAtMaxZoom(radiusInMeters, latitude)
会得到一个大小正确的圆:
"circle-radius": {
stops: [
[0, 0],
[20, metersToPixelsAtMaxZoom(radiusInMeters, latitude)]
],
base: 2
}
我已经通过使用 GeoJSON 多边形为我的用例解决了这个问题。它不是严格意义上的圆,但通过增加多边形的边数,您可以得到非常接近的圆。
此方法的额外好处是它会自动根据地图正确更改其间距、大小、方位角等。
这是生成 GeoJSON 多边形的函数
var createGeoJSONCircle = function(center, radiusInKm, points) {
if(!points) points = 64;
var coords = {
latitude: center[1],
longitude: center[0]
};
var km = radiusInKm;
var ret = [];
var distanceX = km/(111.320*Math.cos(coords.latitude*Math.PI/180));
var distanceY = km/110.574;
var theta, x, y;
for(var i=0; i<points; i++) {
theta = (i/points)*(2*Math.PI);
x = distanceX*Math.cos(theta);
y = distanceY*Math.sin(theta);
ret.push([coords.longitude+x, coords.latitude+y]);
}
ret.push(ret[0]);
return {
"type": "geojson",
"data": {
"type": "FeatureCollection",
"features": [{
"type": "Feature",
"geometry": {
"type": "Polygon",
"coordinates": [ret]
}
}]
}
};
};
你可以这样使用它:
map.addSource("polygon", createGeoJSONCircle([-93.6248586, 41.58527859], 0.5));
map.addLayer({
"id": "polygon",
"type": "fill",
"source": "polygon",
"layout": {},
"paint": {
"fill-color": "blue",
"fill-opacity": 0.6
}
});
如果你以后需要更新你创建的圈子你可以这样做(注意需要抓取 data
属性 传递给 setData):
map.getSource('polygon').setData(createGeoJSONCircle([-93.6248586, 41.58527859], 1).data);
输出如下所示:
Lucas 和 fphilipe 的回答非常完美!对于那些使用 react-native-mapbox 并绘制 over 地图的人,您必须考虑屏幕的像素密度,如下所示:
pixelValue(latitude: number, meters: number, zoomLevel: number) {
const mapPixels = meters / (78271.484 / 2 ** zoomLevel) / Math.cos((latitude * Math.PI) / 180);
const screenPixel = mapPixels * Math.floor(PixelRatio.get());
return screenPixel;
}
虽然所有答案都很复杂,但这里是最简单的答案
var center = [84.82512804700335, 26.241818082937552];
var radius = 5;
var options = {steps: 50, units: 'kilometers', properties: {foo: 'bar'}};
var circle = turf.circle(center, radius, options);
DEMO LINK
结果
使用@turf/turf
的简单方法
import * as turf from "@turf/turf";
import mapboxgl from "mapbox-gl";
map.on('load', function(){
let _center = turf.point([longitude, latitude]);
let _radius = 25;
let _options = {
steps: 80,
units: 'kilometers' // or "mile"
};
let _circle = turf.circle(_center, _radius, _options);
map.addSource("circleData", {
type: "geojson",
data: _circle,
});
map.addLayer({
id: "circle-fill",
type: "fill",
source: "circleData",
paint: {
"fill-color": "yellow",
"fill-opacity": 0.2,
},
});
});
Important note
在这种情况下使用 mapboxgl v1 如果你使用 mapboxgl v2 你会得到一个错误
**Uncaught ReferenceError: _createClass is not defined**
要解决这个错误,必须使用下面的方法
https://github.com/mapbox/mapbox-gl-js-docs/blob/6d91ce00e7e1b2495872dac969e497366befb7d7/docs/pages/api/index.md#transpiling-v2
我找到了这个 MapboxCircle 模块
只需要导入脚本
<script src='https://npmcdn.com/mapbox-gl-circle/dist/mapbox-gl-circle.min.js'></script>
并打印你的圈子
var myCircle = new MapboxCircle({lat: 39.984, lng: -75.343}, 25000, {
editable: true,
minRadius: 1500,
fillColor: '#29AB87'
}).addTo(myMapboxGlMap);
感谢@Brad Dwyer,这是他解决方案的 Ruby 版本:
def createGeoJSONCircle(coordinates, radius = 2, points = 64)
coordinate = {
longitude: coordinates.first[0].to_f,
latitude: coordinates.first[1].to_f,
}
ret = []
# radius is set in kilometers
distanceX = radius / (111.320 * Math.cos(coordinate[:latitude] * Math::PI / 180))
distanceY = radius / 110.574
for i in 0..points
theta = (i.to_f / points.to_f) * (2 * Math::PI)
x = distanceX * Math.cos(theta)
y = distanceY * Math.sin(theta)
ret << [(coordinate[:longitude] + x).to_s, (coordinate[:latitude] + y).to_s]
end
ret << ret.first
ret
end
扩展@fphilipe 的回答并跟进评论:-
Mapbox 使用正确的表达方式是
'circle-radius': [
'interpolate',
['exponential', 2],
['zoom'],
0, 0,
20, [
'/',
['/', meters, 0.075],
['cos', ['*', ['get', 'lat'], ['/', Math.PI, 180]]],
],
],
这假设您的特征属性包含纬度作为名为“lat”的标签。您只需要替换 meters
变量。
另外:为了提高精度,建议在停止中包含缩放级别,我尝试了以下代码,但由于某种原因它没有用。没有出现错误,但圆半径不准确。
'circle-radius': [
'interpolate',
['exponential', 2],
['zoom'],
0, 0,
20, [
'/',
['/', meters, ['/', 78271.484, ['^', 2, ['zoom']]]],
['cos', ['*', ['get', 'lat'], ['/', Math.PI, 180]]],
],
]
如果有人解决了这个问题,请发表评论(无需使用视口信息和状态管理动态传递缩放级别)。抱歉没有将此作为后续评论发布。谢谢!
我正在将地图从使用 mapbox.js 转换为 mapbox-gl.js ,并且无法绘制一个使用英里或米作为半径而不是像素的圆。这个特定的圆用于显示从中心点到任何方向的距离区域。
以前我可以使用以下内容,然后将其添加到图层组:
// 500 miles = 804672 meters
L.circle(L.latLng(41.0804, -85.1392), 804672, {
stroke: false,
fill: true,
fillOpacity: 0.6,
fillColor: "#5b94c6",
className: "circle_500"
});
我发现在 Mapbox GL 中唯一的 documentation 是这样的:
map.addSource("source_circle_500", {
"type": "geojson",
"data": {
"type": "FeatureCollection",
"features": [{
"type": "Feature",
"geometry": {
"type": "Point",
"coordinates": [-85.1392, 41.0804]
}
}]
}
});
map.addLayer({
"id": "circle500",
"type": "circle",
"source": "source_circle_500",
"layout": {
"visibility": "none"
},
"paint": {
"circle-radius": 804672,
"circle-color": "#5b94c6",
"circle-opacity": 0.6
}
});
但这会以像素为单位呈现圆圈,不随缩放比例缩放。目前有没有一种方法可以使用 Mapbox GL 来渲染一个基于距离和缩放比例的圆(或多个)图层?
我目前使用的是 Mapbox GL v0.19.0。
此功能未内置于 GL JS 中,但您可以使用 functions 来模拟它。
<!DOCTYPE html>
<html>
<head>
<meta charset='utf-8' />
<title></title>
<meta name='viewport' content='initial-scale=1,maximum-scale=1,user-scalable=no' />
<script src='https://api.tiles.mapbox.com/mapbox-gl-js/v0.19.0/mapbox-gl.js'></script>
<link href='https://api.tiles.mapbox.com/mapbox-gl-js/v0.19.0/mapbox-gl.css' rel='stylesheet' />
<style>
body {
margin: 0;
padding: 0;
}
#map {
position: absolute;
top: 0;
bottom: 0;
width: 100%;
}
</style>
</head>
<body>
<div id='map'></div>
<script>
mapboxgl.accessToken = 'pk.eyJ1IjoibHVjYXN3b2oiLCJhIjoiNWtUX3JhdyJ9.WtCTtw6n20XV2DwwJHkGqQ';
var map = new mapboxgl.Map({
container: 'map',
style: 'mapbox://styles/mapbox/streets-v8',
center: [-74.50, 40],
zoom: 9,
minZoom: 5,
maxZoom: 15
});
map.on('load', function() {
map.addSource("source_circle_500", {
"type": "geojson",
"data": {
"type": "FeatureCollection",
"features": [{
"type": "Feature",
"geometry": {
"type": "Point",
"coordinates": [-74.50, 40]
}
}]
}
});
map.addLayer({
"id": "circle500",
"type": "circle",
"source": "source_circle_500",
"paint": {
"circle-radius": {
stops: [
[5, 1],
[15, 1024]
],
base: 2
},
"circle-color": "red",
"circle-opacity": 0.6
}
});
});
</script>
</body>
</html>
重要提示:
- 确定特定真实世界测量的函数参数并不简单。它们随要素的经度/纬度而变化。
- 由于平铺数据的性质和我们为 WebGL 打包数据的方式,大于 1024 像素的圆无法正确呈现
详细说明
地图支持 0 到 20 之间的缩放级别。假设我们定义半径如下:
"circle-radius": {
stops: [
[0, 0],
[20, RADIUS]
],
base: 2
}
由于我们为最小缩放级别 (0) 和最大缩放级别 (20) 定义了一个值,因此地图将在所有缩放级别呈现圆。对于介于两者之间的所有缩放级别,它会产生(大约)RADIUS/2^(20-zoom)
的半径。因此,如果我们将 RADIUS
设置为与我们的度量值匹配的正确像素大小,我们将获得所有缩放级别的正确半径。
所以我们基本上是在将米转换为缩放级别 20 的像素大小的转换因子之后。当然,这个因子取决于纬度。如果我们在最大缩放级别 20 处测量赤道处水平线的长度并除以这条线跨越的像素数,我们得到一个系数 ~0.075m/px(每像素米)。应用 1 / cos(phi)
的墨卡托纬度比例因子,我们获得了任何纬度的正确米像素比:
const metersToPixelsAtMaxZoom = (meters, latitude) =>
meters / 0.075 / Math.cos(latitude * Math.PI / 180)
因此,将 RADIUS
设置为 metersToPixelsAtMaxZoom(radiusInMeters, latitude)
会得到一个大小正确的圆:
"circle-radius": {
stops: [
[0, 0],
[20, metersToPixelsAtMaxZoom(radiusInMeters, latitude)]
],
base: 2
}
我已经通过使用 GeoJSON 多边形为我的用例解决了这个问题。它不是严格意义上的圆,但通过增加多边形的边数,您可以得到非常接近的圆。
此方法的额外好处是它会自动根据地图正确更改其间距、大小、方位角等。
这是生成 GeoJSON 多边形的函数
var createGeoJSONCircle = function(center, radiusInKm, points) {
if(!points) points = 64;
var coords = {
latitude: center[1],
longitude: center[0]
};
var km = radiusInKm;
var ret = [];
var distanceX = km/(111.320*Math.cos(coords.latitude*Math.PI/180));
var distanceY = km/110.574;
var theta, x, y;
for(var i=0; i<points; i++) {
theta = (i/points)*(2*Math.PI);
x = distanceX*Math.cos(theta);
y = distanceY*Math.sin(theta);
ret.push([coords.longitude+x, coords.latitude+y]);
}
ret.push(ret[0]);
return {
"type": "geojson",
"data": {
"type": "FeatureCollection",
"features": [{
"type": "Feature",
"geometry": {
"type": "Polygon",
"coordinates": [ret]
}
}]
}
};
};
你可以这样使用它:
map.addSource("polygon", createGeoJSONCircle([-93.6248586, 41.58527859], 0.5));
map.addLayer({
"id": "polygon",
"type": "fill",
"source": "polygon",
"layout": {},
"paint": {
"fill-color": "blue",
"fill-opacity": 0.6
}
});
如果你以后需要更新你创建的圈子你可以这样做(注意需要抓取 data
属性 传递给 setData):
map.getSource('polygon').setData(createGeoJSONCircle([-93.6248586, 41.58527859], 1).data);
输出如下所示:
Lucas 和 fphilipe 的回答非常完美!对于那些使用 react-native-mapbox 并绘制 over 地图的人,您必须考虑屏幕的像素密度,如下所示:
pixelValue(latitude: number, meters: number, zoomLevel: number) {
const mapPixels = meters / (78271.484 / 2 ** zoomLevel) / Math.cos((latitude * Math.PI) / 180);
const screenPixel = mapPixels * Math.floor(PixelRatio.get());
return screenPixel;
}
虽然所有答案都很复杂,但这里是最简单的答案
var center = [84.82512804700335, 26.241818082937552];
var radius = 5;
var options = {steps: 50, units: 'kilometers', properties: {foo: 'bar'}};
var circle = turf.circle(center, radius, options);
DEMO LINK
结果
使用@turf/turf
的简单方法import * as turf from "@turf/turf";
import mapboxgl from "mapbox-gl";
map.on('load', function(){
let _center = turf.point([longitude, latitude]);
let _radius = 25;
let _options = {
steps: 80,
units: 'kilometers' // or "mile"
};
let _circle = turf.circle(_center, _radius, _options);
map.addSource("circleData", {
type: "geojson",
data: _circle,
});
map.addLayer({
id: "circle-fill",
type: "fill",
source: "circleData",
paint: {
"fill-color": "yellow",
"fill-opacity": 0.2,
},
});
});
Important note
在这种情况下使用 mapboxgl v1 如果你使用 mapboxgl v2 你会得到一个错误
**Uncaught ReferenceError: _createClass is not defined**
要解决这个错误,必须使用下面的方法 https://github.com/mapbox/mapbox-gl-js-docs/blob/6d91ce00e7e1b2495872dac969e497366befb7d7/docs/pages/api/index.md#transpiling-v2
我找到了这个 MapboxCircle 模块
只需要导入脚本
<script src='https://npmcdn.com/mapbox-gl-circle/dist/mapbox-gl-circle.min.js'></script>
并打印你的圈子
var myCircle = new MapboxCircle({lat: 39.984, lng: -75.343}, 25000, {
editable: true,
minRadius: 1500,
fillColor: '#29AB87'
}).addTo(myMapboxGlMap);
感谢@Brad Dwyer,这是他解决方案的 Ruby 版本:
def createGeoJSONCircle(coordinates, radius = 2, points = 64)
coordinate = {
longitude: coordinates.first[0].to_f,
latitude: coordinates.first[1].to_f,
}
ret = []
# radius is set in kilometers
distanceX = radius / (111.320 * Math.cos(coordinate[:latitude] * Math::PI / 180))
distanceY = radius / 110.574
for i in 0..points
theta = (i.to_f / points.to_f) * (2 * Math::PI)
x = distanceX * Math.cos(theta)
y = distanceY * Math.sin(theta)
ret << [(coordinate[:longitude] + x).to_s, (coordinate[:latitude] + y).to_s]
end
ret << ret.first
ret
end
扩展@fphilipe 的回答并跟进评论:-
Mapbox 使用正确的表达方式是
'circle-radius': [
'interpolate',
['exponential', 2],
['zoom'],
0, 0,
20, [
'/',
['/', meters, 0.075],
['cos', ['*', ['get', 'lat'], ['/', Math.PI, 180]]],
],
],
这假设您的特征属性包含纬度作为名为“lat”的标签。您只需要替换 meters
变量。
另外:为了提高精度,建议在停止中包含缩放级别,我尝试了以下代码,但由于某种原因它没有用。没有出现错误,但圆半径不准确。
'circle-radius': [
'interpolate',
['exponential', 2],
['zoom'],
0, 0,
20, [
'/',
['/', meters, ['/', 78271.484, ['^', 2, ['zoom']]]],
['cos', ['*', ['get', 'lat'], ['/', Math.PI, 180]]],
],
]
如果有人解决了这个问题,请发表评论(无需使用视口信息和状态管理动态传递缩放级别)。抱歉没有将此作为后续评论发布。谢谢!