admin管理员组文章数量:1122846
I have a JS class to add maps with routes to my page (mapbox-gl-js v3.8.0).
Once the route has loaded, I'm using fitBounds
to adjust the map to fill the available space with the route.
This all works fine until I apply a bearing and/or pitch. What seems to happen is that the map is zoomed to fill with the route as if bearing & pitch are both zero, then the bearing & pitch are applied. This has the effect of making the route either too small or too big (depending on orientation etc).
I've tried making an array of my points transformed by the bearing and creating a bounds rectangle from that which should in theory work, but it just results in the same end where the size/zoom is optimised for the north aligned view before applying the rotation.
Does anyone have any insight into how I can fill the map container with my rotated route?
The relevant class methods for getting and fitting the route are:
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(
`/${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);
}
I've tried this from the console with no luck, it was my last iteration of trying to get this to work:
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
}
);
}
With pitch
and bearing
both 0, all works as it should:
With bearing -60 & using the class getRoute() method:
It's not only not fitted the route, it's at an even lower zoom level than bearing=0.
With bearing -60 and using the test fitRotatedRoute() function:
Slightly better zoom level but still a long way off.
If anyone has any insight into how to do this properly, it'd be great to know. MapBox docs only seem to deal with bearing/pitch zero examples.
I have a JS class to add maps with routes to my page (mapbox-gl-js v3.8.0).
Once the route has loaded, I'm using fitBounds
to adjust the map to fill the available space with the route.
This all works fine until I apply a bearing and/or pitch. What seems to happen is that the map is zoomed to fill with the route as if bearing & pitch are both zero, then the bearing & pitch are applied. This has the effect of making the route either too small or too big (depending on orientation etc).
I've tried making an array of my points transformed by the bearing and creating a bounds rectangle from that which should in theory work, but it just results in the same end where the size/zoom is optimised for the north aligned view before applying the rotation.
Does anyone have any insight into how I can fill the map container with my rotated route?
The relevant class methods for getting and fitting the route are:
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);
}
I've tried this from the console with no luck, it was my last iteration of trying to get this to work:
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
}
);
}
With pitch
and bearing
both 0, all works as it should:
With bearing -60 & using the class getRoute() method:
It's not only not fitted the route, it's at an even lower zoom level than bearing=0.
With bearing -60 and using the test fitRotatedRoute() function:
Slightly better zoom level but still a long way off.
If anyone has any insight into how to do this properly, it'd be great to know. MapBox docs only seem to deal with bearing/pitch zero examples.
Share Improve this question asked Nov 22, 2024 at 2:52 Rich - enzedonlineRich - enzedonline 1,2238 silver badges12 bronze badges1 Answer
Reset to default 0This was the solution that worked best for me. It's still an approximation due to projection distortion (treats a spherical slice as a 2D object) but it's meets the purpose.
My class method for finding the bounding box when the map is north aligned is:
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]])
}
The first step is to convert the bearing to radians. Next, find the centroid of the points, and rotate by the bearing (given in radians) about the centroid. Then calculate the bounding box for the rotated points. Finally, un-rotate the bounding box and take the coordinates of the result.
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);
}
I don't need to do this if the bearing is North, so I wrap both methods in another class method:
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);
}
}
Rather than use fitBounds
, I get the cameraForBounds
object and apply that via easeTo
. The optimum zoom is calculated from the pixel size of the bounding box as a log2 proportion of the container.
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
});
});
}
}
Now the fit is a lot better:
Also works well if a pitch is used:
本文标签:
版权声明:本文标题:javascript - Mapbox GL JS - fitBounds with bearing andor pitch sets bounds for North aligned, zero pitch map. How to set bounds 内容由网友自发贡献,该文观点仅代表作者本人, 转载请联系作者并注明出处:http://www.betaflare.com/web/1736305957a1932777.html, 本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容,一经查实,本站将立刻删除。
发表评论