我有一个 JS 类,可以将带有路线的地图添加到我的页面(mapbox-gl-js v3.8.0)。
路线加载后,我将使用
fitBounds
调整地图以用路线填充可用空间。
在我应用轴承和/或螺距之前,这一切都工作正常。似乎发生的情况是,地图被缩放以填充路线,就好像方位角和螺距都为零一样,然后应用方位角和螺距。这会导致路线太小或太大(取决于方向等)。
我尝试过制作由方位角转换的点数组,并从理论上应该起作用的矩形创建边界矩形,但它只会导致相同的结果,在应用之前,尺寸/缩放针对北对齐视图进行了优化旋转。
有人知道如何用我的旋转路线填充地图容器吗?
获取和拟合路线的相关类方法有:
setView = (bounds, duration = 0) => {
// bounds should be array of arrays in format [[min_lng, min_lat],[max_lng, max_lat]]
// duration is animation length in milliseconds
this.map.fitBounds(bounds, {
padding: {
top: this.map_settings.padding.top,
right: this.map_settings.padding.right,
bottom: this.map_settings.padding.bottom,
left: this.map_settings.padding.left,
},
pitch: this.map_settings.pitch,
bearing: this.map_settings.bearing,
duration: duration
});
}
drawRoute = async () => {
// build the gps points query string
const points = this.map_settings.waypoints.map((coord) => [coord.longitude, coord.latitude].join());
const gps_list = points.join(";");
const query = await fetch(
`https://api.mapbox.com/directions/v5/mapbox/${this.map_settings.route_type}/${gps_list}?steps=false&geometries=geojson&access_token=${mapboxgl.accessToken}`,
{ method: "GET" }
);
// return if api call not successful
if (!query.ok) {
console.warn("Map Block: Error determining route");
return
}
const json = await query.json();
const data = json.routes[0];
const route = data.geometry.coordinates;
const geojson = {
type: "Feature",
properties: {},
geometry: {
type: "LineString",
coordinates: route,
},
};
this.map.addLayer({
id: `route-${this.map_settings.uid}`,
type: "line",
source: {
type: "geojson",
data: geojson,
},
layout: {
"line-join": "round",
"line-cap": "round",
},
paint: {
"line-color": "#3887be",
"line-width": 5,
"line-opacity": 0.75,
},
});
// set map bounds to fit route
const bounds = new mapboxgl.LngLatBounds(route[0], route[0]);
for (const coord of route) {
bounds.extend(coord);
}
this.setView(bounds, 1000);
}
我已经从控制台尝试过这个,但没有成功,这是我试图让它工作的最后一次迭代:
fitRotatedRoute = (routeCoordinates, mapBearing) => {
// Step 1: Rotate the route coordinates by the negative of the map's bearing
const radians = (mapBearing * Math.PI) / 180; // Convert map bearing to radians
// Function to rotate a point by the given angle
const rotatePoint = ([lng, lat], center, radians) => {
const dx = lng - center.lng;
const dy = lat - center.lat;
return [
center.lng + dx * Math.cos(radians) - dy * Math.sin(radians),
center.lat + dx * Math.sin(radians) + dy * Math.cos(radians),
];
};
// Step 2: Find the centroid of the route (average of coordinates)
const centroid = routeCoordinates.reduce(
(acc, [lng, lat]) => ({
lng: acc.lng + lng / routeCoordinates.length,
lat: acc.lat + lat / routeCoordinates.length,
}),
{ lng: 0, lat: 0 }
);
// Step 3: Rotate each coordinate by the negative of the map's bearing
const rotatedPoints = routeCoordinates.map((coord) =>
rotatePoint(coord, centroid, -radians)
);
// Step 4: Calculate the axis-aligned bounding box (AABB) of the rotated coordinates
const minLng = Math.min(...rotatedPoints.map(([lng]) => lng));
const maxLng = Math.max(...rotatedPoints.map(([lng]) => lng));
const minLat = Math.min(...rotatedPoints.map(([_, lat]) => lat));
const maxLat = Math.max(...rotatedPoints.map(([_, lat]) => lat));
// Step 5: Fit the bounds on the map using the calculated AABB
testMap.fitBounds(
[
[minLng, minLat], // Southwest corner
[maxLng, maxLat], // Northeast corner
],
{
padding: {
top: mapSettings.padding.top,
right: mapSettings.padding.right,
bottom: mapSettings.padding.bottom,
left: mapSettings.padding.left,
},
pitch: mapSettings.pitch,
bearing: mapBearing, // Apply map bearing (rotation)
duration: 1000, // Animation duration
}
);
}
当
pitch
和 bearing
都为 0 时,一切都会正常工作:
它不仅不适合路线,而且缩放级别比方位=0还要低。
轴承 -60 并使用测试 fitRotatedRoute() 函数:
缩放级别稍好一些,但仍有很长的路要走。
如果有人对如何正确执行此操作有任何见解,那么很高兴知道。 MapBox 文档似乎只处理方位/螺距零示例。
这是最适合我的解决方案。由于投影失真(将球形切片视为 2D 对象),它仍然是一个近似值,但它满足了目的。
当地图北对齐时查找边界框的类方法是:
findBoundingBox = (points) => {
const { swX, neX, swY, neY } = points.reduce(
(acc, [x, y]) => ({
swX: Math.min(acc.swX, x),
neX: Math.max(acc.neX, x),
swY: Math.min(acc.swY, y),
neY: Math.max(acc.neY, y),
}),
{ swX: Infinity, neX: -Infinity, swY: Infinity, neY: -Infinity }
);
return new mapboxgl.LngLatBounds([[swX, swY], [neX, neY]])
}
第一步是将方位角转换为弧度。 接下来,找到点的质心,并通过方位角(以弧度给出)绕质心旋转。 然后计算旋转点的边界框。 最后,取消旋转边界框并获取结果的坐标。
findRotatedBoundingBox = (points, bearing) => {
// convert degrees to radians
const toRadians = (degrees) => (degrees * Math.PI) / 180;
// Rotate a point [lng, lat] around a given origin by an angle in radians
const rotatePoint = ([lng, lat], angle, origin) => {
const cosTheta = Math.cos(angle);
const sinTheta = Math.sin(angle);
const translatedLng = lng - origin[0];
const translatedLat = lat - origin[1];
const xRot = translatedLng * cosTheta - translatedLat * sinTheta;
const yRot = translatedLng * sinTheta + translatedLat * cosTheta;
return [xRot, yRot];
}
// Find centroid from an array of points
const findCentroid = (points) => {
return points.reduce(
([sumLng, sumLat], [lng, lat]) => [sumLng + lng, sumLat + lat],
[0, 0]
).map((sum) => sum / points.length);
}
const bearingRadians = toRadians(bearing);
const centroid = findCentroid(points);
// Rotate all points to the rotated coordinate space using the centroid
const rotatedPoints = points.map((point) => rotatePoint(point, bearingRadians, centroid));
// Find bounding box in rotated space
const rotatedBounds = this.findBoundingBox(rotatedPoints).toArray();
// Rotate the bounding box corners back to the original space
const bounds = rotatedBounds.map(
(corner) => rotatePoint(corner, -bearingRadians, [0, 0]) // Unrotate without centering
).map(
([lng, lat]) => [lng + centroid[0], lat + centroid[1]]
);
return new mapboxgl.LngLatBounds(bounds);
}
如果方位角是北,我不需要这样做,所以我将这两个方法包装在另一个类方法中:
getBounds = (points) => {
// get the bounding box for the waypoints
if (this.mapSettings.bearing === 0) {
return this.findBoundingBox(points);
} else {
return this.findRotatedBoundingBox(points, this.mapSettings.bearing);
}
}
我没有使用
fitBounds
,而是获取 cameraForBounds
对象并通过 easeTo
应用它。最佳缩放是根据边界框的像素大小作为容器的 log2 比例来计算的。
fitBoundsToContainer = (bounds, bearing, pitch) => {
// cancel any running animations
this.map.stop();
// get camera bounds given bounding box, pitch and bearing
const cameraBounds = this.map.cameraForBounds(bounds, {
padding: 0,
pitch: pitch,
bearing: bearing
});
// Get the map's container dimensions
const container = this.map.getContainer();
const containerWidth = container.offsetWidth - this.mapSettings.padding.left - this.mapSettings.padding.right;
const containerHeight = container.offsetHeight - this.mapSettings.padding.top - this.mapSettings.padding.bottom;
// Get bounding box dimensions in px
const sw = this.map.project(bounds.getSouthWest());
const ne = this.map.project(bounds.getNorthEast());
const bboxWidth = Math.abs(sw.x - ne.x);
const bboxHeight = Math.abs(sw.y - ne.y);
// calculate optimal zoom
const scaleWidth = containerWidth / bboxWidth;
const scaleHeight = containerHeight / bboxHeight;
const scale = Math.min(scaleWidth, scaleHeight);
const optimalZoom = this.map.getZoom() + Math.log2(scale)
// calculate offset in case padding uneven in either direction
let offset = null;
if (
(this.mapSettings.padding.left !== this.mapSettings.padding.right) ||
(this.mapSettings.padding.top !== this.mapSettings.padding.bottom)
) {
offset = [
this.mapSettings.padding.right - this.mapSettings.padding.left,
this.mapSettings.padding.bottom - this.mapSettings.padding.top
]
}
// pan map to camera bounds then apply any padding offset
this.map.easeTo({
...cameraBounds,
padding: 0,
zoom: optimalZoom,
duration: 1000
});
if (offset) {
this.map.once('moveend', () => {
this.map.panBy(offset, {
duration: 1000
});
});
}
}