Xây dựng hệ thống bản đồ trong nhà và xử lý chuyển đổi tầng chuyên nghiệp cho Frontend

Trong các dự án hiện đại như quản lý đô thị thông minh, trung tâm thương mại hay bãi đỗ xe ngầm, nhu cầu hiển thị bản đồ không chỉ dừng lại ở tọa độ phẳng. Người dùng cần biết chính xác họ đang ở tầng mấy, vị trí cửa hàng nằm ở đâu trong không gian đa tầng. Đây chính là lúc chúng ta cần triển khai giải pháp bản đồ trong nhà (Indoor Mapping).

Các phương pháp triển khai bản đồ trong nhà

Tùy vào yêu cầu về hiệu năng và trải nghiệm người dùng, chúng ta có ba hướng tiếp cận chính:

Phương pháp Mô tả Ưu điểm Nhược điểm
Image Overlay Chèn ảnh sơ đồ mặt bằng lên tọa độ bản đồ. Triển khai cực nhanh, đơn giản. Không thể tương tác với từng phòng, vỡ nét khi phóng to.
Vector Drawing (GeoJSON) Dùng GeoJSON vẽ các Polygon đại diện cho phòng, hành lang. Dễ tương tác, dung lượng nhẹ, tùy biến style linh hoạt. Cần xử lý logic lọc dữ liệu theo tầng phức tạp hơn.
3D Model (BIM/glTF) Load mô hình 3D thực tế từ các file glTF hoặc OBJ. Trực quan, thẩm mỹ cao, xoay góc nhìn đa chiều. Chi phí phát triển cao, nặng máy, khó tối ưu trên mobile.

Đối với phần lớn các ứng dụng doanh nghiệp (Dashboard, App quản lý), phương pháp Vector Drawing là lựa chọn tối ưu nhất vì cân bằng được giữa hiệu năng và khả năng tương tác.

Xây dựng bộ quản lý tầng (Floor Manager)

Về cốt lõi, việc chuyển đổi tầng là quá trình lọc dữ liệu và điều khiển trạng thái hiển thị (Visibility) của các lớp (Layers). Thay vì xóa và vẽ lại liên tục, chúng ta nên sử dụng cơ chế lưu trữ đệm (Cache) để tăng tốc độ phản hồi.


class BuildingLevelController {
    constructor(mapInstance, rawIndoorData) {
        this.map = mapInstance;
        this.fullData = rawIndoorData;
        this.activeLevel = null;
        this.levelLayers = new Map(); // Lưu trữ các layer theo tầng
        this.availableLevels = this.parseLevels(rawIndoorData);
    }

    // Trích xuất danh sách các tầng từ dữ liệu GeoJSON
    parseLevels(data) {
        const levels = new Set();
        data.features.forEach(feat => {
            if (feat.properties.level) {
                levels.add(feat.properties.level);
            }
        });
        return Array.from(levels).sort();
    }

    // Khởi tạo hiển thị tầng mặc định
    setup(initialLevel = this.availableLevels[0]) {
        this.availableLevels.forEach(level => {
            this.levelLayers.set(level, []);
        });
        this.changeTo(initialLevel);
    }

    // Logic chuyển đổi tầng chính
    changeTo(levelCode) {
        if (!this.availableLevels.includes(levelCode)) return;

        // Ẩn tất cả các tầng đang hiển thị
        this.hideAll();

        // Kiểm tra nếu tầng này đã được vẽ chưa, nếu chưa thì render
        if (this.levelLayers.get(levelCode).length === 0) {
            this.drawLevel(levelCode);
        } else {
            this.showLevel(levelCode);
        }

        this.activeLevel = levelCode;
        console.log(`Đã chuyển sang tầng: ${levelCode}`);
    }

    drawLevel(levelCode) {
        const levelFeatures = this.fullData.features.filter(
            f => f.properties.level === levelCode
        );

        const layers = levelFeatures.map(feature => {
            const polygon = this.map.createPolygon({
                path: feature.geometry.coordinates,
                extData: feature.properties,
                style: {
                    fillColor: '#e0e0e0',
                    strokeColor: '#999',
                    fillOpacity: 0.7
                }
            });

            // Gắn sự kiện tương tác cho từng phòng
            polygon.on('click', () => this.handleRoomClick(feature.properties));
            return polygon;
        });

        this.levelLayers.set(levelCode, layers);
    }

    showLevel(levelCode) {
        const layers = this.levelLayers.get(levelCode);
        layers.forEach(l => l.setVisible(true));
    }

    hideAll() {
        this.levelLayers.forEach(layers => {
            layers.forEach(l => l.setVisible(false));
        });
    }

    handleRoomClick(props) {
        console.log(`Thông tin vị trí: ${props.name || 'Không xác định'}`);
    }
}

Thuật toán xác định vị trí trong phòng (Point-in-Polygon)

Trong bản đồ nhà, chúng ta thường cần biết một điểm tọa độ (như vị trí GPS hoặc điểm click) nằm trong căn phòng nào. Thuật toán phổ biến nhất là Ray Casting.


/**
 * Kiểm tra một tọa độ có nằm trong vùng đa giác hay không
 * @param {Array} point [lng, lat]
 * @param {Array} vs Mảng các tọa độ đỉnh của đa giác
 */
function checkInside(point, vs) {
    const x = point[0], y = point[1];
    let inside = false;
    
    for (let i = 0, j = vs.length - 1; i < vs.length; j = i++) {
        const xi = vs[i][0], yi = vs[i][1];
        const xj = vs[j][0], yj = vs[j][1];
        
        const intersect = ((yi > y) !== (yj > y))
            && (x < (xj - xi) * (y - yi) / (yj - yi) + xi);
        if (intersect) inside = !inside;
    }
    
    return inside;
}

// Ví dụ áp dụng khi người dùng click lên bản đồ
map.on('click', (e) => {
    const userCoord = [e.lnglat.getLng(), e.lnglat.getLat()];
    const currentFloorRooms = getAllRoomsFromLevel(controller.activeLevel);

    const match = currentFloorRooms.find(room => 
        checkInside(userCoord, room.geometry.coordinates[0])
    );

    if (match) {
        renderRoomDetail(match.properties);
    }
});

Tối ưu hóa hiệu năng và trải nghiệm

Khi làm việc với bản đồ đa tầng, có 3 vấn đề kỹ thuật cần lưu ý:

  1. Lazy Loading: Không nên render toàn bộ các tầng cùng một lúc. Chỉ khi người dùng chọn tầng nào, chúng ta mới khởi tạo các đối tượng đồ họa của tầng đó để tiết kiệm tài nguyên bộ nhớ (RAM).
  2. Z-Index Management: Đảm bảo các lớp bản đồ trong nhà có chỉ số Z-Index cao hơn lớp bản đồ nền (Base Map) để tránh hiện tượng nhấp nháy hoặc bị che lấp.
  3. Xử lý chồng lấn POI: Với các tòa nhà có mật độ cửa hàng dày đặc, hãy sử dụng tính năng "Collision Detection" hoặc đặt ngưỡng hiển thị (Zoom Level) để ẩn bớt các nhãn tên khi thu nhỏ bản đồ.

Dữ liệu và Hệ tọa độ

Một sai lầm phổ biến là sử dụng sai hệ tọa độ khiến bản đồ trong nhà bị lệch so với bản đồ vệ tinh bên ngoài. Luôn đảm bảo dữ liệu GeoJSON của bạn sử dụng cùng một hệ tọa độ với Map SDK (ví dụ: WGS84 cho Mapbox hoặc GCJ-02 cho các bản đồ tại khu vực Trung Quốc). Nếu dữ liệu từ file CAD/BIM của kiến trúc sư, bạn cần thực hiện bước hiệu chỉnh (Georeferencing) để khớp các điểm góc tòa nhà với thực tế.

Thẻ: indoor-mapping GeoJSON frontend-maps JavaScript GIS

Đăng vào ngày 13 tháng 8 lúc 08:40