Cahill-Keyes C++20 implementation notes

Documentation index · Geometric context · Forward/reverse API · Bibliography

Scope and result

The implementation replaces the former per-point Node.js subprocess, shared temporary file, and fixed-height caches with an in-process C++20 forward projection and an octant-qualified reverse solver. Its geometric construction is derived from the coordinate-conversion core of Mary Jo Graça and Gene Keyes’s MegamapMaker-prep9.pl, including:

The C++ implementation is not required to reproduce the Perl program’s floating-point behavior. In particular, its line and circle intersections, intermediate precision, scale handling, and segment traversal intentionally diverge where the older techniques are ill-conditioned. The Perl output is a useful provenance and coarse-compatibility corpus, not a correctness oracle.

The public class remains a60::carto::ckproj and continues to implement a60::carto::projection_api. Its existing projection_base constructor and named presets remain available. New code can construct it directly from an a60::carto::frame.

The work was delivered in five stages:

  1. Native projection: translate the forward construction to C++20 and verify it against the Perl results and augment_carto_geo_specific anchors.
  2. Variable frames: derive every length and origin from frame.frame_area, enforce the projection’s 2:1 aspect ratio, and retain the named compatibility presets.
  3. Numerical hardening: normalize the internal scaffold, replace the unstable intersection formulae, test representable boundary neighborhoods, and remove generator coordinate perturbations.
  4. Documentation: record the geometry, formulas, implementation choices, usage, verification method, provenance, and bibliography in this documentation set.
  5. Reverse projection: undo the selected M-layout octant, solve the canonical piecewise construction with a bounded forward-residual search, and expose candidates through the projection-neutral runtime (API 3 now; originally delivered in API 2).

Code organization

Component Responsibility
cart0freak0-cahill-keyes.h Numeric forward projection, octant-qualified reverse solver, frame validation, projection_api adapter, screen-coordinate conversion, raster naming, and compatibility presets
cart0freak0-cahill-keyes-functions.h Scale- and offset-aware splitting of projected paths at wrapped frame edges
cart0freak0-cahill-keyes-slicing.h Carrier-frame viewport descriptors, exact-octant clipping, SVG slice wrappers, and verification
test-cahill-keyes-projection.cc Compatibility points, representable boundary neighborhoods, scale invariance, continuity, domain sweep, and invalid input
test-cahill-keyes-projection-api.cc Public API anchors, variable frames, invalid frames, raster paths, and compatibility construction
test-cahill-keyes-path-functions.cc Horizontal, vertical, corner, two-edge, variable-frame, stateful, and invalid path cases
test-cahill-keyes-slicing.cc Four-strip and exact-octant geometry, metadata, source references, physical units, and invalid carriers
test-forward-reverse-projection-api.cc Runtime round trips, 1,560 zone samples, registered seams, qualified poles, batches, outside points, and capability status

ck_native::forward_projection builds a dimensionless, long double scaffold with MG = 1 and stores the requested output altitude separately. Calls to operator() are stateless: they evaluate the canonical construction, assemble the octant, and apply the output scale only once at the end. Reverse calls are also stateless and evaluate the same forward construction during their residual search. No cache or external process is required.

Coordinate conventions

The two public layers deliberately use different argument and coordinate conventions:

Layer Call Axes
ck_native::forward_projection (longitude, latitude) centered Cartesian; x right, y up
projection_api::meridians_to_point_2d (latitude, longitude) map frame; x right, y down

Both geographic arguments are degrees. The native class accepts finite latitudes in [-90, 90] and finite longitudes in [-180, 180]; values outside those closed intervals throw std::invalid_argument.

Frame-derived scale

Let a requested frame have width W and height H. A valid Cahill-Keyes frame must satisfy:

W > 0
H > 0
W = 2H
W and H are finite

is_cahill_keyes_frame() compares W with 2H using only a roundoff-sized tolerance:

tolerance = 16 * epsilon(double) * max(W, 2H)
valid = abs(W - 2H) <= tolerance

This admits arithmetic noise, not approximate 2:1 aspect ratios. For example, 2000.001 × 1000 is rejected.

The geometric scale represented by a requested frame is:

MGout = H / 2 = W / 4
qout  = MGout / 10000 = H / 20000 = W / 40000

The original M-layout spans 40,000 units when MG = 10,000, so these formulas fit it exactly to the frame width. The implementation does not repeatedly multiply each construction value by qout. It instead evaluates an equivalent canonical scaffold with MG = 1 and these dimensionless constants:

Quantity Value
MA 0.094
ordinary latitude spacing 0.01 per degree
polar latitude spacing 0.0104 per degree
AP75 0.156
AP73 0.176

After octant assembly, the canonical coordinate is multiplied by MGout. This makes the geometric decisions independent of page or raster size and eliminates the former scale-dependent intersection classification.

The standard frame constructor centers the map at (W/2, H/2). After native projection returns (x, y), the API result is:

X = W / 2 + x
Y = H / 2 - y

The legacy projection_base constructor preserves explicitly supplied origins, so its general form is X = longitude_zero_x + x and Y = latitude_zero_y - y.

Geographic normalization

Raster registration adjustment

ckproj first adds one degree to longitude and wraps values greater than 180°:

λ' = λ + 1°
if λ' > 180°, λ' = λ' - 360°

This is a cartofreako/Visionscarto raster registration adjustment, not part of the Cahill-Keyes construction. It preserves the behavior of the raster assets and the geographic anchors already used by the project. The native class itself implements the canonical longitude boundaries without this offset.

Octant and half-octant coordinates

For native longitude λ' and latitude φ, LLtoMP from the Perl source is represented by:

o0 = floor((λ' + 200) / 90) + 1
u  = λ' + 200 - 90(o0 - 1) - 45
s  = -1 when u < 0, otherwise +1
m  = abs(u)
p  = abs(φ)

The quotient is non-negative over the validated longitude domain, so the C++ integer conversion has the same result as floor. If o0 is 5 it wraps to octant 1. Southern latitudes remap the four northern sectors as follows:

1 -> 6
2 -> 7
3 -> 8
4 -> 5

The result has 0 ≤ m ≤ 45, 0 ≤ p ≤ 90, a side sign s, and an octant number 1–8. In the native formula, the longitude sectors are:

Octant sector Half-open native longitude interval
1 / 6 [-180, -110) and [160, 180]
2 / 7 [-110, -20)
3 / 8 [-20, 70)
4 / 5 [70, 160)

Because the public API applies the one-degree registration adjustment, its visible seam longitudes are -111°, -21°, 69°, and 159°.

Numeric geometry primitives

Geographic inputs and public outputs remain double, but the complete dimensionless construction uses long double, std::numbers::pi_v<long double>, the long double trigonometric overloads, and std::fma where it reduces cancellation. Only the final assembled coordinate is scaled and rounded to double.

long double supplies the widest standard floating-point type available on the target; the algorithm does not rely on extra precision alone. Canonical scaling and the stable geometric formulations are required even on targets where long double and double have the same representation.

For points P=(Px,Py) and Q=(Qx,Qy), distance is:

d(P,Q) = hypot(Px - Qx, Py - Qy)

Interpolation a distance l along a segment of total construction length L is:

interpolate(l, L, P, Q) = P + (Q - P) * l/L

Some calls intentionally use a signed L; this preserves the source construction’s direction near the 73° transition at the outer edge. A zero total length is rejected with std::domain_error.

For two lines through P1, P2 at angles θ1, θ2, use unit direction vectors r = (cos θ1, sin θ1) and s = (cos θ2, sin θ2). With the two-dimensional scalar cross product ×, their intersection is:

t = (P2 - P1) × s / (r × s)
intersection = P1 + tr

This avoids tangent slopes, their vertical-line singularity, and division by a difference of two rounded tangents. The construction rejects only exactly parallel direction vectors; the m = 0 meridian has its explicit analytic limit.

For a circle with center C, radius r, and a segment from P0 to P1, set d = P1 - P0 and project the center onto the segment’s supporting line:

a  = d·d
t0 = (C - P0)·d / a
e  = P0 + t0*d - C
h² = r² - e·e
dt = sqrt(h² / a)
t  = t0 ± dt

Only factors in [0,1] are segment intersections. Roundoff tolerances are derived from long double epsilon and the magnitudes of the radial terms and segment factors. A negative h² or an endpoint factor is clamped only when it falls inside that error bound. Accepted roots are ordered along the segment.

This deliberately replaces the Perl routine’s expanded quadratic coefficient, naïve quadratic roots, exact discriminant sign test, and exact endpoint tests. Those choices were responsible for reachable false “no intersection” results near the 45° half-octant boundary.

Reference half-octant construction

Preliminary scaffold

Let the canonical scaffold use S = MG = 1 and q = 1/10000. The primary points are:

M = (0, 0)
G = (S, 0)
A = (940q, 0)
N = (S, S tan 30°)

The remaining fixed points follow from intersections and scaled distances:

B = line(M, 30°) ∩ line(A, 45°)
D = N + (M - N) * MB/MN
F = (S, Ny - MB)
E = (Nx - MA sin 30°, Ny - MA cos 30°)
Δm = (GF + AB) / 45

U = A + AP73 * (cos 30°, sin 30°)
T = line(U, -60°) ∩ line(B, 30°)

Δm is the distance advanced along the piecewise equator for each degree of half-octant meridian.

The supple-zone circle is constrained to have center C = (√3 Cy, Cy) and to pass through D and a constructed point V at m = 29°, p = 15°. Equating the two squared radii gives:

Cy = ((V - D)·(V + D))
     / (2 * (√3(Vx - Dx) + (Vy - Dy)))
Cx = √3 Cy
R  = distance(C, D)

The dot-product difference-of-squares form avoids subtracting two independently rounded squared norms. All these values are computed once in the native projection constructor.

Meridian framework

For half-meridian m, the equatorial point Q(m) advances by l = Δm * m along G→F→E: first vertically from G to F, then by interpolation from F to E.

Two joints divide the constructed meridian into torrid, middle, and frigid portions:

Jt(m) = line(M, 2m/3) ∩ line(Q(m), m/3)
Jf(m) = line(A, m) ∩ line(M, 2m/3)

At m = 0, Jf uses the limiting center-line point (Ax + AB, 0). The segment lengths used for latitude equisection are:

Lt = distance(Q, Jt)
Lm = distance(Jt, Jf)
Lf = signed distance(Jf, P73)

For m ≤ 30°, P73 lies on the circle centered at A with radius 1760q. For m > 30°, it is the intersection of the -60° line through T and the m-degree line through Jf. Near 45° the source construction can put the transition in the middle segment; the implementation preserves that case with a negative Lf. P75 lies on the circle centered at A with radius 1560q.

A–L zone dispatch

The conditions are evaluated in the following order. This defines which geometric formula owns an exact boundary; it is not a promise to reproduce the source program’s floating-point branch classification:

Zone Ordered condition Construction
A m = 0, p ≥ 75 Center line, 104q units per degree from the pole
B m = 0, p < 75 Center line, 100q units per degree from G
C m != 0, p ≥ 75 Polar circle centered at A, radius 104q(90-p)
D p = 0 Equatorial point Q(m)
E p ≥ 73, m ≤ 30 Circle centered at A, radius AP75 + 100q(75-p)
F m = 45, p ≤ 15 Linear interpolation E→D
G m = 45, 15 < p ≤ 73 Linear interpolation D→T
H m = 45, 73 < p < 75 Equisection through P75→B→P73
I m ≤ 29 Equisection over Q→Jt→Jf→P73
J m > 29, p ≥ 73 Frigid supple interpolation through P75, Jf, and P73
K m > 29, p ≤ 15 Torrid supple interpolation to the computed 15° path length
L m > 29, 15 < p < 73 Middle supple interpolation from the 15° path length to P73

Zones I, K, and L walk a cumulative length across the constructed segments, then interpolate within whichever segment contains that length. Zone I, for example, uses:

l = p * (Lt + Lm + Lf) / 73

For m > 29°, the 15° path length is found by traversing the meridian in geographic order, Q→Jt→Jf. The implementation first intersects the circle with Q→Jt, then with Jt→Jf, and converts the accepted segment factor directly to cumulative meridian length. A joint hit has the same cumulative length from either segment. This ordering intentionally differs from the Perl routine’s middle-segment-first search and states the desired geometry directly: parallel 15 is the first circle crossing encountered poleward from the equator. Failure to intersect either segment is a construction error and throws std::domain_error.

Octant assembly

After zone construction, the half-octant sign is applied as (x,y) = (x,sy). The two supported rotations are implemented directly:

rotate -60°:
  x' =  x cos60° + y sin60°
  y' = -x sin60° + y cos60°

rotate -120°:
  x' = -x cos60° + y sin60°
  y' = -x sin60° - y cos60°

North octants use those rotations directly. South octants first reflect x = 2MG - x. The M-layout transform table is:

Octant Reflection Rotation Translate x
1 none -120° -MG
2 none -60° -MG
3 none -120° +MG
4 none -60° +MG
5 x = 2MG - x -60° +MG
6 x = 2MG - x -120° -MG
7 x = 2MG - x -60° -MG
8 x = 2MG - x -120° +MG

Finally, every octant is translated vertically by MG sin60°.

Octant-qualified reverse

The reverse contract is deliberately not a single unqualified (x,y) -> (longitude,latitude) function. The interrupted M-layout can place a boundary on more than one planar copy, so the native method accepts one official assembly octant and the projection-neutral runtime either honors a caller’s nativeCell or enumerates all eight cells.

For a selected octant, inverse() divides out MGout, removes the common vertical translation, reverses the octant’s -60° or -120° rotation, and undoes the southern x = 2MG - x reflection when present. The result is a signed canonical half-octant coordinate. Its y sign chooses the west/east half; the remaining magnitude must be explained by one pair:

0 <= m <= 45
0 <= p <= 90

The forward is continuous but piecewise at the A–L zone boundaries. Rather than differentiate across those joints, the reverse evaluates a five-degree m,p lattice augmented by m=29 and p=73, chooses the smallest-residual basin, and applies a bounded two-dimensional pattern search. A result is accepted only when the selected octant’s authoritative forward transform returns to the requested point within tolerance_pixels. This makes the forward residual, rather than a copied external inverse, the numerical oracle.

The geographic sector is reconstructed from the assembly-octant table, and the public runtime subtracts the one-degree raster registration before returning longitude. Runtime cells 0..3 represent the four northern sectors; cells 4..7 are their southern counterparts. Equator points, outer 45° meridians, and poles carry boundary=true. Every meridian converges at a pole, so a cell-qualified polar result uses that octant’s center longitude as a stable representative; only its latitude and residual are geographically meaningful there.

This capability is retained unchanged in runtime API 3 and advertises inverseMode: "face-qualified"; geometry command buffers remain ABI 1. See the projection-neutral forward/reverse contract for statuses, batches, WebAssembly, workers, D3, and TypeScript behavior.

Reverse implementation plan and completion record

The Stage 14 implementation followed this bounded plan:

  1. Preserve the public contract. Keep the point API independently versioned from geometry ABI 1, add no sentinel coordinates, and promote capability metadata only after a checked candidate path exists. Complete.
  2. Invert assembly before geography. Convert screen coordinates to the centered scaffold and exactly reverse the selected octant’s translation, rotation, and southern reflection. Complete.
  3. Solve the authoritative construction. Reuse meridian_parallel_to_xy() as the oracle over bounded (m,p) rather than maintaining a second set of inverse A–L formulas. Complete.
  4. Restore registration and topology. Reconstruct the registered sector longitude, undo the public one-degree offset, retain runtime-cell identity, and explicitly classify cuts and poles. Complete.
  5. Verify native numerics. Cover compatibility anchors, every assembly octant, both half-octants and hemispheres, all piecewise transitions, unqualified enumeration, exact public seams, poles, batches, invalid input, and forward residuals. Complete.
  6. Verify consumer parity. Rebuild the all-projection and compatibility WebAssembly modules, then exercise Node, typed arrays, D3, main-thread browser calls, and module-worker calls. Complete.
  7. Complete composition semantics. Reuse this checked solver for Star-X carrier component 0; give its fixed 60°S unified Antarctic cap component 1, explicit candidate/component status, and a separate bounded inverse. Complete. See the Star-X forward/reverse consequence.

Issues found during implementation

Public construction and usage

The preferred factory takes the frame that owns the requested frame.frame_area:

const a60::carto::frame::area dimensions {4224, 2112};
const a60::carto::frame map_frame {dimensions};

const auto projection = a60::carto::make_cahill_keyes_projection(
  map_frame, "visionscarto-cahillkeyes-44x22.300");

const auto [x, y] = projection.meridians_to_point_2d(
  48.8575, 2.3514); // latitude, longitude: Paris

The equivalent direct construction is:

const a60::carto::ckproj projection {map_frame, "optional-raster-name"};

The raster name is carried in projection_base::name and used by image_filename(). It does not load a file and does not affect numeric output. Existing named objects such as ck_1xengc, ck_2xengc, and ck_44x22 are compatibility presets built on the same variable-frame constructor.

Invalid frame dimensions throw std::invalid_argument during construction. Geographic range errors throw the same exception during projection. Internal degenerate geometry uses std::domain_error.

Projected path seam handling

The forward API maps one geographic point at a time. Connecting its results without considering the octahedral cuts can draw a long false line across the map. The separate path utilities consume already projected screen points and split those paths at discontinuities:

#include "a60-carto.h"
#include "cart0freak0-cahill-keyes-functions.h"

a60::vrange geographic {
  {21.3, -157.8}, // latitude, longitude
  {-18.1, 178.4},
};

a60::vrange projected;
for (const auto& location : geographic)
  projected.push_back(a60::carto::ckwecarto_44x22.to_point_2d(location));

const a60::vvranges segments = a60::carto::fold_path_edges(
  a60::carto::ckwecarto_44x22, projected);
// Render every element of segments as a separate SVG path.

fold_path_edges() is the preferred interface. It does not modify its input, returns no segments for an empty input, and otherwise returns only nonempty segments. Original points stay ordered. Each split adds an exit point on one frame edge and a corresponding entry point on the opposite edge.

minimize_path_distance() remains as a compatibility interface for callers that render one segment per iteration. It returns the next continuous segment and mutates its vrange& argument. After a split, that argument contains the unprocessed suffix beginning at the opposite-edge entry point. When no split remains, the function returns the final segment and clears the argument:

while (!projected.empty())
  {
    const a60::vrange segment = a60::carto::minimize_path_distance(
      a60::carto::ckwecarto_44x22, projected);
    // Render segment.
  }

Repeated calls produce exactly the same segmentation as fold_path_edges().

The shared generation path applies this fold automatically for Cahill-Keyes. It tests each original adjacent projected-point pair before generic native-cell splitting, because splitting first would discard the relationship needed to extend an exit and entry to opposite carrier edges. A detected outer-frame wrap is rendered through fold_path_edges(); all other pairs retain the shared native-cell transition logic used by the other projections. This is the path used by astronomy reference curves, graticules, Natural Earth geometry, orbital tracks, atmosphere contours, and network paths.

Native-cell classification uses the same operations as the forward projection: the public one-degree longitude registration is performed in double, then the registered value is promoted to long double before the native octant formula is evaluated. This ordering matters immediately beside the visible seams at -111°, -21°, 69°, and 159°. A former add-360/subtract-360 normalization could round a one-ULP neighbor onto the seam for classification while the forward projection still selected the opposite planar copy.

Detection and edge-intersection formulas

Let the projection frame have width W, height H, and drawing origin (ox, oy). Its screen-space edges are:

L = ox       R = ox + W
T = oy       B = oy + H

An adjacent pair is a horizontal wrap candidate when its points occupy the opposite outer quarters and abs(xc - xp) >= W/2. It is a vertical candidate when its points occupy opposite halves and abs(yc - yp) >= H/3. These thresholds retain the historical Cahill-Keyes M-layout classification while deriving all lengths from the current variable frame.

For a left-to-right wrap, the current point is temporarily unwrapped as xc' = xc - W and the exit edge is L; the reverse direction uses xc' = xc + W and R. Top/bottom wrapping applies the same construction with H. For one coordinate s, its unwrapped endpoint e, and the selected edge b, the segment parameter is:

t = (b - s) / (e - s)

The other coordinate is linearly interpolated at t. The result is clamped only to absorb floating-point roundoff at the frame boundary. If both axes wrap, both endpoint coordinates are unwrapped and the earlier intersection is emitted first. Equal intersection parameters pass through paired opposite corners in one split; unequal parameters create the necessary middle segment between two different frame edges.

The frame origin may be positive or negative, and dimensions may be any valid 2:1 size. The helper independently checks the aspect ratio so an incompatible cartography fails before path processing. Invalid dimensions/origins and non-finite projected points throw std::invalid_argument. Large jumps that do not match an opposite-edge classification are preserved unchanged rather than losing a point.

Carrier slicing and enlargement

The full Cahill-Keyes projection and a published enlargement have different frame rules. The complete world is first projected on a finite, positive 2:1 carrier. A slice is then a viewport into those projected coordinates; it is not another projection and is not required to be 2:1.

For a carrier-space view (x0, y0, w, h), the slice descriptor records an output frame of w × h with origin offsets (-x0, -y0). Its local transform is only:

Xslice = Xcarrier - x0
Yslice = Ycarrier - y0

This preserves scale and geographic registration. Enlargement happens because the smaller viewport receives its own physical page or raster resolution, not because its coordinates are stretched. Passing an 11×22 strip to ckproj would be an architectural error: the projection correctly rejects that frame, whereas the slice writer correctly accepts it as a viewport.

Two descriptor families are generated:

  1. make_four_slices() divides the carrier into four equal-width, full-height rectangles. A 44×22 world therefore produces four 11×22 pages containing official octant pairs (1,6), (2,7), (3,8), and (4,5).
  2. make_eight_slices() projects a sampled outline for each official octant, computes its natural axis-aligned bounds, and uses the outline as an SVG clip path. These eight pages have varying ratios and are semantic faces, not cells of a rectangular 4×2 grid.

The four ordered latitude ranges retained in descriptor metadata are presentation context only. Geographic filtering belongs before projection; changing a projected viewport cannot turn its irregular face content into a latitude band.

The SVG wrapper keeps viewBox values in unitless carrier coordinates and expresses output width and height in inches. It references the complete Earth SVG through <use>, avoiding twelve duplicated copies of a large vector document. The sibling master SVG is therefore required by an SVG slice, while the exported PDF and PNG are self-contained. The complete generation, historical printing context, targets, and raster behavior are documented in the generation guide.

Provenance, compatibility, and intentional divergence

The C++ implementation ports the original mathematical subroutines, not the Perl program’s interactive “Blocks,” file conversion, coastline generation, or alternative area-code layouts. Corresponding design choices are:

These are intentional numerical divergences from MegamapMaker-prep9.pl. Coordinate agreement with the Perl samples is kept as a coarse regression signal, but exact last-bit agreement, exception behavior, root preference, and scale-dependent branch behavior are explicitly not compatibility requirements.

Resolved near-cut numerical defect

The former direct port could throw Cahill-Keyes parallel 15 misses its meridian for valid coordinates a few representable values inside a 45° half-octant boundary. One reproduced case was public longitude approximately 68.9999999999998°, latitude 20°: an 11-unit scaffold threw while 528-, 1056-, and 3300-unit scaffolds succeeded. The exact 69° cut also succeeded because it selected a separate equality branch. A geographic construction cannot legitimately change intersection existence with output size, so this was classified as a forward-projection defect rather than a source-compatibility requirement.

The source of the defect was a combination of scale-dependent intermediate coordinates, cancellation in the expanded circle quadratic, and exact tests of the discriminant and segment endpoints. Generator path bisection reaches these neighborhoods routinely even though a half-degree coordinate sweep does not.

Before the numerical correction, Orbital Technosphere generation worked around the defect by retrying failed points with latitude and longitude offsets. That workaround could move a coordinate across the very cut being resolved, concealed unrelated projection exceptions, and made output depend on retry order. It has been removed: orbital markers, observer sites, labels, and reference paths now use the same exact-coordinate projection path as every other generator.

Ocean banding and the Star-X lower-left equatorial notch were different failures: they came from SVG ring closure, polygon clipping, and paint order, not the point transform. Their fixes remain documented under Natural Earth geometry processing.

There is no subprocess, shell command, JavaScript runtime, temporary output file, or global coordinate cache in the forward path.

Verification

Run all standalone tests under strict C++20 warnings:

make check

During Stage 14 development, the reverse-specific unattended checks are:

make check-forward-reverse-projection-api
make check-wasm-projections
make check-wasm-projections-browser

The checks cover:

The path-function executable was also run with AddressSanitizer and UndefinedBehaviorSanitizer. Leak detection was disabled because it is not supported under the execution environment’s ptrace; address and undefined behavior checks passed. The projection, frame, and path headers were also compiled together against the neighboring real a60 and izzi definitions. The tests otherwise contain small compatible fixtures so the implementation can be built without the rest of the application dependency graph.

Invariants and limitations

Provenance and licensing

The native implementation is derived from assets.static/cahill-keyes/MegamapMaker-prep9.pl, dated 2012-03-15 and attributed in its header to Mary Jo Graça, with Gene Keyes credited for the projection design and calculations. That header permits modification, reproduction, and redistribution for non-commercial use with attribution to both Graça and Keyes, and directs commercial users to Gene Keyes. It does not name a particular Creative Commons license variant.

That upstream notice should be reviewed alongside this repository’s license before redistribution; do not assume that the repository license erases the source-specific attribution, non-commercial-use, or contact language. The bibliography records the source and design history.

Documentation index · Geometric context · Forward/reverse API · Bibliography