feat(map): add functional sector grid v2
This commit is contained in:
@@ -1,6 +1,10 @@
|
||||
const EPSILON = 1e-9;
|
||||
const WGS84_EQUATORIAL_RADIUS_METERS = 6_378_137;
|
||||
const WGS84_FLATTENING = 1 / 298.257223563;
|
||||
const WGS84_ECCENTRICITY_SQUARED = WGS84_FLATTENING * (2 - WGS84_FLATTENING);
|
||||
const WGS84_ECCENTRICITY = Math.sqrt(WGS84_ECCENTRICITY_SQUARED);
|
||||
export const MAX_LOCAL_GRID_INDEX = 512;
|
||||
export const MAX_GRID_CHILD_PAGE_SIZE = 10_000;
|
||||
|
||||
const finite = (value, fallback) => Number.isFinite(Number(value)) ? Number(value) : fallback;
|
||||
const clamp = (value, minimum, maximum) => Math.min(maximum, Math.max(minimum, value));
|
||||
@@ -8,6 +12,42 @@ const canonicalZero = (value) => Object.is(value, -0) ? 0 : value;
|
||||
const signedIndex = (value) => value >= 0 ? `+${value}` : String(value);
|
||||
const decimalToken = (value, digits) => Number(value).toFixed(digits).replace("-0.", "0.");
|
||||
|
||||
const positiveNumber = (value, errorCode) => {
|
||||
const number = Number(value);
|
||||
if (!Number.isFinite(number) || number <= 0) throw new Error(errorCode);
|
||||
return number;
|
||||
};
|
||||
|
||||
const safeInteger = (value, errorCode = "grid_index_must_be_safe_integer") => {
|
||||
const number = Number(value);
|
||||
if (!Number.isSafeInteger(number)) throw new Error(errorCode);
|
||||
return canonicalZero(number);
|
||||
};
|
||||
|
||||
const integerMultiple = (outer, inner, errorCode) => {
|
||||
const ratio = outer / inner;
|
||||
const rounded = Math.round(ratio);
|
||||
if (!Number.isSafeInteger(rounded)
|
||||
|| rounded < 1
|
||||
|| Math.abs(ratio - rounded) > EPSILON * Math.max(1, Math.abs(ratio))) {
|
||||
throw new Error(errorCode);
|
||||
}
|
||||
return rounded;
|
||||
};
|
||||
|
||||
function childPageRange(totalCount, options) {
|
||||
const total = safeInteger(totalCount, "grid_child_count_must_be_safe_integer");
|
||||
const offset = safeInteger(options?.offset ?? 0, "grid_child_offset_must_be_safe_integer");
|
||||
if (offset < 0) throw new Error("grid_child_offset_must_be_non_negative");
|
||||
const remaining = Math.max(0, total - offset);
|
||||
const limit = options?.limit == null
|
||||
? remaining
|
||||
: safeInteger(options.limit, "grid_child_limit_must_be_safe_integer");
|
||||
if (limit < 1 && remaining > 0) throw new Error("grid_child_limit_must_be_positive");
|
||||
if (limit > MAX_GRID_CHILD_PAGE_SIZE) throw new Error("grid_child_page_limit_exceeded");
|
||||
return { start: Math.min(offset, total), end: Math.min(total, offset + Math.max(0, limit)) };
|
||||
}
|
||||
|
||||
export function normalizeLongitudeDegrees(value) {
|
||||
const longitude = finite(value, 0);
|
||||
return canonicalZero(((longitude + 180) % 360 + 360) % 360 - 180);
|
||||
@@ -81,6 +121,360 @@ export function localParentSector(address, childStepMeters, parentDefinition) {
|
||||
}, parentDefinition);
|
||||
}
|
||||
|
||||
function localHierarchyMetrics(definition) {
|
||||
const stepMeters = positiveNumber(definition?.stepMeters, "grid_step_must_be_positive");
|
||||
const tileSizeMeters = positiveNumber(definition?.tileSizeMeters, "grid_tile_size_must_be_positive");
|
||||
const minorPerMajor = integerMultiple(
|
||||
tileSizeMeters,
|
||||
stepMeters,
|
||||
"grid_tile_size_must_be_integer_multiple_of_step",
|
||||
);
|
||||
return { stepMeters, tileSizeMeters, minorPerMajor };
|
||||
}
|
||||
|
||||
function localMajorDefinitionToken(definition, metrics = localHierarchyMetrics(definition)) {
|
||||
return `${localDefinitionToken({ ...definition, stepMeters: metrics.stepMeters })}/t${decimalToken(metrics.tileSizeMeters, 3)}`;
|
||||
}
|
||||
|
||||
export function localMajorTileId(definition, eastIndex, northIndex) {
|
||||
const metrics = localHierarchyMetrics(definition);
|
||||
const east = safeInteger(eastIndex);
|
||||
const north = safeInteger(northIndex);
|
||||
return `grid/local/${localMajorDefinitionToken(definition, metrics)}/e${signedIndex(east)}/n${signedIndex(north)}`;
|
||||
}
|
||||
|
||||
function localMajorAddress(definition, eastIndex, northIndex) {
|
||||
const metrics = localHierarchyMetrics(definition);
|
||||
const east = safeInteger(eastIndex);
|
||||
const north = safeInteger(northIndex);
|
||||
return {
|
||||
family: "local-enu-major",
|
||||
lod: definition.lod,
|
||||
eastIndex: east,
|
||||
northIndex: north,
|
||||
minorPerSide: metrics.minorPerMajor,
|
||||
id: localMajorTileId(definition, east, north),
|
||||
};
|
||||
}
|
||||
|
||||
export function localMajorTileAt(point, definition) {
|
||||
const { tileSizeMeters } = localHierarchyMetrics(definition);
|
||||
return localMajorAddress(
|
||||
definition,
|
||||
Math.floor(finite(point?.eastMeters, 0) / tileSizeMeters),
|
||||
Math.floor(finite(point?.northMeters, 0) / tileSizeMeters),
|
||||
);
|
||||
}
|
||||
|
||||
export function localMajorTileBounds(address, tileSizeMeters) {
|
||||
const size = positiveNumber(tileSizeMeters, "grid_tile_size_must_be_positive");
|
||||
const eastIndex = safeInteger(address?.eastIndex);
|
||||
const northIndex = safeInteger(address?.northIndex);
|
||||
return {
|
||||
west: eastIndex * size,
|
||||
east: (eastIndex + 1) * size,
|
||||
south: northIndex * size,
|
||||
north: (northIndex + 1) * size,
|
||||
};
|
||||
}
|
||||
|
||||
export function localMajorTileForSector(address, definition) {
|
||||
const { minorPerMajor } = localHierarchyMetrics(definition);
|
||||
return localMajorAddress(
|
||||
definition,
|
||||
Math.floor(safeInteger(address?.eastIndex) / minorPerMajor),
|
||||
Math.floor(safeInteger(address?.northIndex) / minorPerMajor),
|
||||
);
|
||||
}
|
||||
|
||||
export function localMajorTileChildren(address, definition, options) {
|
||||
const { minorPerMajor } = localHierarchyMetrics(definition);
|
||||
const majorEastIndex = safeInteger(address?.eastIndex);
|
||||
const majorNorthIndex = safeInteger(address?.northIndex);
|
||||
const firstEastIndex = safeInteger(majorEastIndex * minorPerMajor);
|
||||
const firstNorthIndex = safeInteger(majorNorthIndex * minorPerMajor);
|
||||
safeInteger(firstEastIndex + minorPerMajor - 1);
|
||||
safeInteger(firstNorthIndex + minorPerMajor - 1);
|
||||
const page = childPageRange(minorPerMajor ** 2, options);
|
||||
const children = [];
|
||||
for (let childIndex = page.start; childIndex < page.end; childIndex += 1) {
|
||||
const eastIndex = firstEastIndex + childIndex % minorPerMajor;
|
||||
const northIndex = firstNorthIndex + Math.floor(childIndex / minorPerMajor);
|
||||
children.push({
|
||||
family: "local-enu",
|
||||
lod: definition.lod,
|
||||
eastIndex,
|
||||
northIndex,
|
||||
id: localSectorId(definition, eastIndex, northIndex),
|
||||
});
|
||||
}
|
||||
return children;
|
||||
}
|
||||
|
||||
export function localMajorTileNeighbors(address, definition) {
|
||||
const eastIndex = safeInteger(address?.eastIndex);
|
||||
const northIndex = safeInteger(address?.northIndex);
|
||||
return {
|
||||
north: localMajorAddress(definition, eastIndex, northIndex + 1),
|
||||
east: localMajorAddress(definition, eastIndex + 1, northIndex),
|
||||
south: localMajorAddress(definition, eastIndex, northIndex - 1),
|
||||
west: localMajorAddress(definition, eastIndex - 1, northIndex),
|
||||
};
|
||||
}
|
||||
|
||||
export function isLocalMajorLineIndex(lineIndex, definition) {
|
||||
const { minorPerMajor } = localHierarchyMetrics(definition);
|
||||
return safeInteger(lineIndex, "grid_line_index_must_be_safe_integer") % minorPerMajor === 0;
|
||||
}
|
||||
|
||||
export function localSectorCenter(address, stepMeters) {
|
||||
const bounds = localSectorBounds(address, stepMeters);
|
||||
return {
|
||||
eastMeters: (bounds.west + bounds.east) / 2,
|
||||
northMeters: (bounds.south + bounds.north) / 2,
|
||||
};
|
||||
}
|
||||
|
||||
export function localSectorAreaSquareMeters(address, stepMeters) {
|
||||
const bounds = localSectorBounds(address, stepMeters);
|
||||
return (bounds.east - bounds.west) * (bounds.north - bounds.south);
|
||||
}
|
||||
|
||||
function localSectorDetail(address, stepMeters) {
|
||||
const bounds = localSectorBounds(address, stepMeters);
|
||||
return {
|
||||
address,
|
||||
bounds,
|
||||
center: localSectorCenter(address, stepMeters),
|
||||
areaSquareMeters: localSectorAreaSquareMeters(address, stepMeters),
|
||||
};
|
||||
}
|
||||
|
||||
export function localSectorSummary(address, definition) {
|
||||
const bounds = localSectorBounds(address, definition.stepMeters);
|
||||
const neighbors = Object.fromEntries(
|
||||
Object.entries(localSectorNeighbors(address, definition))
|
||||
.map(([direction, neighbor]) => [direction, localSectorDetail(neighbor, definition.stepMeters)]),
|
||||
);
|
||||
const majorTile = definition?.tileSizeMeters == null
|
||||
? null
|
||||
: localMajorTileSummary(localMajorTileForSector(address, definition), definition);
|
||||
return {
|
||||
id: address.id,
|
||||
address,
|
||||
family: "local-enu",
|
||||
lod: address.lod,
|
||||
label: `L${address.lod} E${signedIndex(address.eastIndex)} N${signedIndex(address.northIndex)}`,
|
||||
indices: { eastIndex: address.eastIndex, northIndex: address.northIndex },
|
||||
bounds,
|
||||
center: localSectorCenter(address, definition.stepMeters),
|
||||
areaSquareMeters: localSectorAreaSquareMeters(address, definition.stepMeters),
|
||||
neighbors,
|
||||
majorTile,
|
||||
parentMajorTile: majorTile,
|
||||
};
|
||||
}
|
||||
|
||||
export function localMajorTileCenter(address, tileSizeMeters) {
|
||||
const bounds = localMajorTileBounds(address, tileSizeMeters);
|
||||
return {
|
||||
eastMeters: (bounds.west + bounds.east) / 2,
|
||||
northMeters: (bounds.south + bounds.north) / 2,
|
||||
};
|
||||
}
|
||||
|
||||
export function localMajorTileAreaSquareMeters(address, tileSizeMeters) {
|
||||
const bounds = localMajorTileBounds(address, tileSizeMeters);
|
||||
return (bounds.east - bounds.west) * (bounds.north - bounds.south);
|
||||
}
|
||||
|
||||
function localMajorTileDetail(address, tileSizeMeters) {
|
||||
const bounds = localMajorTileBounds(address, tileSizeMeters);
|
||||
return {
|
||||
address,
|
||||
bounds,
|
||||
center: localMajorTileCenter(address, tileSizeMeters),
|
||||
areaSquareMeters: localMajorTileAreaSquareMeters(address, tileSizeMeters),
|
||||
};
|
||||
}
|
||||
|
||||
export function localMajorTileSummary(address, definition) {
|
||||
const metrics = localHierarchyMetrics(definition);
|
||||
const bounds = localMajorTileBounds(address, metrics.tileSizeMeters);
|
||||
const neighbors = Object.fromEntries(
|
||||
Object.entries(localMajorTileNeighbors(address, definition))
|
||||
.map(([direction, neighbor]) => [direction, localMajorTileDetail(neighbor, metrics.tileSizeMeters)]),
|
||||
);
|
||||
return {
|
||||
id: address.id,
|
||||
address,
|
||||
family: "local-enu-major",
|
||||
lod: address.lod,
|
||||
label: `L${address.lod} TILE E${signedIndex(address.eastIndex)} N${signedIndex(address.northIndex)}`,
|
||||
indices: { eastIndex: address.eastIndex, northIndex: address.northIndex },
|
||||
minorPerSide: metrics.minorPerMajor,
|
||||
childCount: safeInteger(metrics.minorPerMajor ** 2, "grid_child_count_must_be_safe_integer"),
|
||||
bounds,
|
||||
center: localMajorTileCenter(address, metrics.tileSizeMeters),
|
||||
areaSquareMeters: localMajorTileAreaSquareMeters(address, metrics.tileSizeMeters),
|
||||
neighbors,
|
||||
};
|
||||
}
|
||||
|
||||
function localVolumeMetrics(definition) {
|
||||
const stepMeters = positiveNumber(definition?.stepMeters, "grid_step_must_be_positive");
|
||||
const altitudeBandMeters = positiveNumber(
|
||||
definition?.altitudeBandMeters,
|
||||
"grid_altitude_band_must_be_positive",
|
||||
);
|
||||
const altitudeFloorMeters = Number(
|
||||
definition?.altitudeFloorMeters ?? definition?.altitudeOriginMeters ?? 0,
|
||||
);
|
||||
if (!Number.isFinite(altitudeFloorMeters)) {
|
||||
throw new Error("grid_altitude_floor_must_be_finite");
|
||||
}
|
||||
const altitudeCeilingMeters = definition?.altitudeCeilingMeters == null
|
||||
? Number.POSITIVE_INFINITY
|
||||
: Number(definition.altitudeCeilingMeters);
|
||||
if ((!Number.isFinite(altitudeCeilingMeters) && altitudeCeilingMeters !== Number.POSITIVE_INFINITY)
|
||||
|| altitudeCeilingMeters <= altitudeFloorMeters) {
|
||||
throw new Error("grid_altitude_ceiling_must_exceed_floor");
|
||||
}
|
||||
return { stepMeters, altitudeBandMeters, altitudeFloorMeters, altitudeCeilingMeters };
|
||||
}
|
||||
|
||||
function localVolumeDefinitionToken(definition, metrics = localVolumeMetrics(definition)) {
|
||||
const ceilingToken = Number.isFinite(metrics.altitudeCeilingMeters)
|
||||
? decimalToken(metrics.altitudeCeilingMeters, 3)
|
||||
: "inf";
|
||||
return `${localDefinitionToken({ ...definition, stepMeters: metrics.stepMeters })}/f${decimalToken(metrics.altitudeFloorMeters, 3)}/c${ceilingToken}/h${decimalToken(metrics.altitudeBandMeters, 3)}`;
|
||||
}
|
||||
|
||||
export function localVolumeId(definition, eastIndex, northIndex, bandIndex) {
|
||||
const metrics = localVolumeMetrics(definition);
|
||||
const east = safeInteger(eastIndex);
|
||||
const north = safeInteger(northIndex);
|
||||
const band = safeInteger(bandIndex, "grid_altitude_band_index_must_be_safe_integer");
|
||||
if (band < 0
|
||||
|| metrics.altitudeFloorMeters + band * metrics.altitudeBandMeters >= metrics.altitudeCeilingMeters) {
|
||||
throw new Error("grid_altitude_band_index_out_of_range");
|
||||
}
|
||||
return `grid/local-volume/${localVolumeDefinitionToken(definition, metrics)}/e${signedIndex(east)}/n${signedIndex(north)}/z${signedIndex(band)}`;
|
||||
}
|
||||
|
||||
function localVolumeAddress(definition, eastIndex, northIndex, bandIndex) {
|
||||
const metrics = localVolumeMetrics(definition);
|
||||
const east = safeInteger(eastIndex);
|
||||
const north = safeInteger(northIndex);
|
||||
const band = safeInteger(bandIndex, "grid_altitude_band_index_must_be_safe_integer");
|
||||
if (band < 0) throw new Error("grid_altitude_band_index_out_of_range");
|
||||
const altitudeFloorMeters = metrics.altitudeFloorMeters + band * metrics.altitudeBandMeters;
|
||||
if (altitudeFloorMeters >= metrics.altitudeCeilingMeters) {
|
||||
throw new Error("grid_altitude_band_index_out_of_range");
|
||||
}
|
||||
const altitudeCeilingMeters = Math.min(
|
||||
metrics.altitudeCeilingMeters,
|
||||
altitudeFloorMeters + metrics.altitudeBandMeters,
|
||||
);
|
||||
return {
|
||||
family: "local-enu-volume",
|
||||
lod: definition.lod,
|
||||
eastIndex: east,
|
||||
northIndex: north,
|
||||
bandIndex: band,
|
||||
altitudeFloorMeters,
|
||||
altitudeCeilingMeters,
|
||||
altitudeBandMeters: altitudeCeilingMeters - altitudeFloorMeters,
|
||||
id: localVolumeId(definition, east, north, band),
|
||||
};
|
||||
}
|
||||
|
||||
export function localVolumeAt(point, definition) {
|
||||
const metrics = localVolumeMetrics(definition);
|
||||
const altitudeMeters = Number(point?.altitudeMeters);
|
||||
if (!Number.isFinite(altitudeMeters)
|
||||
|| altitudeMeters < metrics.altitudeFloorMeters
|
||||
|| altitudeMeters >= metrics.altitudeCeilingMeters) {
|
||||
return null;
|
||||
}
|
||||
const eastIndex = Math.floor(finite(point?.eastMeters, 0) / metrics.stepMeters);
|
||||
const northIndex = Math.floor(finite(point?.northMeters, 0) / metrics.stepMeters);
|
||||
const bandIndex = Math.floor(
|
||||
(altitudeMeters - metrics.altitudeFloorMeters) / metrics.altitudeBandMeters,
|
||||
);
|
||||
return localVolumeAddress(definition, eastIndex, northIndex, bandIndex);
|
||||
}
|
||||
|
||||
export function localVolumeBounds(address, definition) {
|
||||
const metrics = localVolumeMetrics(definition);
|
||||
const horizontal = localSectorBounds(address, metrics.stepMeters);
|
||||
const bandIndex = safeInteger(address?.bandIndex, "grid_altitude_band_index_must_be_safe_integer");
|
||||
if (bandIndex < 0) throw new Error("grid_altitude_band_index_out_of_range");
|
||||
const altitudeFloorMeters = metrics.altitudeFloorMeters + bandIndex * metrics.altitudeBandMeters;
|
||||
if (altitudeFloorMeters >= metrics.altitudeCeilingMeters) {
|
||||
throw new Error("grid_altitude_band_index_out_of_range");
|
||||
}
|
||||
return {
|
||||
...horizontal,
|
||||
altitudeFloorMeters,
|
||||
altitudeCeilingMeters: Math.min(
|
||||
metrics.altitudeCeilingMeters,
|
||||
altitudeFloorMeters + metrics.altitudeBandMeters,
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
export function localVolumeNeighbors(address, definition) {
|
||||
const metrics = localVolumeMetrics(definition);
|
||||
const eastIndex = safeInteger(address?.eastIndex);
|
||||
const northIndex = safeInteger(address?.northIndex);
|
||||
const bandIndex = safeInteger(address?.bandIndex, "grid_altitude_band_index_must_be_safe_integer");
|
||||
const aboveFloor = metrics.altitudeFloorMeters + (bandIndex + 1) * metrics.altitudeBandMeters;
|
||||
return {
|
||||
north: localVolumeAddress(definition, eastIndex, northIndex + 1, bandIndex),
|
||||
east: localVolumeAddress(definition, eastIndex + 1, northIndex, bandIndex),
|
||||
south: localVolumeAddress(definition, eastIndex, northIndex - 1, bandIndex),
|
||||
west: localVolumeAddress(definition, eastIndex - 1, northIndex, bandIndex),
|
||||
above: aboveFloor >= metrics.altitudeCeilingMeters
|
||||
? null
|
||||
: localVolumeAddress(definition, eastIndex, northIndex, bandIndex + 1),
|
||||
below: bandIndex === 0
|
||||
? null
|
||||
: localVolumeAddress(definition, eastIndex, northIndex, bandIndex - 1),
|
||||
};
|
||||
}
|
||||
|
||||
export function localVolumeCenter(address, definition) {
|
||||
const horizontal = localSectorCenter(address, definition.stepMeters);
|
||||
const bounds = localVolumeBounds(address, definition);
|
||||
return {
|
||||
...horizontal,
|
||||
altitudeMeters: (bounds.altitudeFloorMeters + bounds.altitudeCeilingMeters) / 2,
|
||||
};
|
||||
}
|
||||
|
||||
export function localVolumeSummary(address, definition) {
|
||||
const bounds = localVolumeBounds(address, definition);
|
||||
const footprintAreaSquareMeters = (bounds.east - bounds.west) * (bounds.north - bounds.south);
|
||||
return {
|
||||
id: address.id,
|
||||
address,
|
||||
family: "local-enu-volume",
|
||||
lod: address.lod,
|
||||
label: `L${address.lod} E${signedIndex(address.eastIndex)} N${signedIndex(address.northIndex)} Z${signedIndex(address.bandIndex)}`,
|
||||
indices: {
|
||||
eastIndex: address.eastIndex,
|
||||
northIndex: address.northIndex,
|
||||
bandIndex: address.bandIndex,
|
||||
},
|
||||
bounds,
|
||||
center: localVolumeCenter(address, definition),
|
||||
footprintAreaSquareMeters,
|
||||
volumeCubicMeters: footprintAreaSquareMeters
|
||||
* (bounds.altitudeCeilingMeters - bounds.altitudeFloorMeters),
|
||||
};
|
||||
}
|
||||
|
||||
export function localGridPlan({ stepMeters, radiusMeters, maximumMarkers = 5_000 }) {
|
||||
const step = Math.max(1, finite(stepMeters, 1));
|
||||
const requestedRadius = Math.max(step, finite(radiusMeters, step));
|
||||
@@ -138,6 +532,342 @@ export function graticuleSectorBounds(address, stepDegrees) {
|
||||
};
|
||||
}
|
||||
|
||||
function graticuleTopology(stepDegrees, errorCode = "grid_graticule_step_must_partition_hemisphere") {
|
||||
const step = positiveNumber(stepDegrees, "grid_graticule_step_must_be_positive");
|
||||
const longitudeHemisphereCount = integerMultiple(180, step, errorCode);
|
||||
const latitudeHemisphereCount = integerMultiple(90, step, errorCode);
|
||||
return {
|
||||
stepDegrees: step,
|
||||
longitudeHemisphereCount,
|
||||
latitudeHemisphereCount,
|
||||
longitudeCount: longitudeHemisphereCount * 2,
|
||||
latitudeCount: latitudeHemisphereCount * 2,
|
||||
};
|
||||
}
|
||||
|
||||
function wrapGraticuleIndex(index, hemisphereCount) {
|
||||
const span = hemisphereCount * 2;
|
||||
return canonicalZero(((index + hemisphereCount) % span + span) % span - hemisphereCount);
|
||||
}
|
||||
|
||||
function graticuleSectorAddress(definition, longitudeIndex, latitudeIndex) {
|
||||
const longitude = safeInteger(longitudeIndex);
|
||||
const latitude = safeInteger(latitudeIndex);
|
||||
return {
|
||||
family: "wgs84-graticule",
|
||||
lod: definition.lod,
|
||||
longitudeIndex: longitude,
|
||||
latitudeIndex: latitude,
|
||||
id: graticuleSectorId(definition, longitude, latitude),
|
||||
};
|
||||
}
|
||||
|
||||
export function graticuleSectorNeighbors(address, definition) {
|
||||
const stepDegrees = positiveNumber(definition?.stepDegrees, "grid_graticule_step_must_be_positive");
|
||||
const minimumLongitudeIndex = Math.floor(-180 / stepDegrees);
|
||||
const maximumLongitudeIndex = Math.floor((180 - EPSILON) / stepDegrees);
|
||||
const minimumLatitudeIndex = Math.floor(-90 / stepDegrees);
|
||||
const maximumLatitudeIndex = Math.floor((90 - EPSILON) / stepDegrees);
|
||||
const longitudeIndex = safeInteger(address?.longitudeIndex);
|
||||
const latitudeIndex = safeInteger(address?.latitudeIndex);
|
||||
if (longitudeIndex < minimumLongitudeIndex || longitudeIndex > maximumLongitudeIndex) {
|
||||
throw new Error("grid_graticule_longitude_index_out_of_range");
|
||||
}
|
||||
if (latitudeIndex < minimumLatitudeIndex || latitudeIndex > maximumLatitudeIndex) {
|
||||
throw new Error("grid_graticule_latitude_index_out_of_range");
|
||||
}
|
||||
return {
|
||||
north: latitudeIndex === maximumLatitudeIndex
|
||||
? null
|
||||
: graticuleSectorAddress(definition, longitudeIndex, latitudeIndex + 1),
|
||||
east: graticuleSectorAddress(
|
||||
definition,
|
||||
longitudeIndex === maximumLongitudeIndex ? minimumLongitudeIndex : longitudeIndex + 1,
|
||||
latitudeIndex,
|
||||
),
|
||||
south: latitudeIndex === minimumLatitudeIndex
|
||||
? null
|
||||
: graticuleSectorAddress(definition, longitudeIndex, latitudeIndex - 1),
|
||||
west: graticuleSectorAddress(
|
||||
definition,
|
||||
longitudeIndex === minimumLongitudeIndex ? maximumLongitudeIndex : longitudeIndex - 1,
|
||||
latitudeIndex,
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
function graticuleHierarchyMetrics(definition) {
|
||||
const stepDegrees = positiveNumber(definition?.stepDegrees, "grid_graticule_step_must_be_positive");
|
||||
const majorStepDegrees = positiveNumber(
|
||||
definition?.majorStepDegrees,
|
||||
"grid_graticule_major_step_must_be_positive",
|
||||
);
|
||||
const minorPerMajor = integerMultiple(
|
||||
majorStepDegrees,
|
||||
stepDegrees,
|
||||
"grid_graticule_major_step_must_be_integer_multiple_of_step",
|
||||
);
|
||||
const topology = graticuleTopology(
|
||||
majorStepDegrees,
|
||||
"grid_graticule_major_step_must_partition_hemisphere",
|
||||
);
|
||||
return { ...topology, stepDegrees, majorStepDegrees, minorPerMajor };
|
||||
}
|
||||
|
||||
function graticuleMajorDefinitionToken(definition, metrics = graticuleHierarchyMetrics(definition)) {
|
||||
return `${graticuleDefinitionToken({ ...definition, stepDegrees: metrics.stepDegrees })}/m${decimalToken(metrics.majorStepDegrees, 6)}`;
|
||||
}
|
||||
|
||||
export function graticuleMajorTileId(definition, longitudeIndex, latitudeIndex) {
|
||||
const metrics = graticuleHierarchyMetrics(definition);
|
||||
const longitude = wrapGraticuleIndex(
|
||||
safeInteger(longitudeIndex),
|
||||
metrics.longitudeHemisphereCount,
|
||||
);
|
||||
const latitude = safeInteger(latitudeIndex);
|
||||
if (latitude < -metrics.latitudeHemisphereCount || latitude >= metrics.latitudeHemisphereCount) {
|
||||
throw new Error("grid_graticule_major_latitude_index_out_of_range");
|
||||
}
|
||||
return `grid/${graticuleMajorDefinitionToken(definition, metrics)}/x${signedIndex(longitude)}/y${signedIndex(latitude)}`;
|
||||
}
|
||||
|
||||
function graticuleMajorAddress(definition, longitudeIndex, latitudeIndex) {
|
||||
const metrics = graticuleHierarchyMetrics(definition);
|
||||
const longitude = wrapGraticuleIndex(safeInteger(longitudeIndex), metrics.longitudeHemisphereCount);
|
||||
const latitude = safeInteger(latitudeIndex);
|
||||
if (latitude < -metrics.latitudeHemisphereCount || latitude >= metrics.latitudeHemisphereCount) {
|
||||
throw new Error("grid_graticule_major_latitude_index_out_of_range");
|
||||
}
|
||||
return {
|
||||
family: "wgs84-graticule-major",
|
||||
lod: definition.lod,
|
||||
longitudeIndex: longitude,
|
||||
latitudeIndex: latitude,
|
||||
minorPerSide: metrics.minorPerMajor,
|
||||
id: graticuleMajorTileId(definition, longitude, latitude),
|
||||
};
|
||||
}
|
||||
|
||||
export function graticuleMajorTileAt(point, definition) {
|
||||
const { majorStepDegrees } = graticuleHierarchyMetrics(definition);
|
||||
const longitude = normalizeLongitudeDegrees(point?.longitude);
|
||||
const latitude = clamp(finite(point?.latitude, 0), -90, 90 - EPSILON);
|
||||
return graticuleMajorAddress(
|
||||
definition,
|
||||
Math.floor(longitude / majorStepDegrees),
|
||||
Math.floor(latitude / majorStepDegrees),
|
||||
);
|
||||
}
|
||||
|
||||
export function graticuleMajorTileBounds(address, majorStepDegrees) {
|
||||
const topology = graticuleTopology(
|
||||
majorStepDegrees,
|
||||
"grid_graticule_major_step_must_partition_hemisphere",
|
||||
);
|
||||
const longitudeIndex = wrapGraticuleIndex(
|
||||
safeInteger(address?.longitudeIndex),
|
||||
topology.longitudeHemisphereCount,
|
||||
);
|
||||
const latitudeIndex = safeInteger(address?.latitudeIndex);
|
||||
if (latitudeIndex < -topology.latitudeHemisphereCount
|
||||
|| latitudeIndex >= topology.latitudeHemisphereCount) {
|
||||
throw new Error("grid_graticule_major_latitude_index_out_of_range");
|
||||
}
|
||||
return {
|
||||
west: longitudeIndex * topology.stepDegrees,
|
||||
east: (longitudeIndex + 1) * topology.stepDegrees,
|
||||
south: latitudeIndex * topology.stepDegrees,
|
||||
north: (latitudeIndex + 1) * topology.stepDegrees,
|
||||
};
|
||||
}
|
||||
|
||||
export function graticuleMajorTileForSector(address, definition) {
|
||||
const metrics = graticuleHierarchyMetrics(definition);
|
||||
return graticuleMajorAddress(
|
||||
definition,
|
||||
Math.floor(safeInteger(address?.longitudeIndex) / metrics.minorPerMajor),
|
||||
Math.floor(safeInteger(address?.latitudeIndex) / metrics.minorPerMajor),
|
||||
);
|
||||
}
|
||||
|
||||
export function graticuleMajorTileChildren(address, definition, options) {
|
||||
const metrics = graticuleHierarchyMetrics(definition);
|
||||
const major = graticuleMajorAddress(definition, address?.longitudeIndex, address?.latitudeIndex);
|
||||
const firstLongitudeIndex = major.longitudeIndex * metrics.minorPerMajor;
|
||||
const firstLatitudeIndex = major.latitudeIndex * metrics.minorPerMajor;
|
||||
const page = childPageRange(metrics.minorPerMajor ** 2, options);
|
||||
const children = [];
|
||||
for (let childIndex = page.start; childIndex < page.end; childIndex += 1) {
|
||||
children.push(graticuleSectorAddress(
|
||||
definition,
|
||||
firstLongitudeIndex + childIndex % metrics.minorPerMajor,
|
||||
firstLatitudeIndex + Math.floor(childIndex / metrics.minorPerMajor),
|
||||
));
|
||||
}
|
||||
return children;
|
||||
}
|
||||
|
||||
export function graticuleMajorTileNeighbors(address, definition) {
|
||||
const metrics = graticuleHierarchyMetrics(definition);
|
||||
const tile = graticuleMajorAddress(definition, address?.longitudeIndex, address?.latitudeIndex);
|
||||
return {
|
||||
north: tile.latitudeIndex === metrics.latitudeHemisphereCount - 1
|
||||
? null
|
||||
: graticuleMajorAddress(definition, tile.longitudeIndex, tile.latitudeIndex + 1),
|
||||
east: graticuleMajorAddress(definition, tile.longitudeIndex + 1, tile.latitudeIndex),
|
||||
south: tile.latitudeIndex === -metrics.latitudeHemisphereCount
|
||||
? null
|
||||
: graticuleMajorAddress(definition, tile.longitudeIndex, tile.latitudeIndex - 1),
|
||||
west: graticuleMajorAddress(definition, tile.longitudeIndex - 1, tile.latitudeIndex),
|
||||
};
|
||||
}
|
||||
|
||||
export function isGraticuleMajorLineIndex(lineIndex, definition) {
|
||||
const { minorPerMajor } = graticuleHierarchyMetrics(definition);
|
||||
return safeInteger(lineIndex, "grid_line_index_must_be_safe_integer") % minorPerMajor === 0;
|
||||
}
|
||||
|
||||
export function isGraticuleMajorLineValue(valueDegrees, definition) {
|
||||
const { majorStepDegrees } = graticuleHierarchyMetrics(definition);
|
||||
const ratio = finite(valueDegrees, Number.NaN) / majorStepDegrees;
|
||||
return Number.isFinite(ratio)
|
||||
&& Math.abs(ratio - Math.round(ratio)) <= EPSILON * Math.max(1, Math.abs(ratio));
|
||||
}
|
||||
|
||||
function authalicQ(latitudeRadians) {
|
||||
const sine = Math.sin(latitudeRadians);
|
||||
const eccentricitySine = WGS84_ECCENTRICITY * sine;
|
||||
return (1 - WGS84_ECCENTRICITY_SQUARED) * (
|
||||
sine / (1 - WGS84_ECCENTRICITY_SQUARED * sine * sine)
|
||||
- Math.log((1 - eccentricitySine) / (1 + eccentricitySine)) / (2 * WGS84_ECCENTRICITY)
|
||||
);
|
||||
}
|
||||
|
||||
export function geodeticRectangleAreaSquareMeters(bounds) {
|
||||
const south = clamp(finite(bounds?.south, -90), -90, 90);
|
||||
const north = clamp(finite(bounds?.north, 90), -90, 90);
|
||||
if (north <= south) return 0;
|
||||
const rawWest = finite(bounds?.west, -180);
|
||||
const rawEast = finite(bounds?.east, 180);
|
||||
let longitudeSpanDegrees = rawEast - rawWest;
|
||||
if (Math.abs(longitudeSpanDegrees) >= 360 - EPSILON) longitudeSpanDegrees = 360;
|
||||
else if (longitudeSpanDegrees < 0) longitudeSpanDegrees += 360;
|
||||
longitudeSpanDegrees = clamp(longitudeSpanDegrees, 0, 360);
|
||||
const longitudeSpanRadians = longitudeSpanDegrees * Math.PI / 180;
|
||||
const southRadians = south * Math.PI / 180;
|
||||
const northRadians = north * Math.PI / 180;
|
||||
return WGS84_EQUATORIAL_RADIUS_METERS ** 2
|
||||
* longitudeSpanRadians
|
||||
* Math.abs(authalicQ(northRadians) - authalicQ(southRadians))
|
||||
/ 2;
|
||||
}
|
||||
|
||||
function geodeticBoundsCenter(bounds) {
|
||||
const width = bounds.east - bounds.west;
|
||||
return {
|
||||
longitude: width >= 360 - EPSILON
|
||||
? 0
|
||||
: normalizeLongitudeDegrees(bounds.west + width / 2),
|
||||
latitude: (bounds.south + bounds.north) / 2,
|
||||
};
|
||||
}
|
||||
|
||||
export function graticuleSectorCenter(address, stepDegrees) {
|
||||
return geodeticBoundsCenter(graticuleSectorBounds(address, stepDegrees));
|
||||
}
|
||||
|
||||
export function graticuleSectorAreaSquareMeters(address, stepDegrees) {
|
||||
return geodeticRectangleAreaSquareMeters(graticuleSectorBounds(address, stepDegrees));
|
||||
}
|
||||
|
||||
function graticuleSectorDetail(address, stepDegrees) {
|
||||
const bounds = graticuleSectorBounds(address, stepDegrees);
|
||||
return {
|
||||
address,
|
||||
bounds,
|
||||
center: graticuleSectorCenter(address, stepDegrees),
|
||||
areaSquareMeters: graticuleSectorAreaSquareMeters(address, stepDegrees),
|
||||
};
|
||||
}
|
||||
|
||||
export function graticuleSectorSummary(address, definition) {
|
||||
const bounds = graticuleSectorBounds(address, definition.stepDegrees);
|
||||
const neighbors = Object.fromEntries(
|
||||
Object.entries(graticuleSectorNeighbors(address, definition))
|
||||
.map(([direction, neighbor]) => [
|
||||
direction,
|
||||
neighbor == null ? null : graticuleSectorDetail(neighbor, definition.stepDegrees),
|
||||
]),
|
||||
);
|
||||
const majorTile = definition?.majorStepDegrees == null
|
||||
? null
|
||||
: graticuleMajorTileSummary(graticuleMajorTileForSector(address, definition), definition);
|
||||
return {
|
||||
id: address.id,
|
||||
address,
|
||||
family: "wgs84-graticule",
|
||||
lod: address.lod,
|
||||
label: `L${address.lod} X${signedIndex(address.longitudeIndex)} Y${signedIndex(address.latitudeIndex)}`,
|
||||
indices: {
|
||||
longitudeIndex: address.longitudeIndex,
|
||||
latitudeIndex: address.latitudeIndex,
|
||||
},
|
||||
bounds,
|
||||
center: graticuleSectorCenter(address, definition.stepDegrees),
|
||||
areaSquareMeters: graticuleSectorAreaSquareMeters(address, definition.stepDegrees),
|
||||
neighbors,
|
||||
majorTile,
|
||||
parentMajorTile: majorTile,
|
||||
};
|
||||
}
|
||||
|
||||
export function graticuleMajorTileCenter(address, majorStepDegrees) {
|
||||
return geodeticBoundsCenter(graticuleMajorTileBounds(address, majorStepDegrees));
|
||||
}
|
||||
|
||||
export function graticuleMajorTileAreaSquareMeters(address, majorStepDegrees) {
|
||||
return geodeticRectangleAreaSquareMeters(graticuleMajorTileBounds(address, majorStepDegrees));
|
||||
}
|
||||
|
||||
function graticuleMajorTileDetail(address, majorStepDegrees) {
|
||||
const bounds = graticuleMajorTileBounds(address, majorStepDegrees);
|
||||
return {
|
||||
address,
|
||||
bounds,
|
||||
center: graticuleMajorTileCenter(address, majorStepDegrees),
|
||||
areaSquareMeters: graticuleMajorTileAreaSquareMeters(address, majorStepDegrees),
|
||||
};
|
||||
}
|
||||
|
||||
export function graticuleMajorTileSummary(address, definition) {
|
||||
const metrics = graticuleHierarchyMetrics(definition);
|
||||
const bounds = graticuleMajorTileBounds(address, metrics.majorStepDegrees);
|
||||
const neighbors = Object.fromEntries(
|
||||
Object.entries(graticuleMajorTileNeighbors(address, definition))
|
||||
.map(([direction, neighbor]) => [
|
||||
direction,
|
||||
neighbor == null ? null : graticuleMajorTileDetail(neighbor, metrics.majorStepDegrees),
|
||||
]),
|
||||
);
|
||||
return {
|
||||
id: address.id,
|
||||
address,
|
||||
family: "wgs84-graticule-major",
|
||||
lod: address.lod,
|
||||
label: `L${address.lod} TILE X${signedIndex(address.longitudeIndex)} Y${signedIndex(address.latitudeIndex)}`,
|
||||
indices: {
|
||||
longitudeIndex: address.longitudeIndex,
|
||||
latitudeIndex: address.latitudeIndex,
|
||||
},
|
||||
minorPerSide: metrics.minorPerMajor,
|
||||
childCount: safeInteger(metrics.minorPerMajor ** 2, "grid_child_count_must_be_safe_integer"),
|
||||
bounds,
|
||||
center: graticuleMajorTileCenter(address, metrics.majorStepDegrees),
|
||||
areaSquareMeters: graticuleMajorTileAreaSquareMeters(address, metrics.majorStepDegrees),
|
||||
neighbors,
|
||||
};
|
||||
}
|
||||
|
||||
export function splitLongitudeRange(west, east) {
|
||||
const rawWest = finite(west, -180);
|
||||
const rawEast = finite(east, 180);
|
||||
|
||||
Reference in New Issue
Block a user