Skill Tracer
Skills › Scientific Agent Skills › geomaster › coordinate-systems.md

coordinate-systems.md

368 lines · skills/geomaster/references/coordinate-systems.md at 5f2bc03

Open on GitHub
1# Coordinate Reference Systems (CRS)
2
3Coordinate-system reference, reviewed 2026-10-01 against PyProj/PROJ. Code with external data is illustrative. CRS names were checked against the installed PROJ database; a numerical round trip does not establish datum accuracy or grid availability.
4

Coordinate Reference Systems (CRS)

Coordinate-system reference, reviewed 2026-10-01 against PyProj/PROJ. Code with external data is illustrative. CRS names were checked against the installed PROJ database; a numerical round trip does not establish datum accuracy or grid availability.

Raw lines 1 to 4
5## Table of Contents
6
71. [Fundamentals](#fundamentals)
82. [Common CRS Codes](#common-crs-codes)
93. [Projected vs Geographic](#projected-vs-geographic)
104. [UTM Zones](#utm-zones)
115. [Transformations](#transformations)
126. [Best Practices](#best-practices)

Table of Contents

  1. Fundamentals
  2. Common CRS Codes
  3. Projected vs Geographic
  4. UTM Zones
  5. Transformations
  6. Best Practices
Raw lines 5 to 13
14## Fundamentals

Fundamentals

Raw lines 14 to 15
16### What is a CRS?
18A Coordinate Reference System defines how coordinates relate to positions on Earth:
20- **Geographic CRS**: Uses angular latitude/longitude; usually degrees, but inspect axis units
21- **Projected CRS**: Uses Cartesian coordinates (meters, feet)
22- **Vertical CRS**: Defines height/depth (e.g., ellipsoidal heights)

What is a CRS?

A Coordinate Reference System defines how coordinates relate to positions on Earth:

  • Geographic CRS: Uses angular latitude/longitude; usually degrees, but inspect axis units
  • Projected CRS: Uses Cartesian coordinates (meters, feet)
  • Vertical CRS: Defines height/depth (e.g., ellipsoidal heights)
Raw lines 16 to 23
24### Components
261. **Datum**: Mathematical model of Earth's shape
27 - WGS 84 (EPSG:4326) - Global GPS
28 - NAD 83 (EPSG:4269) - North America
29 - ETRS89 (EPSG:4258) - Europe
312. **Projection**: Transformation from curved to flat surface
32 - Cylindrical (Mercator)
33 - Conic (Lambert Conformal)
34 - Azimuthal (Polar Stereographic)
363. **Units**: Degrees, meters, feet, etc.

Components

  1. Datum: Mathematical model of Earth's shape
  2. WGS 84 (EPSG:4326) - Global GPS
  3. NAD 83 (EPSG:4269) - North America
  4. ETRS89 (EPSG:4258) - Europe
  1. Projection: Transformation from curved to flat surface
  2. Cylindrical (Mercator)
  3. Conic (Lambert Conformal)
  4. Azimuthal (Polar Stereographic)
  1. Units: Degrees, meters, feet, etc.
Raw lines 24 to 37
38## Common CRS Codes

Common CRS Codes

Raw lines 38 to 39
40### Geographic CRS (Lat/Lon)
42| EPSG | Name | Area | Notes |
43|------|------|------|-------|
44| 4326 | WGS 84 | Global | GPS default, use for storage |
45| 4269 | NAD83 | North America | USGS data, slightly different from WGS84 |
46| 4258 | ETRS89 | Europe | European reference frame |
47| 4283 | GDA94 | Australia | Australian datum |

Geographic CRS (Lat/Lon)

EPSGNameAreaNotes
4326WGS 84GlobalGPS default, use for storage
4269NAD83North AmericaUSGS data, slightly different from WGS84
4258ETRS89EuropeEuropean reference frame
4283GDA94AustraliaAustralian datum
Raw lines 40 to 48
49### Projected CRS (Meters)
51| EPSG | Name | Area | Distortion | Notes |
52|------|------|------|------------|-------|
53| 3857 | Web Mercator | Global (85°S-85°N) | High near poles | Web maps (Google, OSM) |
54| 32601-32660 | UTM Zone N | 6° longitude zones | Check area of use | Metric calculations |
55| 32701-32760 | UTM Zone S | 6° longitude zones | Check area of use | Southern hemisphere |
56| 3395 | World Mercator | World | Increases toward poles | Conformal, not equal-area |
57| 5070 | CONUS Albers | USA (conterminous) | Low | US national mapping |
58| 2154 | Lambert-93 | France | Very low | French national projection |

Projected CRS (Meters)

EPSGNameAreaDistortionNotes
3857Web MercatorGlobal (85°S-85°N)High near polesWeb maps (Google, OSM)
32601-32660UTM Zone N6° longitude zonesCheck area of useMetric calculations
32701-32760UTM Zone S6° longitude zonesCheck area of useSouthern hemisphere
3395World MercatorWorldIncreases toward polesConformal, not equal-area
5070CONUS AlbersUSA (conterminous)LowUS national mapping
2154Lambert-93FranceVery lowFrench national projection
Raw lines 49 to 59
60### Regional Projections
62**United States:**
63- EPSG:5070 - NAD83 / Conus Albers
64- EPSG:3338 - NAD83 / Alaska Albers
65- ESRI:102003 - USA Contiguous Albers Equal Area
66- EPSG:2227 - California Zone 3 (US Feet)
68**Europe:**
69- EPSG:3035 - Europe Equal Area 2001
70- EPSG:3857 - Web Mercator (web mapping)
71- EPSG:2154 - Lambert 93 (France)
72- EPSG:25832-25836 - UTM zones (ETRS89)
74**Other:**
75- EPSG:3112 - GDA94 / Geoscience Australia Lambert (Australia)
76- EPSG:2056 - CH1903+ / LV95 (Switzerland)
77- EPSG:4326 - WGS 84 (global default)

Regional Projections

United States:

  • EPSG:5070 - NAD83 / Conus Albers
  • EPSG:3338 - NAD83 / Alaska Albers
  • ESRI:102003 - USA Contiguous Albers Equal Area
  • EPSG:2227 - California Zone 3 (US Feet)

Europe:

  • EPSG:3035 - Europe Equal Area 2001
  • EPSG:3857 - Web Mercator (web mapping)
  • EPSG:2154 - Lambert 93 (France)
  • EPSG:25832-25836 - UTM zones (ETRS89)

Other:

  • EPSG:3112 - GDA94 / Geoscience Australia Lambert (Australia)
  • EPSG:2056 - CH1903+ / LV95 (Switzerland)
  • EPSG:4326 - WGS 84 (global default)
Raw lines 60 to 78
79## Projected vs Geographic

Projected vs Geographic

Raw lines 79 to 80
81### When to Use Geographic (EPSG:4326)
83✅ Storing data (databases, files)
84✅ Global datasets
85✅ Web APIs (GeoJSON, KML)
86✅ Latitude/longitude queries
87✅ GPS coordinates
89```python
90# Bad: Distance calculation in geographic CRS
91# Assume gdf already has a verified geographic CRS
92distance = gdf.geometry.length # WRONG! Returns degrees, not meters
94# Good: Calculate distance in projected CRS
95gdf_projected = gdf.to_crs("EPSG:32633") # UTM Zone 33N
96distance_m = gdf_projected.geometry.length # Correct: meters
97```

When to Use Geographic (EPSG:4326)

✅ Storing data (databases, files) ✅ Global datasets ✅ Web APIs (GeoJSON, KML) ✅ Latitude/longitude queries ✅ GPS coordinates

# Bad: Distance calculation in geographic CRS
# Assume gdf already has a verified geographic CRS
distance = gdf.geometry.length  # WRONG! Returns degrees, not meters

# Good: Calculate distance in projected CRS
gdf_projected = gdf.to_crs("EPSG:32633")  # UTM Zone 33N
distance_m = gdf_projected.geometry.length  # Correct: meters
Raw lines 81 to 98
99### When to Use Projected
101✅ Area/distance calculations
102✅ Buffer operations
103✅ Spatial analysis
104✅ High-resolution mapping
105✅ Engineering applications
107```python
108import geopandas as gpd
110# Project to appropriate UTM zone
111gdf = gdf.to_crs(gdf.estimate_utm_crs())
113# Now area and distance are accurate
114area_sqm = gdf.geometry.area
115buffer_1km = gdf.geometry.buffer(1000) # 1000 meters
116```

When to Use Projected

✅ Area/distance calculations ✅ Buffer operations ✅ Spatial analysis ✅ High-resolution mapping ✅ Engineering applications

import geopandas as gpd

# Project to appropriate UTM zone
gdf = gdf.to_crs(gdf.estimate_utm_crs())

# Now area and distance are accurate
area_sqm = gdf.geometry.area
buffer_1km = gdf.geometry.buffer(1000)  # 1000 meters
Raw lines 99 to 117
118### Web Mercator Warning
120⚠️ **EPSG:3857 (Web Mercator) for visualization only**
122```python
123# DON'T use Web Mercator for area calculations
124gdf_web = gdf.to_crs("EPSG:3857")
125area = gdf_web.geometry.area # WRONG! Significant distortion
127# DO use appropriate projection
128gdf_utm = gdf.to_crs("EPSG:32633") # or estimate_utm_crs()
129area = gdf_utm.geometry.area # Correct
130```

Web Mercator Warning

⚠️ EPSG:3857 (Web Mercator) for visualization only

# DON'T use Web Mercator for area calculations
gdf_web = gdf.to_crs("EPSG:3857")
area = gdf_web.geometry.area  # WRONG! Significant distortion

# DO use appropriate projection
gdf_utm = gdf.to_crs("EPSG:32633")  # or estimate_utm_crs()
area = gdf_utm.geometry.area  # Correct
Raw lines 118 to 131
132## UTM Zones
134### Understanding UTM Zones
136Earth is divided into 60 zones (6° longitude each):
137- Zones 1-60: West to East
138- Each zone divided into North (326xx) and South (327xx)

Understanding UTM Zones

Earth is divided into 60 zones (6° longitude each):

  • Zones 1-60: West to East
  • Each zone divided into North (326xx) and South (327xx)
Raw lines 134 to 139
140### Finding Your UTM Zone
142```python
143def get_utm_zone(longitude, latitude):
144 """Nominal 6-degree zone only; special Norway/Svalbard zones need registry lookup."""
145 import math
147 if not (-180 <= longitude <= 180 and -80 <= latitude <= 84):
148 raise ValueError('Outside the nominal UTM domain')
149 zone = min(60, math.floor((longitude + 180) / 6) + 1)
151 if latitude >= 0:
152 epsg = 32600 + zone # Northern hemisphere
153 else:
154 epsg = 32700 + zone # Southern hemisphere
156 return f"EPSG:{epsg}"
158# Example
159get_utm_zone(-122.4, 37.7) # Returns 'EPSG:32610' (Zone 10N)
160```

Finding Your UTM Zone

def get_utm_zone(longitude, latitude):
    """Nominal 6-degree zone only; special Norway/Svalbard zones need registry lookup."""
    import math

    if not (-180 <= longitude <= 180 and -80 <= latitude <= 84):
        raise ValueError('Outside the nominal UTM domain')
    zone = min(60, math.floor((longitude + 180) / 6) + 1)

    if latitude >= 0:
        epsg = 32600 + zone  # Northern hemisphere
    else:
        epsg = 32700 + zone  # Southern hemisphere

    return f"EPSG:{epsg}"

# Example
get_utm_zone(-122.4, 37.7)  # Returns 'EPSG:32610' (Zone 10N)
Raw lines 140 to 161
162### Auto-Detect UTM Zone with GeoPandas
164```python
165import geopandas as gpd
167# Load data
168gdf = gpd.read_file('data.geojson')
170# Estimate best UTM zone
171utm_crs = gdf.estimate_utm_crs()
172print(f"Best UTM CRS: {utm_crs}")
174# Reproject
175gdf_projected = gdf.to_crs(utm_crs)
176```

Auto-Detect UTM Zone with GeoPandas

import geopandas as gpd

# Load data
gdf = gpd.read_file('data.geojson')

# Estimate best UTM zone
utm_crs = gdf.estimate_utm_crs()
print(f"Best UTM CRS: {utm_crs}")

# Reproject
gdf_projected = gdf.to_crs(utm_crs)
Raw lines 162 to 177
178### Special UTM Cases
180**UPS (Universal Polar Stereographic):**
181- EPSG:5041 - UPS North (Arctic)
182- EPSG:5042 - UPS South (Antarctic)
184**Other national grids (not UTM):**
185- EPSG:31466-31469 - German Gauss-Krüger zones
186- EPSG:2056 - Swiss LV95 (oblique Mercator)

Special UTM Cases

UPS (Universal Polar Stereographic):

  • EPSG:5041 - UPS North (Arctic)
  • EPSG:5042 - UPS South (Antarctic)

Other national grids (not UTM):

  • EPSG:31466-31469 - German Gauss-Krüger zones
  • EPSG:2056 - Swiss LV95 (oblique Mercator)
Raw lines 178 to 187
188## Transformations

Transformations

Raw lines 188 to 189
190### Basic Transformation
192```python
193from pyproj import Transformer
195# Create transformer
196transformer = Transformer.from_crs(
197 "EPSG:4326", # WGS 84 (lat/lon)
198 "EPSG:32610", # UTM Zone 10N for this California point
199 always_xy=True # Input: x=lon, y=lat (not y=lat, x=lon)
202# Transform single point
203lon, lat = -122.4, 37.7
204x, y = transformer.transform(lon, lat)
205print(f"Easting: {x:.2f}, Northing: {y:.2f}")
206```

Basic Transformation

from pyproj import Transformer

# Create transformer
transformer = Transformer.from_crs(
    "EPSG:4326",  # WGS 84 (lat/lon)
    "EPSG:32610", # UTM Zone 10N for this California point
    always_xy=True  # Input: x=lon, y=lat (not y=lat, x=lon)
)

# Transform single point
lon, lat = -122.4, 37.7
x, y = transformer.transform(lon, lat)
print(f"Easting: {x:.2f}, Northing: {y:.2f}")
Raw lines 190 to 207
208### Batch Transformation
210```python
211import numpy as np
212from pyproj import Transformer
214# Arrays of coordinates
215lon_array = [-122.4, -122.3]
216lat_array = [37.7, 37.8]
218transformer = Transformer.from_crs("EPSG:4326", "EPSG:32610", always_xy=True)
219xs, ys = transformer.transform(lon_array, lat_array)
220```

Batch Transformation

import numpy as np
from pyproj import Transformer

# Arrays of coordinates
lon_array = [-122.4, -122.3]
lat_array = [37.7, 37.8]

transformer = Transformer.from_crs("EPSG:4326", "EPSG:32610", always_xy=True)
xs, ys = transformer.transform(lon_array, lat_array)
Raw lines 208 to 221
222### Transformation with PyProj CRS
224```python
225from pyproj import CRS
227# Get CRS information
228crs = CRS.from_epsg(32633)
230print(f"Name: {crs.name}")
231print(f"Type: {crs.type_name}")
232print(f"Area of use: {crs.area_of_use.name}")
233print(f"Datum: {crs.datum.name}")
234print(f"Ellipsoid: {crs.ellipsoid.name}")
235```

Transformation with PyProj CRS

from pyproj import CRS

# Get CRS information
crs = CRS.from_epsg(32633)

print(f"Name: {crs.name}")
print(f"Type: {crs.type_name}")
print(f"Area of use: {crs.area_of_use.name}")
print(f"Datum: {crs.datum.name}")
print(f"Ellipsoid: {crs.ellipsoid.name}")
Raw lines 222 to 236
237## Best Practices

Best Practices

Raw lines 237 to 238
239### 1. Always Know Your CRS
241```python
242import geopandas as gpd
244gdf = gpd.read_file('data.geojson')
246# Check CRS immediately
247print(f"CRS: {gdf.crs}") # Should never be None!
249# If absent, obtain the source CRS from metadata; never guess it from bounds.
250if gdf.crs is None:
251 raise ValueError("Source CRS is unknown")
252```

1. Always Know Your CRS

import geopandas as gpd

gdf = gpd.read_file('data.geojson')

# Check CRS immediately
print(f"CRS: {gdf.crs}")  # Should never be None!

# If absent, obtain the source CRS from metadata; never guess it from bounds.
if gdf.crs is None:
    raise ValueError("Source CRS is unknown")
Raw lines 239 to 253
254### 2. Verify CRS Before Operations
256```python
257def ensure_same_crs(gdf1, gdf2):
258 """Ensure two GeoDataFrames have same CRS."""
259 if gdf1.crs is None or gdf2.crs is None:
260 raise ValueError("Source CRS is unknown")
261 if gdf1.crs != gdf2.crs:
262 gdf2 = gdf2.to_crs(gdf1.crs)
263 print(f"Reprojected gdf2 to {gdf1.crs}")
264 return gdf1, gdf2
266# Use before spatial operations
267zones, points = ensure_same_crs(zones_gdf, points_gdf)
268result = gpd.sjoin(points, zones, predicate='within')
269```

2. Verify CRS Before Operations

def ensure_same_crs(gdf1, gdf2):
    """Ensure two GeoDataFrames have same CRS."""
    if gdf1.crs is None or gdf2.crs is None:
        raise ValueError("Source CRS is unknown")
    if gdf1.crs != gdf2.crs:
        gdf2 = gdf2.to_crs(gdf1.crs)
        print(f"Reprojected gdf2 to {gdf1.crs}")
    return gdf1, gdf2

# Use before spatial operations
zones, points = ensure_same_crs(zones_gdf, points_gdf)
result = gpd.sjoin(points, zones, predicate='within')
Raw lines 254 to 270
271### 3. Use Appropriate Projections
273```python
274# For local analysis (< 500km extent)
275gdf_local = gdf.to_crs(gdf.estimate_utm_crs())
277# For national/regional analysis
278gdf_us = gdf.to_crs("EPSG:5070") # NAD83 / Conus Albers
279gdf_eu = gdf.to_crs("EPSG:3035") # Europe Equal Area
281# For web visualization
282gdf_web = gdf.to_crs("EPSG:3857") # Web Mercator
283```

3. Use Appropriate Projections

# For local analysis (< 500km extent)
gdf_local = gdf.to_crs(gdf.estimate_utm_crs())

# For national/regional analysis
gdf_us = gdf.to_crs("EPSG:5070")  # NAD83 / Conus Albers
gdf_eu = gdf.to_crs("EPSG:3035")  # Europe Equal Area

# For web visualization
gdf_web = gdf.to_crs("EPSG:3857")  # Web Mercator
Raw lines 271 to 284
285### 4. Preserve Original CRS
287```python
288# Keep original as backup
289gdf_original = gdf.copy()
290original_crs = gdf.crs
292# Do analysis in projected CRS
293gdf_projected = gdf.to_crs(gdf.estimate_utm_crs())
294result = gdf_projected.geometry.buffer(1000)
296# Convert back if needed
297result = result.to_crs(original_crs)
298```

4. Preserve Original CRS

# Keep original as backup
gdf_original = gdf.copy()
original_crs = gdf.crs

# Do analysis in projected CRS
gdf_projected = gdf.to_crs(gdf.estimate_utm_crs())
result = gdf_projected.geometry.buffer(1000)

# Convert back if needed
result = result.to_crs(original_crs)
Raw lines 285 to 299
300## Common Pitfalls

Common Pitfalls

Raw lines 300 to 301
302### Mistake 1: Area in Degrees
304```python
305# WRONG: Area in square degrees
306gdf = gpd.read_file('data.geojson')
307area = gdf.geometry.area # Wrong!
309# CORRECT: Use projected CRS
310gdf_proj = gdf.to_crs(gdf.estimate_utm_crs())
311area_sqm = gdf_proj.geometry.area
312area_sqkm = area_sqm / 1_000_000
313```

Mistake 1: Area in Degrees

# WRONG: Area in square degrees
gdf = gpd.read_file('data.geojson')
area = gdf.geometry.area  # Wrong!

# CORRECT: Use projected CRS
gdf_proj = gdf.to_crs(gdf.estimate_utm_crs())
area_sqm = gdf_proj.geometry.area
area_sqkm = area_sqm / 1_000_000
Raw lines 302 to 314
315### Mistake 2: Buffer in Geographic CRS
317```python
318# WRONG: Buffer of 1000 degrees
319gdf['buffer'] = gdf.geometry.buffer(1000)
321# CORRECT: Project first
322gdf_proj = gdf.to_crs("EPSG:32610")
323gdf_proj['buffer_km'] = gdf_proj.geometry.buffer(1000) # 1000 meters
324```

Mistake 2: Buffer in Geographic CRS

# WRONG: Buffer of 1000 degrees
gdf['buffer'] = gdf.geometry.buffer(1000)

# CORRECT: Project first
gdf_proj = gdf.to_crs("EPSG:32610")
gdf_proj['buffer_km'] = gdf_proj.geometry.buffer(1000)  # 1000 meters
Raw lines 315 to 325
326### Mistake 3: Mixing CRS
328```python
329# WRONG: Spatial join without checking CRS
330result = gpd.sjoin(gdf1, gdf2, predicate='intersects')
332# CORRECT: Ensure same CRS
333if gdf1.crs != gdf2.crs:
334 gdf2 = gdf2.to_crs(gdf1.crs)
335result = gpd.sjoin(gdf1, gdf2, predicate='intersects')
336```

Mistake 3: Mixing CRS

# WRONG: Spatial join without checking CRS
result = gpd.sjoin(gdf1, gdf2, predicate='intersects')

# CORRECT: Ensure same CRS
if gdf1.crs != gdf2.crs:
    gdf2 = gdf2.to_crs(gdf1.crs)
result = gpd.sjoin(gdf1, gdf2, predicate='intersects')
Raw lines 326 to 337
338## Quick Reference
340```python
341# Common operations
343# Check CRS
344gdf.crs
345rasterio.open('file.tif').crs
347# Reproject
348gdf.to_crs("EPSG:32633")
350# Auto-detect UTM
351gdf.estimate_utm_crs()
353# Transform single point
354from pyproj import Transformer
355tx = Transformer.from_crs("EPSG:4326", "EPSG:32610", always_xy=True)
356x, y = tx.transform(lon, lat)
358# Create custom CRS
359from pyproj import CRS
360custom_crs = CRS.from_proj4(
361 "+proj=utm +zone=10 +ellps=WGS84 +datum=WGS84 +units=m +no_defs"
363```
365For more information, see:
366- [EPSG Registry](https://epsg.org/)
367- [PROJ documentation](https://proj.org/)
368- [pyproj documentation](https://pyproj4.github.io/pyproj/)

Quick Reference

# Common operations

# Check CRS
gdf.crs
rasterio.open('file.tif').crs

# Reproject
gdf.to_crs("EPSG:32633")

# Auto-detect UTM
gdf.estimate_utm_crs()

# Transform single point
from pyproj import Transformer
tx = Transformer.from_crs("EPSG:4326", "EPSG:32610", always_xy=True)
x, y = tx.transform(lon, lat)

# Create custom CRS
from pyproj import CRS
custom_crs = CRS.from_proj4(
    "+proj=utm +zone=10 +ellps=WGS84 +datum=WGS84 +units=m +no_defs"
)

For more information, see:

Raw lines 338 to 368