google_maps_utils 5.0.0 copy "google_maps_utils: ^5.0.0" to clipboard
google_maps_utils: ^5.0.0 copied to clipboard

SphericalUtils, MathUtils and PolyUtils ported from Google's android-maps-utils: distances, headings, bounds, polyline encode/decode and Douglas-Peucker simplification.

example/example.dart

// ============================================================================
// Google Maps Utils for Dart/Flutter — Example
// ============================================================================
//
// This example demonstrates all major features of the google_maps_utils package.
// Each section is self-contained so you can copy individual snippets into your
// own project.
//
// Coordinate convention used throughout:
//   Point.x = latitude  (north/south, range -90 to 90)
//   Point.y = longitude (east/west, range -180 to 180)
//
// Run this example with:
//   dart run example/example.dart
// ============================================================================

import 'dart:math';

import 'package:google_maps_utils/google_maps_utils.dart';

void main() {
  // --------------------------------------------------------------------------
  // 1. DISTANCE BETWEEN TWO POINTS
  // --------------------------------------------------------------------------
  // Computes the great-circle distance between two coordinates in meters.
  // Uses the Haversine formula internally for numerical stability.

  final paris = Point<double>(48.8566, 2.3522); // Paris, France
  final london = Point<double>(51.5074, -0.1278); // London, UK

  final distanceParisLondon =
      GoogleMapsUtils.spherical.computeDistanceBetween(paris, london);
  print('=== Distance ===');
  print('Paris → London: ${distanceParisLondon.toStringAsFixed(2)} meters');
  // Expected: ~341,549 meters (~341 km)
  print('');

  // --------------------------------------------------------------------------
  // 2. HEADING (BEARING)
  // --------------------------------------------------------------------------
  // Returns the initial bearing from one point to another.
  // Result is in degrees clockwise from North, range [-180, 180).

  final heading = GoogleMapsUtils.spherical.computeHeading(paris, london);
  print('=== Heading ===');
  print('Paris → London heading: ${heading.toStringAsFixed(4)}°');
  // A negative value means the heading is west of north.
  print('');

  // --------------------------------------------------------------------------
  // 3. COMPASS CARDINAL DIRECTION
  // --------------------------------------------------------------------------
  // Converts a heading angle to a 16-point compass abbreviation.
  // Useful for user-friendly direction display (e.g. "Head NW").

  final cardinal = GoogleMapsUtils.spherical.getCardinal(heading);
  print('=== Cardinal ===');
  print('Heading ${heading.toStringAsFixed(1)}° = $cardinal');

  // All 16 compass points:
  // N, NNE, NE, ENE, E, ESE, SE, SSE, S, SSW, SW, WSW, W, WNW, NW, NNW
  print('0°   = ${GoogleMapsUtils.spherical.getCardinal(0)}');
  print('90°  = ${GoogleMapsUtils.spherical.getCardinal(90)}');
  print('180° = ${GoogleMapsUtils.spherical.getCardinal(180)}');
  print('270° = ${GoogleMapsUtils.spherical.getCardinal(270)}');
  print('');

  // --------------------------------------------------------------------------
  // 4. ANGLE BETWEEN POINTS
  // --------------------------------------------------------------------------
  // Returns the arc distance on the unit sphere, in radians.
  // Multiply by earth's radius to get meters (same as computeDistanceBetween).

  final angle = GoogleMapsUtils.spherical.computeAngleBetween(paris, london);
  print('=== Angle ===');
  print('Paris → London angle: ${angle.toStringAsFixed(8)} radians');
  print(
      'Verify: angle × earthRadius = ${(angle * GoogleMapsUtils.math.earthRadius).toStringAsFixed(2)} meters');
  print('');

  // --------------------------------------------------------------------------
  // 5. COMPUTE OFFSET (MOVE A POINT)
  // --------------------------------------------------------------------------
  // Given a starting point, distance (meters), and heading (degrees),
  // returns the destination point.
  // Useful for: "Where do I end up if I walk 1km north?"

  final startPoint = Point<double>(-23.5505, -46.6333); // São Paulo, Brazil

  // Move 5000 meters (5km) due north (heading = 0°)
  final movedNorth =
      GoogleMapsUtils.spherical.computeOffset(startPoint, 5000.0, 0.0);
  print('=== Compute Offset ===');
  print('Start: (${startPoint.x}, ${startPoint.y})');
  print(
      'After 5km North: (${movedNorth.x.toStringAsFixed(6)}, ${movedNorth.y.toStringAsFixed(6)})');

  // Move 5000 meters east (heading = 90°)
  final movedEast =
      GoogleMapsUtils.spherical.computeOffset(startPoint, 5000.0, 90.0);
  print(
      'After 5km East:  (${movedEast.x.toStringAsFixed(6)}, ${movedEast.y.toStringAsFixed(6)})');
  print('');

  // --------------------------------------------------------------------------
  // 6. COMPUTE OFFSET ORIGIN (REVERSE LOOKUP)
  // --------------------------------------------------------------------------
  // Given a destination, distance, and heading — find where you started.
  // Returns null if no valid solution exists.

  final origin =
      GoogleMapsUtils.spherical.computeOffsetOrigin(movedNorth, 5000.0, 0.0);
  print('=== Compute Offset Origin ===');
  if (origin != null) {
    print(
        'Reverse of 5km North: (${origin.x.toStringAsFixed(6)}, ${origin.y.toStringAsFixed(6)})');
    print('Original start was:    (${startPoint.x}, ${startPoint.y})');
    // Should match closely!
  }
  print('');

  // --------------------------------------------------------------------------
  // 7. INTERPOLATION (SLERP)
  // --------------------------------------------------------------------------
  // Finds a point at a given fraction along the great-circle path between two
  // points. fraction=0 returns `from`, fraction=1 returns `to`.
  // Useful for animations or placing markers along a route.

  print('=== Interpolation ===');
  final midpoint = GoogleMapsUtils.spherical.interpolate(paris, london, 0.5);
  print(
      'Midpoint Paris→London: (${midpoint.x.toStringAsFixed(4)}, ${midpoint.y.toStringAsFixed(4)})');

  // You can generate multiple waypoints along the path:
  print('10 waypoints along Paris → London:');
  for (int i = 0; i <= 10; i++) {
    final fraction = i / 10.0;
    final wp = GoogleMapsUtils.spherical.interpolate(paris, london, fraction);
    print(
        '  ${(fraction * 100).toStringAsFixed(0)}%: (${wp.x.toStringAsFixed(4)}, ${wp.y.toStringAsFixed(4)})');
  }
  print('');

  // --------------------------------------------------------------------------
  // 8. PATH LENGTH
  // --------------------------------------------------------------------------
  // Computes the total distance along a path (list of points), in meters.
  // The path does not need to be closed.

  final route = <Point<double>>[
    Point(-23.5505, -46.6333), // São Paulo
    Point(-22.9068, -43.1729), // Rio de Janeiro
    Point(-19.9167, -43.9345), // Belo Horizonte
  ];

  final routeLength = GoogleMapsUtils.spherical.computeLength(route);
  print('=== Path Length ===');
  print('São Paulo → Rio → BH: ${(routeLength / 1000).toStringAsFixed(1)} km');
  print('');

  // --------------------------------------------------------------------------
  // 9. POLYLINE DECODING
  // --------------------------------------------------------------------------
  // Google's Encoded Polyline Algorithm compresses coordinates into a string.
  // This is commonly returned by the Directions API and OSRM.
  // decode() converts the string back into a list of points.
  //
  // Reference: https://developers.google.com/maps/documentation/utilities/polylinealgorithm

  final encodedPolyline =
      'wjiaFz`hgQs}GmmBok@}vX|cOzKvvT`uNutJz|UgqAglAjr@ijBz]opA|Vor@}ViqEokCaiGu|@byAkjAvrMgjDj_A??ey@abD';

  final decodedPath = GoogleMapsUtils.poly.decode(encodedPolyline);
  print('=== Polyline Decode ===');
  print('Encoded string length: ${encodedPolyline.length} chars');
  print('Decoded into ${decodedPath.length} points');
  print('First point: (${decodedPath.first.x}, ${decodedPath.first.y})');
  print('Last point:  (${decodedPath.last.x}, ${decodedPath.last.y})');
  print('');

  // --------------------------------------------------------------------------
  // 10. POLYLINE ENCODING
  // --------------------------------------------------------------------------
  // The reverse: encode a list of coordinates into a compact string.
  // Useful for sending paths to APIs or storing them efficiently.

  final reEncoded = GoogleMapsUtils.poly.encode(decodedPath);
  print('=== Polyline Encode ===');
  print('Re-encoded: $reEncoded');
  print('Matches original: ${reEncoded == encodedPolyline}');
  print('');

  // --------------------------------------------------------------------------
  // 11. DOUGLAS-PEUCKER SIMPLIFICATION
  // --------------------------------------------------------------------------
  // Reduces the number of points in a polyline while preserving its shape.
  // The `tolerance` is in meters — a higher value removes more points.
  // Useful for: reducing payload size, improving rendering performance.

  final simplified = GoogleMapsUtils.poly.simplify(decodedPath, 5000.0);
  final simplifiedEncoded = GoogleMapsUtils.poly.encode(simplified);
  print('=== Simplification ===');
  print('Original points:   ${decodedPath.length}');
  print('Simplified points: ${simplified.length}');
  print('Simplified encoded: $simplifiedEncoded');
  print('');

  // --------------------------------------------------------------------------
  // 12. POINT-IN-POLYGON
  // --------------------------------------------------------------------------
  // Determines whether a point lies inside a polygon.
  // Works correctly for polygons of any size, including those that cross the
  // antimeridian (±180°) or enclose a pole.
  //
  // The polygon can be open (auto-closed) or explicitly closed (first == last).
  // A point on a vertex is considered inside.

  // Define a triangle polygon (closed — last point == first point)
  final triangle = <Point<double>>[
    Point(-31.624115, -60.688734),
    Point(-31.624115, -60.684657),
    Point(-31.621594, -60.686717),
    Point(-31.624115, -60.688734), // closing point (optional)
  ];

  final insidePoint = Point<double>(-31.623060, -60.686690);
  final outsidePoint = Point<double>(-31.620000, -60.680000);

  print('=== Point-in-Polygon ===');
  print(
      'Inside point:  ${GoogleMapsUtils.poly.containsLocationPoly(insidePoint, triangle)}'); // true
  print(
      'Outside point: ${GoogleMapsUtils.poly.containsLocationPoly(outsidePoint, triangle)}'); // false

  // With geodesic edges (great-circle segments instead of rhumb lines):
  print(
      'Inside (geodesic): ${GoogleMapsUtils.poly.containsLocationPoly(insidePoint, triangle, geodesic: true)}');
  print('');

  // --------------------------------------------------------------------------
  // 13. POINT-ON-PATH DETECTION
  // --------------------------------------------------------------------------
  // Checks if a point lies on or near a polyline within a tolerance.
  // Default tolerance is 0.1 meters.
  // The `geodesic` flag determines whether edges are great-circle or rhumb.

  final equatorPath = <Point<double>>[
    Point(0.0, 0.0),
    Point(0.0, 10.0),
  ];

  final onPath = Point<double>(0.0, 5.0); // exactly on the equator segment
  final offPath = Point<double>(1.0, 5.0); // 1° north — ~111km away

  print('=== Point-on-Path ===');
  print(
      'Point on equator segment: ${GoogleMapsUtils.poly.isLocationOnPath(onPath, equatorPath, true)}'); // true
  print(
      'Point off the segment:    ${GoogleMapsUtils.poly.isLocationOnPath(offPath, equatorPath, true)}'); // false

  // Get which segment index the point falls on:
  final segIdx =
      GoogleMapsUtils.poly.locationIndexOnPath(onPath, equatorPath, true);
  print('Segment index: $segIdx'); // 0 (between equatorPath[0] and [1])
  print('');

  // --------------------------------------------------------------------------
  // 14. POINT-ON-EDGE (POLYGON BOUNDARY)
  // --------------------------------------------------------------------------
  // Like isLocationOnPath, but includes the closing segment between the last
  // and first points (i.e., treats the list as a closed polygon boundary).

  final square = <Point<double>>[
    Point(0.0, 0.0),
    Point(0.0, 10.0),
    Point(10.0, 10.0),
    Point(10.0, 0.0),
  ];

  // This point sits on the closing edge (from (10,0) back to (0,0))
  final edgePoint = Point<double>(5.0, 0.0);

  print('=== Point-on-Edge ===');
  print(
      'On polygon edge: ${GoogleMapsUtils.poly.isLocationOnEdge(edgePoint, square, true)}'); // true
  print(
      'On open path:    ${GoogleMapsUtils.poly.isLocationOnPath(edgePoint, square, true)}'); // false (closing edge not included)
  print('');

  // --------------------------------------------------------------------------
  // 15. DISTANCE TO A LINE SEGMENT
  // --------------------------------------------------------------------------
  // Computes the shortest spherical distance from a point to a line segment.
  // Useful for finding the closest point on a route to a given location.

  final randomPoint = Point<double>(-23.54545, -23.898098);
  final segStart = Point<double>(0.0, 0.0);
  final segEnd = Point<double>(10.0, 5.0);

  final distToLine =
      GoogleMapsUtils.poly.distanceToLine(randomPoint, segStart, segEnd);
  print('=== Distance to Line ===');
  print('Distance: ${distToLine.toStringAsFixed(2)} meters');
  print('');

  // --------------------------------------------------------------------------
  // 16. COMPUTE AREA
  // --------------------------------------------------------------------------
  // Returns the area enclosed by a polygon in square meters.
  // computeSignedArea returns a positive value for counter-clockwise winding,
  // negative for clockwise. computeArea returns the absolute value.

  final area = GoogleMapsUtils.spherical.computeArea(triangle);
  final signedArea = GoogleMapsUtils.spherical.computeSignedArea(triangle);
  print('=== Area ===');
  print('Area: ${area.toStringAsFixed(2)} m²');
  print('Signed area: ${signedArea.toStringAsFixed(2)} m²');
  print('Winding: ${signedArea > 0 ? "counter-clockwise" : "clockwise"}');
  print('');

  // --------------------------------------------------------------------------
  // 17. BOUNDING BOXES
  // --------------------------------------------------------------------------
  // Create axis-aligned bounding boxes from points or center+radius.
  // Useful for map camera fitting, spatial queries, and tile subdivision.

  print('=== Bounding Boxes ===');

  // From a center point and radius (meters):
  final bounds = GoogleMapsUtils.spherical.toBounds(-23.55, -46.63, 1000.0);
  print('Bounds from center+radius:');
  print(
      '  NE: (${bounds.ne.x.toStringAsFixed(6)}, ${bounds.ne.y.toStringAsFixed(6)})');
  print(
      '  SW: (${bounds.sw.x.toStringAsFixed(6)}, ${bounds.sw.y.toStringAsFixed(6)})');

  // From a list of points (finds the min/max lat/lng):
  final boundsFromPts = GoogleMapsUtils.spherical.toBoundsFromPoints(route);
  print('Bounds from points:');
  print(
      '  NE: (${boundsFromPts.ne.x.toStringAsFixed(4)}, ${boundsFromPts.ne.y.toStringAsFixed(4)})');
  print(
      '  SW: (${boundsFromPts.sw.x.toStringAsFixed(4)}, ${boundsFromPts.sw.y.toStringAsFixed(4)})');

  // Center of a bounding box:
  final center =
      GoogleMapsUtils.spherical.centerFromLatLngBounds(boundsFromPts);
  print(
      'Center: (${center.x.toStringAsFixed(4)}, ${center.y.toStringAsFixed(4)})');
  print('');

  // --------------------------------------------------------------------------
  // 18. SUB-BOUNDS (GRID TILING)
  // --------------------------------------------------------------------------
  // Splits a bounding box into a grid of smaller bounding boxes.
  // division=2 → 4 tiles, division=3 → 9 tiles, etc.
  // Useful for: paginated map loading, spatial partitioning.

  final tiles =
      GoogleMapsUtils.spherical.toSubBounds(boundsFromPts, division: 3);
  print('=== Sub-Bounds ===');
  print('Split into ${tiles.length} tiles (3×3 grid):');
  for (int i = 0; i < tiles.length; i++) {
    final t = tiles[i];
    print(
        '  Tile $i: NE(${t.ne.x.toStringAsFixed(4)}, ${t.ne.y.toStringAsFixed(4)}) '
        'SW(${t.sw.x.toStringAsFixed(4)}, ${t.sw.y.toStringAsFixed(4)})');
  }
  print('');

  // --------------------------------------------------------------------------
  // 19. CLOSED POLYGON CHECK
  // --------------------------------------------------------------------------
  // A simple utility to check whether a polyline is closed (first == last).

  print('=== Closed Polygon Check ===');
  print(
      'Triangle is closed: ${GoogleMapsUtils.poly.isClosedPolygon(triangle)}'); // true
  print(
      'Route is closed:    ${GoogleMapsUtils.poly.isClosedPolygon(route)}'); // false
  print('');

  // --------------------------------------------------------------------------
  // 20. TOLERANCE VARIANTS
  // --------------------------------------------------------------------------
  // isLocationOnPath and isLocationOnEdge have default 0.1m tolerance.
  // You can specify a custom tolerance (in meters) for less/more strictness.

  print('=== Tolerance Variants ===');

  // Point that's ~11 meters off the equator (0.0001° lat ≈ 11m)
  final nearPoint = Point<double>(0.0001, 5.0);

  // Default tolerance (0.1m) — too far away:
  print(
      'Near point (0.1m tolerance):  ${GoogleMapsUtils.poly.isLocationOnPathTolerance(nearPoint, equatorPath, true, 0.1)}'); // false

  // Custom tolerance of 20 meters — close enough:
  print(
      'Near point (20m tolerance):   ${GoogleMapsUtils.poly.isLocationOnPathTolerance(nearPoint, equatorPath, true, 20.0)}'); // true

  // Edge with custom tolerance:
  print(
      'Edge (50m tolerance):         ${GoogleMapsUtils.poly.isLocationOnEdgeTolerance(nearPoint, square, true, 20.0)}'); // true

  // locationIndexOnPath with tolerance:
  final idxTol = GoogleMapsUtils.poly
      .locationIndexOnPathTolerance(nearPoint, equatorPath, true, 20.0);
  print('Segment index (20m tol): $idxTol'); // 0
  print('');

  // --------------------------------------------------------------------------
  // 21. LOCATION INDEX ON EDGE OR PATH (ADVANCED)
  // --------------------------------------------------------------------------
  // The underlying method that powers both isLocationOnPath and isLocationOnEdge.
  // The `closed` parameter controls whether the closing segment is included.

  print('=== locationIndexOnEdgeOrPath ===');

  // closed=false → acts like isLocationOnPath (no closing segment)
  final idxOpen = GoogleMapsUtils.poly
      .locationIndexOnEdgeOrPath(edgePoint, square, false, true, 0.1);
  print('Edge point, open path:   index=$idxOpen'); // -1 (not found)

  // closed=true → acts like isLocationOnEdge (closing segment included)
  final idxClosed = GoogleMapsUtils.poly
      .locationIndexOnEdgeOrPath(edgePoint, square, true, true, 0.1);
  print('Edge point, closed path: index=$idxClosed'); // 3 (on closing segment)
  print('');

  // --------------------------------------------------------------------------
  // 22. MATH UTILS (LOW-LEVEL HELPERS)
  // --------------------------------------------------------------------------
  // GoogleMapsUtils.math provides the building blocks used internally by the
  // spherical and poly utilities. You can use them directly for custom
  // calculations.

  print('=== MathUtils ===');

  // Earth's mean radius (IUGG standard, meters):
  print('Earth radius: ${GoogleMapsUtils.math.earthRadius} meters');

  // Clamp: restricts a value to [low, high]
  print('clamp(15, 0, 10): ${GoogleMapsUtils.math.clamp(15, 0, 10)}'); // 10
  print('clamp(-5, 0, 10): ${GoogleMapsUtils.math.clamp(-5, 0, 10)}'); // 0
  print('clamp(5, 0, 10):  ${GoogleMapsUtils.math.clamp(5, 0, 10)}'); // 5

  // Wrap: wraps a value into the range [min, max)
  // Commonly used for longitude wrapping [-180, 180)
  print(
      'wrap(190, -180, 180):  ${GoogleMapsUtils.math.wrap(190, -180, 180)}'); // -170
  print(
      'wrap(-200, -180, 180): ${GoogleMapsUtils.math.wrap(-200, -180, 180)}'); // 160

  // Mod: non-negative modulo (unlike Dart's %, which can return negatives)
  print('mod(-1, 360):  ${GoogleMapsUtils.math.mod(-1, 360)}'); // 359
  print('mod(361, 360): ${GoogleMapsUtils.math.mod(361, 360)}'); // 1

  // Mercator projection (latitude in radians → mercator Y, and inverse)
  final latRad = GoogleMapsUtils.spherical.toRadians(45.0); // 45° latitude
  final mercY = GoogleMapsUtils.math.mercator(latRad);
  final latBack = GoogleMapsUtils.math.inverseMercator(mercY);
  print('mercator(45° in rad):    ${mercY.toStringAsFixed(8)}');
  print(
      'inverseMercator(y):      ${GoogleMapsUtils.spherical.toDegrees(latBack).toStringAsFixed(4)}°'); // 45°

  // Haversine and inverse haversine
  // hav(θ) = (1 - cos(θ)) / 2 = sin²(θ/2)
  final h = GoogleMapsUtils.math.hav(1.0); // hav(1 radian)
  final recovered = GoogleMapsUtils.math.arcHav(h); // should give back 1.0
  print('hav(1.0):    ${h.toStringAsFixed(8)}');
  print('arcHav(h):   ${recovered.toStringAsFixed(8)}'); // 1.0

  // havDistance: haversine of the central angle between two points (on unit sphere)
  // Arguments are in radians: lat1, lat2, dLng (difference in longitude)
  final lat1 = GoogleMapsUtils.spherical.toRadians(48.8566); // Paris lat
  final lat2 = GoogleMapsUtils.spherical.toRadians(51.5074); // London lat
  final dLng = GoogleMapsUtils.spherical.toRadians(2.3522) -
      GoogleMapsUtils.spherical.toRadians(-0.1278);
  final havDist = GoogleMapsUtils.math.havDistance(lat1, lat2, dLng);
  print('havDistance(Paris, London): ${havDist.toStringAsFixed(10)}');
  // To get actual distance: arcHav(havDist) * earthRadius
  final distFromHav =
      GoogleMapsUtils.math.arcHav(havDist) * GoogleMapsUtils.math.earthRadius;
  print('Distance from hav: ${distFromHav.toStringAsFixed(2)} meters');
  print('');

  // --------------------------------------------------------------------------
  // 23. DEGREE/RADIAN CONVERSIONS
  // --------------------------------------------------------------------------
  // GoogleMapsUtils.spherical exposes toRadians and toDegrees for convenience.

  print('=== Degree/Radian Conversions ===');
  print('90° → radians: ${GoogleMapsUtils.spherical.toRadians(90.0)}'); // π/2
  print(
      'π radians → degrees: ${GoogleMapsUtils.spherical.toDegrees(pi)}'); // 180
  print('');

  // --------------------------------------------------------------------------
  // 24. DISTANCE IN RADIANS (UNIT SPHERE)
  // --------------------------------------------------------------------------
  // distanceRadians returns the arc length on the unit sphere between two
  // points given in radians. Multiply by earthRadius for meters.
  // This is what computeAngleBetween uses internally.

  print('=== Distance Radians ===');
  final dRad = GoogleMapsUtils.spherical.distanceRadians(
    GoogleMapsUtils.spherical.toRadians(paris.x), // lat1
    GoogleMapsUtils.spherical.toRadians(paris.y), // lng1
    GoogleMapsUtils.spherical.toRadians(london.x), // lat2
    GoogleMapsUtils.spherical.toRadians(london.y), // lng2
  );
  print('distanceRadians(Paris, London): ${dRad.toStringAsFixed(8)} rad');
  print(
      'Same as computeAngleBetween:    ${GoogleMapsUtils.spherical.computeAngleBetween(paris, london).toStringAsFixed(8)} rad');
  print('');

  // --------------------------------------------------------------------------
  // DONE!
  // --------------------------------------------------------------------------
  print('✅ All examples completed successfully.');
}
62
likes
160
points
233
downloads

Documentation

API reference

Publisher

verified publisheradriankohls.app

Weekly Downloads

SphericalUtils, MathUtils and PolyUtils ported from Google's android-maps-utils: distances, headings, bounds, polyline encode/decode and Douglas-Peucker simplification.

Homepage
Repository (GitHub)
View/report issues

Topics

#maps #geolocation #polyline #geometry #geodesy

Funding

Consider supporting this project:

bus2.me

License

Apache-2.0 (license)

More

Packages that depend on google_maps_utils