Turns survey points, contours, alignments, pads, corridor lines, pipes and structures into GIS features with their attribute values, in grid coordinates.
How the precision is kept
Attribute values are passed on as 64-bit numbers without rounding in this file. Curved alignments are exported as chords of at most 0.5° of turn, and a pad's area is a shoelace sum on project coordinates, so those two values carry the limits of their method.
Calculations under this policy
6 calculations in 1 source file. The code is read from the released app when this page loads. It is the calculation people run, not a copy kept for this page.
The area is the shoelace sum on the project coordinates as they are (ringArea in gis-geom.js), so with coordinates in the millions it keeps fewer digits than the corners do. The elevation is the pad's stored value passed through Number.
An alignment's exported linework and its LENGTH, STA_START and STA_END attributes.
chords of at most π ÷ 360 (0.5°) of turn
The LENGTH attribute is the alignment's true length; the exported line is a chain of chords, so the line's own measured length is slightly shorter on curves. Vertices are on the curve.
Values are passed on unrounded. A missing invert is written as an empty attribute, but the line's vertex takes z = 0 in its place, so the geometry and the attribute disagree for such a pipe.
The surface's outline loops arranged as outer rings and holes for the boundary export.
Loops are sorted by absolute shoelace area and nested by testing each loop's first vertex against the larger ones. The same limits as the ring area and the inside test apply.
gis-export.js
lines 64 to 75
CivilCraft 0.8.1
/** Loops (each [{x,y}]) nested by containment: even depth = outer, odd = hole of the loop around it. */
function nest(loops) {
const rings = loops.map((l) => close(l.map(P2))).filter((r) => r.length >= 4).map((r) => ({ r, a: Math.abs(ringArea(r)) })).sort((a, b) => b.a - a.a);
const polys = [];
for (let i = 0; i < rings.length; i++) {
let parent = -1;
for (let j = i - 1; j >= 0; j--) if (pointInRing(rings[i].r[0][0], rings[i].r[0][1], rings[j].r)) { parent = j; break; }
rings[i].depth = parent < 0 ? 0 : rings[parent].depth + 1;
if (rings[i].depth % 2 === 0) { rings[i].poly = [rings[i].r]; polys.push(rings[i].poly); } else rings[parent].poly.push(rings[i].r);
}
return polys;
}
Features in grid coordinates
64-bit
Every exported feature's coordinates passed through the project's grid transform, with the N and E attributes of survey points set to match.
For survey points the N and E attributes are taken from the transformed coordinates, so attribute and geometry are the same numbers. Lengths, areas and elevations in attributes stay in project units and are not rescaled by the grid transform.
gis-export.js
lines 153 to 168
CivilCraft 0.8.1
export function exportGroups(project, keys, { toGrid = (x, y) => [x, y] } = {}) {
const kinds = exportKinds(project), used = new Set(), out = [];
for (const key of keys) {
const k = kinds.find((x) => x.key === key), b = build(project, key);
if (!k || !b || !b.features.length) continue;
const features = b.features.map((f) => {
const g = mapXY(f.g, toGrid);
if (!b.coordFields) return { g, p: f.p };
const p = Object.create(null); Object.assign(p, f.p);
if (g) { p.E = g.c[0]; p.N = g.c[1]; }
return { g, p };
});
out.push({ key, label: k.label, kind: key.startsWith('gis:') ? safeName(k.file, new Set()) : k.file, file: safeName(k.file, used), geom: k.geom, hasZ: b.hasZ && features.some((f) => f.g && (f.g.t === 'pt' ? f.g.c.length > 2 : true)), fields: b.fields, features });
}
return out;
}
Elevations to meters for GeoJSON and KML
64-bit
The factor applied to elevations when the export is in longitude and latitude.
z in meters = z × meters per project unit
One multiplication by the factor from crs.js for the project system's unit (1, 0.3048 or 1200/3937). A unit not in that table is treated as 1.
gis-export.js
lines 209 to 227
CivilCraft 0.8.1
/** GIS › Export in one call (the page and the test both use it): the chosen kinds in grid coordinates, written as
* `format`; GeoJSON needs a project system (refused with the reason otherwise); without one, shapefiles and
* GeoPackages carry the project's own numbers and no system (a note says so). */
export async function exportProject(project, keys, format, { stem = 'site' } = {}) {
const crs = project.state.crs;
if (format === 'geojson' && !crs) throw new Error(t('gexp.geojson.need'));
if ((format === 'kml' || format === 'kmz') && !crs) throw new Error(t('kml.need.crs')); // 0.4 V6
const f = crs ? frame(crs) : null;
const groups = exportGroups(project, keys, f ? { toGrid: f.toGrid } : {});
const prj = crs ? await prjFor(crs) : null;
const toLonLat = crs ? transformer(crs, null) : null;
const k = crs ? CRS_METRES[crs.unit] || 1 : 1;
const r = await writeExport(groups, { format, prj, toLonLat, zToMetres: (z) => z * k, stem, srs: srsFor(crs, prj) });
if (!crs) r.notes.unshift(t('gexp.nocrs'));
else if (!['geojson', 'kml', 'kmz'].includes(format) && crs.code == null && !prj) r.notes.unshift(t('gexp.nocode'));
if (crs?.local && !['geojson', 'kml', 'kmz'].includes(format)) r.notes.push(t('gexp.grid', { crs: crs.name }));
r.groups = groups;
return r;
}
In detail
Method
Each kind of object is built as features, every x and y goes through the project's grid transform, and the chosen writer (shapefile, GeoJSON, GeoPackage, KML) stores them. Elevations for GeoJSON and KML are multiplied by meters per project unit.
Number types
64-bit numbers.
Limits
The shapefile, GeoJSON, GeoPackage and KML writers are other files and may round on their own; they were not part of this review. A pipe with no invert is given z = 0 in its geometry while its attribute is empty.
Approach: 64-bit, stated limit
The arithmetic is 64-bit, but the method or the data sets the accuracy: a solver tolerance, a mesh, a step size, a datum shift, a table. The limit is stated where the result is shown.