Geodesy Helpers#
Ground-measurement helpers in pyramids.base.geodesy. Where
pyramids.base.crs answers what a coordinate means, these answer how
far apart two coordinates are on the figure of the earth, and how much of a CRS's
own unit a given ground distance occupies at a given place.
The ellipsoid always comes from the CRS's own datum, never from a hard-coded
WGS 84 default — the same discipline
Dataset.cell_area uses when it integrates cell areas.
Why the place matters#
There is no single metres-per-degree constant. A degree of longitude is about
111 km at the equator and 0 at the pole, and Web Mercator stretches its own metre
by 1 / cos(lat). So ground_distance_in_crs takes the location as a required
argument rather than assuming one:
from pyramids.base.geodesy import ground_distance_in_crs
ground_distance_in_crs(100_000.0, crs=4326, at=(0.0, 0.0)) # 0.8983 degrees
ground_distance_in_crs(100_000.0, crs=4326, at=(0.0, 60.0)) # 1.7917 degrees
This is what sizing a scale bar needs: cleopatra's add_scale_bar takes a length
in the axes' data units, and converting "100 km on the ground" into those units
is a question about the Earth, not about pixels.
Coordinates are always geographic degrees#
Every function here takes geographic degrees and uses crs only to select the
ellipsoid. Projected coordinates are refused, not answered, because PROJ
would return nan for an out-of-range latitude and that would propagate
silently. Reproject first with
reproject_coordinates.
at is always in (x, y) order — easting then northing, longitude then
latitude — whatever axis order the CRS itself declares.
Vector counterparts#
FeatureCollection wraps these as the geodesic counterparts of the planar
operations it inherits from GeoDataFrame, for callers who hold geometries
rather than coordinates:
| Geodesic | Planar twin |
|---|---|
FeatureCollection.geodesic_distance |
.distance |
FeatureCollection.geodesic_length |
.length |
FeatureCollection.geodesic_area |
.area |
See the FeatureCollection reference.
Functions#
pyramids.base.geodesy.geodesic_distance(lon1, lat1, lon2, lat2, *, crs=4326, unit='m')
#
Ground distance between geographic points, on the CRS's own ellipsoid.
Solves the inverse geodesic problem -- the shortest path across the ellipsoid, not a great circle on an assumed sphere, which differs by up to ~0.5 % and by ~20 km on a pole-to-pole span.
All four coordinate arguments are geographic degrees, whatever crs is:
crs selects the ellipsoid to measure on (through its datum), not the
frame the inputs are expressed in. Pass projected coordinates through
:func:pyramids.base.crs.reproject_coordinates first -- handing them in
directly is refused, not answered, because pyproj.Geod would return nan
for an out-of-range latitude and that would propagate silently.
Scalars in, scalar out; arrays in, array out -- the loop runs inside PROJ, so a transect of thousands of vertices costs one call. The arrays must all be the same length: PROJ pairs them elementwise and does not broadcast, so a scalar cannot be mixed with an array.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
lon1
|
Any
|
Longitude(s) of the first point, in degrees. |
required |
lat1
|
Any
|
Latitude(s) of the first point, in degrees. |
required |
lon2
|
Any
|
Longitude(s) of the second point, in degrees. |
required |
lat2
|
Any
|
Latitude(s) of the second point, in degrees. |
required |
crs
|
Any
|
The CRS whose datum names the ellipsoid to measure on. Any form
:func: |
4326
|
unit
|
str
|
|
'm'
|
Returns:
| Type | Description |
|---|---|
float | FloatArray
|
float | FloatArray: The distance(s), in |
float | FloatArray
|
coordinate argument is scalar, otherwise an array of the same shape as |
float | FloatArray
|
the inputs, which PROJ pairs elementwise. |
Raises:
| Type | Description |
|---|---|
ValueError
|
|
CRSError
|
|
Examples:
- A degree of longitude at the equator:
- The same degree at 60 degrees north is about half as long, which is the whole reason a ground distance cannot be read off a cell size:
- Vectorised, and in kilometres:
- An unrecognised unit is refused, and the message lists the valid ones:
See Also
ground_distance_in_crs: The inverse -- a ground distance expressed in a CRS's own units.
Source code in src/pyramids/base/geodesy.py
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 | |
pyramids.base.geodesy.ground_distance_in_crs(distance_m, *, crs, at, azimuth=_EAST)
#
How many of crs's own units span distance_m of ground at at.
The question a scale bar asks: cleopatra's add_scale_bar takes a length in
the axes' data units, so sizing a "100 km" bar means converting 100 km of
ground into the display CRS's units -- and that conversion depends on where
on the map it is measured. A degree of longitude runs from 111 km at the
equator to 0 at the pole, and Web Mercator stretches by 1 / cos(lat), so
at is required rather than defaulted.
The ground distance is walked out from at along azimuth by the direct
geodesic problem, and the straight-line separation of the two endpoints is
measured back in crs. Because a geodesic heading east is not a parallel,
the endpoint drifts slightly in latitude; the returned value is the full
separation, which is what a bar drawn between those two points spans.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
distance_m
|
float
|
The ground distance, in metres. Must be finite and positive. |
required |
crs
|
Any
|
The CRS to express the answer in, in any form
:func: |
required |
at
|
tuple[float, float]
|
|
required |
azimuth
|
float
|
Direction to measure along, in degrees clockwise from north.
Default |
_EAST
|
Returns:
| Name | Type | Description |
|---|---|---|
float |
float
|
The span in |
Raises:
| Type | Description |
|---|---|
ValueError
|
|
CRSError
|
|
Examples:
- 100 km of ground at the equator, in degrees:
- The same 100 km at 60 degrees north needs about twice as many degrees:
- Web Mercator is in metres, but not ground metres away from the equator -- at 60 north its unit is stretched by about two:
- A non-positive ground distance is refused rather than returning zero:
See Also
geodesic_distance: The forward direction -- ground metres between two geographic points.
Source code in src/pyramids/base/geodesy.py
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 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 | |
pyramids.base.geodesy.geodesic_geometry_length(geometry, *, crs=4326, unit='m')
#
Length of a geometry's lines along the ellipsoid.
The geodesic counterpart of shapely's planar .length, which on a
geographic CRS measures in degrees and is meaningless as a ground distance.
geometry must already be in geographic coordinates; crs only selects
the ellipsoid. A polygon reports its perimeter, a point reports 0.0, and a
multi-part geometry sums its parts -- all of which is pyproj.Geod's own
behaviour, passed through rather than reinterpreted.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
geometry
|
Any
|
Any shapely geometry, in geographic coordinates. |
required |
crs
|
Any
|
The CRS whose datum names the ellipsoid. Default |
4326
|
unit
|
str
|
|
'm'
|
Returns:
| Name | Type | Description |
|---|---|---|
float |
float
|
The length in |
Raises:
| Type | Description |
|---|---|
ValueError
|
|
CRSError
|
|
Examples:
- A one-degree line at the equator is ~111 km long:
- A polygon reports its perimeter, in kilometres here:
- A point has no length:
See Also
geodesic_geometry_area: The area counterpart.
Source code in src/pyramids/base/geodesy.py
651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 | |
pyramids.base.geodesy.geodesic_geometry_area(geometry, *, crs=4326, unit='m2')
#
Area of a geometry on the ellipsoid.
The geodesic counterpart of shapely's planar .area, which on a geographic
CRS reports square degrees -- a quantity that varies with latitude and so
cannot be compared between rows of a grid.
geometry must already be in geographic coordinates; crs only selects
the ellipsoid. The result is always non-negative: pyproj.Geod signs the
area by ring orientation (negative for a clockwise ring), which describes
winding rather than size, so the magnitude is returned. A line or point
reports 0.0.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
geometry
|
Any
|
Any shapely geometry, in geographic coordinates. |
required |
crs
|
Any
|
The CRS whose datum names the ellipsoid. Default |
4326
|
unit
|
str
|
|
'm2'
|
Returns:
| Name | Type | Description |
|---|---|---|
float |
float
|
The area in |
Raises:
| Type | Description |
|---|---|
ValueError
|
|
CRSError
|
|
Examples:
- A one-degree square at the equator covers ~12 309 square kilometres:
- Ring orientation does not change the size:
- A line encloses nothing:
See Also
geodesic_geometry_length: The length counterpart.
Source code in src/pyramids/base/geodesy.py
729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 | |