Skip to content

Geometry Calculations

The SDK provides basic geometry calculation functionality, while also recommending the use of professional geometry calculation libraries (such as turf.js) to handle complex geometry operations.

Important Note: The navigation map SDK supports both 2D (flat projection) and 3D (spherical projection) modes. In navigation map applications, all calculations involving distance, bearing, and paths should use Great Circle calculations to ensure accuracy. All calculation methods provided in this document are based on spherical geometry.

Basic Calculations

Calculate Distance Between Two Points (Great Circle Distance)

In navigation map applications, the distance between two points should be calculated using Great Circle Distance, which is the shortest distance between two points on a sphere.

Important Notes:

  • Not straight-line distance: This does not calculate Euclidean straight-line distance (like a line through the Earth's interior), but rather the shortest path along the Earth's surface
  • Great circle path: Great circle distance is the length of the great circle arc between two points on a sphere, which is the shortest distance for actual flight paths
  • Independent of projection: Whether the map is in 2D (Mercator projection) or 3D (Globe View) mode, great circle distance calculations produce the same results, because calculations are directly based on WGS84 longitude/latitude coordinates and do not depend on map projection
javascript
/**
 * Calculate great circle distance between two points (Haversine formula)
 * @param {Array<number>} point1 - [longitude, latitude]
 * @param {Array<number>} point2 - [longitude, latitude]
 * @param {string} unit - Unit: "km" (kilometers) or "nm" (nautical miles), default "km"
 * @returns {number} Distance (in specified unit)
 */
function calculateDistance(point1, point2, unit = "km") {
  const [lng1, lat1] = point1;
  const [lng2, lat2] = point2;
  
  // WGS84 ellipsoid mean radius (kilometers)
  // Note: For high-precision applications, more precise ellipsoid radius can be used
  const R_KM = 6371.0088; // Mean radius, approximately 6371 km
  const R_NM = R_KM / 1.852; // Nautical miles = kilometers / 1.852
  
  const dLat = (lat2 - lat1) * Math.PI / 180;
  const dLng = (lng2 - lng1) * Math.PI / 180;
  
  // Haversine formula (based on sphere model)
  const a = Math.sin(dLat / 2) * Math.sin(dLat / 2) +
    Math.cos(lat1 * Math.PI / 180) * Math.cos(lat2 * Math.PI / 180) *
    Math.sin(dLng / 2) * Math.sin(dLng / 2);
  
  const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
  const distanceKm = R_KM * c;
  
  // Unit conversion: 1 km = 0.539957 nautical miles
  if (unit === "nm" || unit === "NM") {
    return distanceKm / 1.852; // Return nautical miles
  }
  
  return distanceKm; // Return kilometers
}

// Usage example
const distanceKm = calculateDistance(
  [116.3974, 39.9093], // Beijing
  [121.4737, 31.2304], // Shanghai
  "km"
);
console.log("Distance:", distanceKm.toFixed(2), "kilometers");

// Aviation chart applications: Use nautical miles (NM) as standard unit
const distanceNm = calculateDistance(
  [116.3974, 39.9093],
  [121.4737, 31.2304],
  "nm"
);
console.log("Distance:", distanceNm.toFixed(2), "nautical miles (NM)");

Calculate Bearing (Great Circle Bearing)

In navigation map applications, bearing should be calculated using Great Circle Bearing, which is the initial direction from the start point to the end point.

javascript
/**
 * Calculate great circle bearing between two points (initial bearing)
 * @param {Array<number>} point1 - Start point [longitude, latitude]
 * @param {Array<number>} point2 - End point [longitude, latitude]
 * @returns {number} True bearing (degrees, 0-360, 0° is north, increases clockwise)
 * 
 * Note: This function returns true bearing, relative to geographic north.
 * For magnetic bearing, magnetic variation correction based on current position is required.
 * Magnetic variation can be obtained from IGRF (International Geomagnetic Reference Field) model.
 */
function calculateBearing(point1, point2) {
  const [lng1, lat1] = point1;
  const [lng2, lat2] = point2;
  
  const dLng = (lng2 - lng1) * Math.PI / 180;
  const lat1Rad = lat1 * Math.PI / 180;
  const lat2Rad = lat2 * Math.PI / 180;
  
  // Great circle bearing formula
  const y = Math.sin(dLng) * Math.cos(lat2Rad);
  const x = Math.cos(lat1Rad) * Math.sin(lat2Rad) -
    Math.sin(lat1Rad) * Math.cos(lat2Rad) * Math.cos(dLng);
  
  const bearing = Math.atan2(y, x) * 180 / Math.PI;
  return (bearing + 360) % 360; // Convert to 0-360 degrees
}

/**
 * Convert true bearing to magnetic bearing
 * @param {number} trueBearing - True bearing (degrees)
 * @param {number} magneticVariation - Magnetic variation (degrees, positive for east, negative for west)
 * @returns {number} Magnetic bearing (degrees)
 */
function trueToMagnetic(trueBearing, magneticVariation) {
  return (trueBearing - magneticVariation + 360) % 360;
}

// Note: Great circle bearing changes as position changes (except along equator or meridians)
// To calculate reverse bearing at end point, swap start and end points

// Usage example
const trueBearing = calculateBearing(
  [116.3974, 39.9093],
  [121.4737, 31.2304]
);
console.log("True bearing:", trueBearing.toFixed(2), "degrees");

// Example: Convert to magnetic bearing (assuming magnetic variation of -5.5°, i.e., west)
const magneticVariation = -5.5; // Actual value should be obtained from IGRF model
const magneticBearing = trueToMagnetic(trueBearing, magneticVariation);
console.log("Magnetic bearing:", magneticBearing.toFixed(2), "degrees");

Calculate Midpoint (Great Circle Midpoint)

On a sphere, simply averaging longitude and latitude does not give the true great circle midpoint. For navigation map applications, great circle midpoint calculation should be used.

javascript
/**
 * Calculate great circle midpoint between two points
 * Note: This is not a simple average of coordinates, but the midpoint of the great circle arc on the sphere
 * @param {Array<number>} point1 - [longitude, latitude]
 * @param {Array<number>} point2 - [longitude, latitude]
 * @returns {Array<number>|null} Midpoint coordinates [longitude, latitude], returns null if points are antipodal
 * 
 * Warning: If two points are exactly opposite on Earth (antipodal points),
 * there are infinite great circle paths, and the function may produce mathematically indeterminate values.
 */
function calculateMidpoint(point1, point2) {
  const [lng1, lat1] = point1;
  const [lng2, lat2] = point2;
  
  // Check if points are antipodal (approximately 180 degrees apart)
  const distance = calculateDistance(point1, point2, "km");
  if (distance > 20000) { // Approximately half the Earth's circumference
    console.warn("Warning: Points may be antipodal, great circle midpoint calculation may be inaccurate");
  }
  
  // Convert to radians
  const lat1Rad = lat1 * Math.PI / 180;
  const lng1Rad = lng1 * Math.PI / 180;
  const lat2Rad = lat2 * Math.PI / 180;
  const dLng = (lng2 - lng1) * Math.PI / 180;
  
  // Calculate great circle midpoint
  const Bx = Math.cos(lat2Rad) * Math.cos(dLng);
  const By = Math.cos(lat2Rad) * Math.sin(dLng);
  
  const midLat = Math.atan2(
    Math.sin(lat1Rad) + Math.sin(lat2Rad),
    Math.sqrt((Math.cos(lat1Rad) + Bx) ** 2 + By ** 2)
  );
  
  const midLng = lng1Rad + Math.atan2(By, Math.cos(lat1Rad) + Bx);
  
  return [
    midLng * 180 / Math.PI,
    midLat * 180 / Math.PI,
  ];
}

// Usage example
const midpoint = calculateMidpoint(
  [116.3974, 39.9093],
  [121.4737, 31.2304]
);
console.log("Great circle midpoint:", midpoint);

// Note: For short distances (<100 km), simple average has small error
// But for long distances or high-precision applications, great circle midpoint must be used

## Using Turf.js

It is recommended to use [Turf.js](https://turfjs.org/) for complex geometry calculations.

### Install Turf.js

```bash
npm install @turf/turf

Distance Calculation

javascript
import * as turf from "@turf/turf";

const point1 = turf.point([116.3974, 39.9093]);
const point2 = turf.point([121.4737, 31.2304]);

// Calculate distance (kilometers)
const distanceKm = turf.distance(point1, point2, { units: "kilometers" });
console.log("Distance:", distanceKm.toFixed(2), "kilometers");

// Aviation chart applications: Use nautical miles (NM) as standard unit
// Turf.js supports "nauticalmiles" unit
const distanceNm = turf.distance(point1, point2, { units: "nauticalmiles" });
console.log("Distance:", distanceNm.toFixed(2), "nautical miles (NM)");

Important Notes:

  • turf.distance behaves consistently in 2D and 3D modes: turf.distance calculates directly based on WGS84 longitude/latitude coordinates and does not depend on map projection mode (2D Mercator or 3D Globe View)
  • Calculation principle: Turf.js internally uses Haversine formula or similar algorithms, directly processing geographic coordinates, independent of how the map is displayed (projection)
  • Practical application: Whether the map is currently in 2D or 3D mode, turf.distance calculation results are the same, because it calculates actual geographic distance (great circle distance), not visual distance on screen

Area Calculation

javascript
import * as turf from "@turf/turf";

const polygon = turf.polygon([
  [
    [116.3974, 39.9093],
    [121.4737, 39.9093],
    [121.4737, 31.2304],
    [116.3974, 31.2304],
    [116.3974, 39.9093],
  ],
]);

// Calculate area (square meters)
const area = turf.area(polygon);
console.log("Area:", area, "square meters");
console.log("Area:", area / 1000000, "square kilometers");

Centroid Calculation

javascript
import * as turf from "@turf/turf";

const polygon = turf.polygon([...]);
const centroid = turf.centroid(polygon);
console.log("Centroid:", centroid.geometry.coordinates);

Bounding Box Calculation

javascript
import * as turf from "@turf/turf";

const points = turf.featureCollection([
  turf.point([116.3974, 39.9093]),
  turf.point([121.4737, 31.2304]),
  turf.point([113.2644, 23.1291]),
]);

const bbox = turf.bbox(points);
console.log("Bounding box:", bbox); // [minLng, minLat, maxLng, maxLat]

// Convert to GeoJSON bounding box
const bboxPolygon = turf.bboxPolygon(bbox);

Point in Polygon Check

javascript
import * as turf from "@turf/turf";

const point = turf.point([116.3974, 39.9093]);
const polygon = turf.polygon([...]);

const isInside = turf.booleanPointInPolygon(point, polygon);
console.log("Point is inside polygon:", isInside);

Buffer Calculation

javascript
import * as turf from "@turf/turf";

const point = turf.point([116.3974, 39.9093]);

// Create buffer (radius 10 kilometers)
const buffered = turf.buffer(point, 10, { units: "kilometers" });

// Add to map
sdk.addGeoJSON("buffer-source", buffered);
sdk.addLayer({
  id: "buffer-layer",
  source: "buffer-source",
  type: "fill",
  paint: {
    "fill-color": "#007bff",
    "fill-opacity": 0.3,
  },
});

Line Length Calculation

javascript
import * as turf from "@turf/turf";

const line = turf.lineString([
  [116.3974, 39.9093],
  [121.4737, 31.2304],
  [113.2644, 23.1291],
]);

// Calculate length (kilometers)
const lengthKm = turf.length(line, { units: "kilometers" });
console.log("Length:", lengthKm.toFixed(2), "kilometers");

// Aviation chart applications: Use nautical miles
const lengthNm = turf.length(line, { units: "nauticalmiles" });
console.log("Length:", lengthNm.toFixed(2), "nautical miles (NM)");

Simplify Geometry

javascript
import * as turf from "@turf/turf";

const line = turf.lineString([...]);

// Simplify line (reduce point count)
const simplified = turf.simplify(line, { tolerance: 0.01, highQuality: false });

⚠️ Important Warning: In aviation chart applications, simplifying boundaries of Restricted Areas, Controlled Airspace, or Flight Procedures is extremely dangerous and may lead to:

  • Visual intrusion judgment errors
  • Flight safety risks
  • Regulatory compliance issues

Recommendation: Use geometry simplification only in non-critical scenarios (such as visualization or performance optimization), and must be validated by business requirements.

Measurement Tools

Using MeasurePlugin

The SDK provides a built-in measurement plugin:

javascript
// Create measurement plugin
const measurePlugin = new navMap.MeasurePlugin();
sdk.use(measurePlugin);

// Enable distance measurement
measurePlugin.enable("distance");

// Enable area measurement
measurePlugin.enable("area");

// Disable measurement
measurePlugin.disable();

Custom Measurement Tool

javascript
class CustomMeasureTool {
  constructor(sdk) {
    this.sdk = sdk;
    this.points = [];
    this.setupEvents();
  }

  setupEvents() {
    this.sdk.on("click", (e) => {
      if (this.isActive) {
        this.addPoint(e.lngLat);
      }
    });
  }

  addPoint(lngLat) {
    this.points.push([lngLat.lng, lngLat.lat]);

    // Add point marker
    this.sdk.addPoint(
      [lngLat.lng, lngLat.lat],
      {},
      {
        paint: {
          "circle-radius": 6,
          "circle-color": "#ff0000",
        },
      }
    );

    // If multiple points, draw line
    if (this.points.length > 1) {
      this.sdk.addLine(
        this.points.slice(-2),
        {},
        {
          paint: {
            "line-color": "#ff0000",
            "line-width": 2,
          },
        }
      );
    }

    // Calculate distance
    if (this.points.length >= 2) {
      const distance = this.calculateTotalDistance();
      this.updateDistanceDisplay(distance);
    }
  }

  calculateTotalDistance() {
    let totalDistance = 0;
    for (let i = 1; i < this.points.length; i++) {
      totalDistance += calculateDistance(this.points[i - 1], this.points[i]);
    }
    return totalDistance;
  }

  updateDistanceDisplay(distance) {
    // Update UI to display distance
    document.getElementById("distance-display").textContent =
      `Total distance: ${distance.toFixed(2)} kilometers`;
  }

  clear() {
    this.points.forEach((point, index) => {
      this.sdk.removeLayer(`measure-point-${index}`);
      if (index > 0) {
        this.sdk.removeLayer(`measure-line-${index - 1}`);
      }
    });
    this.points = [];
  }

  enable() {
    this.isActive = true;
  }

  disable() {
    this.isActive = false;
    this.clear();
  }
}

// Usage
const measureTool = new CustomMeasureTool(sdk);
measureTool.enable();

Coordinate Conversion

Screen Coordinates to Geographic Coordinates

Note: The result of coordinate conversion depends on the current map projection mode:

  • 2D Mode (Flat Projection): Uses Mercator or other flat projections
  • 3D Mode (Spherical Projection): Uses spherical projection, coordinate conversion considers Earth's curvature
javascript
/**
 * Screen pixel coordinates to geographic coordinates
 * @param {Object} pixel - { x: number, y: number }
 * @returns {Object} { lng: number, lat: number }
 */
function pixelToLngLat(pixel) {
  return sdk.map.unproject(pixel);
}

// Usage example
const lngLat = pixelToLngLat({ x: 100, y: 200 });
console.log("Geographic coordinates:", lngLat);

// Check current projection mode
function getProjectionType() {
  const projection = sdk.map.getProjection();
  return projection && projection.type === "globe" ? "3D" : "2D";
}

Geographic Coordinates to Screen Coordinates

javascript
/**
 * Geographic coordinates to screen pixel coordinates
 * @param {Array<number>} lngLat - [longitude, latitude]
 * @returns {Object} { x: number, y: number }
 */
function lngLatToPixel(lngLat) {
  return sdk.map.project(lngLat);
}

// Usage example
const pixel = lngLatToPixel([116.3974, 39.9093]);
console.log("Screen coordinates:", pixel);

// Note: In 3D mode, projection results vary with viewing angle (pitch, bearing)
// In 2D mode, projection results are relatively stable

Complete Example

javascript
import * as turf from "@turf/turf";

let sdk;

async function initMap() {
  sdk = new navMap.MapSDK({
    container: "map",
    center: [116.39, 39.9],
    zoom: 10,
  });

  sdk.on("loadComplete", () => {
    setupGeometryTools();
  });
}

function setupGeometryTools() {
  // 1. Calculate distance between two points
  const distance = calculateDistance(
    [116.3974, 39.9093],
    [121.4737, 31.2304]
  );
  console.log("Beijing to Shanghai distance:", distance, "kilometers");

  // 2. Calculate area using Turf.js
  const polygon = turf.polygon([
    [
      [116.3974, 39.9093],
      [121.4737, 39.9093],
      [121.4737, 31.2304],
      [116.3974, 31.2304],
      [116.3974, 39.9093],
    ],
  ]);
  const area = turf.area(polygon) / 1000000; // Convert to square kilometers
  console.log("Polygon area:", area, "square kilometers");

  // 3. Create buffer
  const point = turf.point([116.3974, 39.9093]);
  const buffer = turf.buffer(point, 50, { units: "kilometers" });

  sdk.addGeoJSON("buffer-source", buffer);
  sdk.addLayer({
    id: "buffer-layer",
    source: "buffer-source",
    type: "fill",
    paint: {
      "fill-color": "#007bff",
      "fill-opacity": 0.3,
    },
  });

  // 4. Check if point is inside polygon
  const testPoint = turf.point([118.0, 35.0]);
  const isInside = turf.booleanPointInPolygon(testPoint, polygon);
  console.log("Point is inside polygon:", isInside);

  // 5. Use measurement plugin
  const measurePlugin = new navMap.MeasurePlugin();
  sdk.use(measurePlugin);
  measurePlugin.enable("distance");
}

initMap();

Differences Between 2D and 3D Modes

Projection Mode Impact

The SDK supports two projection modes:

  • 2D Mode (Flat Projection): Uses flat projection (e.g., Mercator), suitable for local area display
  • 3D Mode (Globe View): Uses spherical projection (Globe View), suitable for global display

Terminology Note: In MapLibre/Mapbox, "Globe View" refers to the spherical projection view mode, which is different from "3D view with terrain height" (Terrain/Pitch). The "3D mode" mentioned in this document specifically refers to Globe View.

Calculation Considerations

  1. Distance Calculation:

    • Regardless of mode, great circle distance should be used, as geographic coordinates are always based on a sphere
    • turf.distance and custom calculateDistance functions produce identical results in 2D and 3D modes
    • These functions calculate directly based on WGS84 longitude/latitude coordinates and do not depend on map projection
    • They calculate actual geographic distance (shortest path along Earth's surface), not visual distance on screen
  2. Bearing Calculation: Great circle bearing should be used, which is the standard for navigation map applications

  3. Midpoint Calculation: Great circle midpoint should be used; simple average is inaccurate on a sphere

  4. Coordinate Conversion: Screen-to-geographic coordinate conversion varies with projection mode

    • 2D mode (Mercator projection): Coordinate conversion based on flat projection
    • 3D mode (Globe View): Coordinate conversion based on spherical projection, considers viewing angle (pitch, bearing)
  5. Area Calculation: When calculating area on a sphere, Earth's curvature should be considered

    • turf.area produces the same results in 2D and 3D modes, because it calculates directly based on geographic coordinates

Mode Detection

javascript
// Detect if currently in 3D mode
function is3DMode() {
  const projection = sdk.map.getProjection();
  return projection && projection.type === "globe";
}

// Choose calculation method based on mode
function calculateDistanceAdaptive(point1, point2) {
  // Use great circle distance for both 2D and 3D modes
  // because geographic coordinates are always based on a sphere
  return calculateDistance(point1, point2);
}

Notes

Coordinate System and Unit Standards

  1. Coordinate System: Strictly follow WGS84 coordinate system (World Geodetic System 1984)
  2. Unit Standards:
    • Distance Unit: Recommend using Nautical Miles (NM), which is the legal unit in aviation chart applications
    • Area Unit: Recommend using Square Nautical Miles
    • Unit conversion: 1 km = 0.539957 NM, 1 NM = 1.852 km

Precision Algorithm Models

  1. Sphere vs Ellipsoid Models:
    • Haversine Formula (examples in this document): Based on sphere model, suitable for general EFB or H5 applications
    • Precision Limitations: Haversine can have errors up to 0.5% over long distances (e.g., transoceanic routes)
    • High-Precision Requirements: For calculations requiring extremely high precision (centimeter-level), recommend:
      • Integrating Vincenty Formula (based on WGS84 ellipsoid)
      • Using Turf.js built-in methods (Turf internally handles some ellipsoid differences)
      • For PBN (Performance Based Navigation) related precision calculations, do not rely solely on frontend Haversine functions; use server-side ARINC 424 data processing results

Bearing and Magnetic Variation

  1. True Bearing vs Magnetic Bearing:
    • This document calculates True Bearing, relative to geographic north
    • Pilots see Magnetic Bearing in the cockpit
    • Magnetic Variation Correction: Actual flight operations require magnetic variation correction based on IGRF (International Geomagnetic Reference Field) model
    • Magnetic variation formula: Magnetic Bearing = True Bearing - Magnetic Variation (positive for east, negative for west)

Great Circle Calculations

  1. Great Circle Calculations: All distance, bearing, and path calculations in navigation map applications should use great circle calculations

Projection Distortion

  1. Projection Distortion:
    • In 2D Mercator projection, visual distances at high latitudes appear longer (projection distortion)
    • But calculateDistance and turf.distance calculate actual geographic distance (great circle distance), unaffected by projection
    • Important: Whether the map is in 2D or 3D mode, distance calculation results are the same, because calculations are directly based on geographic coordinates and independent of projection

Earth Radius

  1. Earth Radius:
    • Mean radius: 6371.0088 km (suitable for most cases)
    • For high-precision applications, precise WGS84 ellipsoid radius can be used
    • Turf.js defaults to 6371008.8 meters (approximately 6371.0088 km)

Precision Considerations

  1. Precision Considerations:
    • Pay attention to precision issues in large-scale calculations
    • Haversine formula has high precision for short distances (<100 km)
    • For very long distances, consider using more precise Vincenty formula

Performance and Library Selection

  1. Performance Optimization: Consider using Web Worker for complex geometry calculations
  2. Library Selection: Recommend using professional libraries like Turf.js for complex geometry operations, which already implement correct great circle calculations

Projection Mode

  1. Projection Mode: Note differences in coordinate conversion between 2D and 3D (Globe View) modes, but geometry calculations themselves should always be based on a sphere