Skip to content

Coordinate Systems

Jangmyun edited this page Aug 17, 2026 · 2 revisions

Coordinate Systems

CesiumJS renders on a WGS84 globe in ECEF (Earth-centred, Earth-fixed) meters. COPC point coordinates almost never arrive that way — they're in a projected CRS (a UTM zone, a state plane, a national grid), often in feet, and sometimes with a separate vertical datum. copcesium's job is to turn those into lon/lat/height and then ECEF, correctly and automatically.

The detection flow

src/crs/detectCrs.ts runs on every load(), reading the COPC file's WKT VLR (exposed by copc.js as copc.wkt):

  1. Extract the horizontal CRS. If the WKT is a compound COMPD_CS["…", PROJCS[…], VERT_CS[…]], the inner PROJCS/GEOGCS (or WKT2 PROJCRS/GEOGCRS) block is pulled out by bracket counting. proj4 can't parse COMPD_CS, so this step is essential.
  2. Geographic short-circuit. If the horizontal CRS is already geographic (GEOGCS/GEOGCRS/GEODCRS), the data is lon/lat degrees — return EPSG:4326 directly.
  3. Feed the WKT straight to proj4. proj4.defs(name, crsWkt) — proj4 parses most WKT1 dialects natively. This is the primary path and needs no lookup table.
  4. Fallback: EPSG code from the WKT. If proj4 can't parse the dialect (common with WKT2), the ID["EPSG", n] / AUTHORITY["EPSG","n"] code is extracted and resolved against a built-in offline table (src/crs/epsgTable.ts, looked up via src/crs/projections.ts).
  5. Give up gracefully. If nothing resolves, return null and let the caller fall back to the default or an explicit override.

The EPSG code in step 4 is deliberately searched only within the extracted horizontal block, not the whole string — otherwise a compound CRS's vertical datum code (e.g. NAVD88) could be mistaken for the horizontal CRS.

Units — the part that silently breaks clouds

proj4 applies the horizontal linear unit to X/Y, but Z is passed through untouched. A file in US survey feet whose heights aren't scaled will float thousands of meters too high. copcesium reads the WKT's linear/vertical unit and exposes it as:

  • zFactor — multiplies every height into meters (feet → 0.3048, US survey foot → 0.30480061…).
  • xyFactor — the XY unit in meters, used for bounding-sphere sizing (so LoD math stays correct in feet-based CRSes).

Both are auto-detected from the WKT vertical/linear unit, and — importantly — they still apply even when you override proj/projDef, because a compound CRS's vertical unit can differ from the horizontal CRS you're supplying. To set them yourself, pass zFactor/xyFactor explicitly; an explicit value always wins over detection.

geoidOffset — orthometric vs. ellipsoidal height

WGS84 heights are ellipsoidal; many datasets store orthometric heights (above a geoid like NAVD88 or EGM2008). The two differ by the local geoid separation — often tens of meters. copcesium doesn't ship a geoid model, so if your cloud sits consistently above or below the terrain, pass a constant:

{ geoidOffset: -20 } // meters added to every height

This is a single global offset, not a per-point geoid grid — good enough when the dataset covers a limited area (the separation varies slowly).

Overriding detection

Pass proj + projDef when the WKT is missing, truncated, or an unrecognized dialect:

const dataSource = await CopcDataSource.load(url, viewer, {
  proj: 'EPSG:2992', // Oregon State Plane North (ft)
  projDef:
    '+proj=lcc +lat_1=43 +lat_2=45.5 +lat_0=41.75 +lon_0=-120.5' +
    ' +x_0=399999.9999999999 +y_0=0 +datum=NAD83 +units=ft +no_defs',
  geoidOffset: -20,
});

If proj4 already knows the code (it ships EPSG:4326 and EPSG:4269 only), projDef can be omitted. For anything else, supply the proj4 string — grab it from epsg.io (the "proj4" export on any CRS page).

The fallback table — what it is and isn't

src/crs/epsgTable.ts is a hand-curated offline EPSG → proj4 map, looked up through src/crs/projections.ts's lookupEpsg(). It exists so detection works without network calls to epsg.io, and only for step 4 above (when proj4's own WKT parsing doesn't recognize a dialect). It covers:

  • WGS84 / NAD83 / ETRS89 UTM zones (generated in a loop, not hand-listed)
  • US State Plane (NAD83, both meter and foot variants)
  • Korean CRSes (Korea 2000 belts 51855188, unified 5179, legacy Bessel 51745177)
  • Japanese (JGD2000/2011 plane-rectangular + UTM), Australian (GDA94/2020 MGA), and common European national grids

If your CRS isn't in the table and proj4 can't parse its WKT, detection returns null — pass proj/projDef explicitly. Because the primary path is "feed WKT to proj4," a CRS absent from the table still works fine as long as its WKT is a dialect proj4 understands.

Debugging coordinates

  • Cloud is on the globe but in the wrong country → wrong horizontal CRS. Check the file's WKT; override proj/projDef.
  • Cloud is in the right place but floating / underground → wrong zFactor (feet not scaled) or a geoid/ellipsoid mismatch (geoidOffset).
  • Cloud is a tiny dot or absurdly huge → wrong xyFactor, usually a feet CRS not detected.
  • Nothing renders and the console shows non-finite coordinate errors → proj4 produced Infinity for out-of-domain input; the source CRS is likely wrong for this data.

See Troubleshooting for the full checklist.

Clone this wiki locally