-
Notifications
You must be signed in to change notification settings - Fork 7
Expand file tree
/
Copy pathexample.dart
More file actions
executable file
·522 lines (438 loc) · 23.2 KB
/
Copy pathexample.dart
File metadata and controls
executable file
·522 lines (438 loc) · 23.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
// ============================================================================
// 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.');
}