Mapbox GL JS - fitBounds,带有北对齐、零螺距地图的方位角和/或螺距集边界。如何设置旋转地图的边界?

问题描述 投票:0回答:1

我有一个 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 时,一切都会正常工作: pitch and bearing both 0

方位角 -60 并使用类 getRoute() 方法: pitch zero, bearing -60, using class method

它不仅不适合路线,而且缩放级别比方位=0还要低。

轴承 -60 并使用测试 fitRotatedRoute() 函数: pitch zero, bearing -60, using test method

缩放级别稍好一些,但仍有很长的路要走。

如果有人对如何正确执行此操作有任何见解,那么很高兴知道。 MapBox 文档似乎只处理方位/螺距零示例。

javascript mapbox mapbox-gl-js
1个回答
0
投票

这是最适合我的解决方案。由于投影失真(将球形切片视为 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
                });

            });
        }
    }

现在贴合度好多了: map with solution applied

如果使用音调,效果也很好: map with solution applied and pitch of 50

© www.soinside.com 2019 - 2024. All rights reserved.