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)); 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); } export function fixedGridOrigin(settings) { return { latitude: clamp(finite(settings?.gridCenterLatitude, 55.7558), -89.9, 89.9), longitude: normalizeLongitudeDegrees(finite(settings?.gridCenterLongitude, 37.6173)), }; } function localDefinitionToken(definition) { const latitude = decimalToken(definition.originLatitude, 6); const longitude = decimalToken(normalizeLongitudeDegrees(definition.originLongitude), 6); const stepMeters = decimalToken(definition.stepMeters, 3); return `${latitude},${longitude}/l${definition.lod}/s${stepMeters}`; } export function localSectorId(definition, eastIndex, northIndex) { return `grid/local/${localDefinitionToken(definition)}/e${signedIndex(eastIndex)}/n${signedIndex(northIndex)}`; } export function localSectorAt(point, definition) { const stepMeters = Math.max(EPSILON, finite(definition.stepMeters, 1)); const eastIndex = canonicalZero(Math.floor(finite(point.eastMeters, 0) / stepMeters)); const northIndex = canonicalZero(Math.floor(finite(point.northMeters, 0) / stepMeters)); return { family: "local-enu", lod: definition.lod, eastIndex, northIndex, id: localSectorId({ ...definition, stepMeters }, eastIndex, northIndex), }; } export function localSectorBounds(address, stepMeters) { const step = Math.max(EPSILON, finite(stepMeters, 1)); return { west: address.eastIndex * step, east: (address.eastIndex + 1) * step, south: address.northIndex * step, north: (address.northIndex + 1) * step, }; } export function localSectorNeighbors(address, definition) { const at = (eastIndex, northIndex) => ({ family: "local-enu", lod: definition.lod, eastIndex, northIndex, id: localSectorId(definition, eastIndex, northIndex), }); return { north: at(address.eastIndex, address.northIndex + 1), east: at(address.eastIndex + 1, address.northIndex), south: at(address.eastIndex, address.northIndex - 1), west: at(address.eastIndex - 1, address.northIndex), }; } export function localParentSector(address, childStepMeters, parentDefinition) { const childStep = Math.max(EPSILON, finite(childStepMeters, 1)); const parentStep = Math.max(EPSILON, finite(parentDefinition.stepMeters, 1)); const ratio = parentStep / childStep; if (!Number.isInteger(ratio) || ratio < 1) throw new Error("grid_parent_step_must_be_integer_multiple"); return localSectorAt({ eastMeters: address.eastIndex * childStep, northMeters: address.northIndex * childStep, }, 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)); // Invalid in-memory inspector edits must not allocate millions of lines // before server validation can reject them. Canonical layouts satisfy the // same ratio explicitly; this is the renderer's final fail-safe. const maximumIndex = Math.min(MAX_LOCAL_GRID_INDEX, Math.ceil(requestedRadius / step)); const radius = Math.min(requestedRadius, maximumIndex * step); const radiusSquared = radius * radius; const lines = []; for (let index = -maximumIndex; index <= maximumIndex; index += 1) { const offsetMeters = index * step; if (Math.abs(offsetMeters) > radius + EPSILON) continue; lines.push({ index, offsetMeters, extentMeters: Math.sqrt(Math.max(0, radiusSquared - offsetMeters * offsetMeters)), }); } const approximateMarkers = Math.PI * (radius / step) ** 2; const markerStride = Math.max(1, Math.ceil(Math.sqrt(approximateMarkers / Math.max(1, maximumMarkers)))); return { stepMeters: step, radiusMeters: radius, requestedRadiusMeters: requestedRadius, clipped: radius < requestedRadius, maximumIndex, markerStride, lines }; } function graticuleDefinitionToken(definition) { return `wgs84/l${definition.lod}/s${decimalToken(definition.stepDegrees, 6)}`; } export function graticuleSectorId(definition, longitudeIndex, latitudeIndex) { return `grid/${graticuleDefinitionToken(definition)}/x${signedIndex(longitudeIndex)}/y${signedIndex(latitudeIndex)}`; } export function graticuleSectorAt(point, definition) { const stepDegrees = Math.max(EPSILON, finite(definition.stepDegrees, 1)); const longitude = normalizeLongitudeDegrees(point.longitude); const latitude = clamp(finite(point.latitude, 0), -90, 90 - EPSILON); const longitudeIndex = canonicalZero(Math.floor(longitude / stepDegrees)); const latitudeIndex = canonicalZero(Math.floor(latitude / stepDegrees)); return { family: "wgs84-graticule", lod: definition.lod, longitudeIndex, latitudeIndex, id: graticuleSectorId({ ...definition, stepDegrees }, longitudeIndex, latitudeIndex), }; } export function graticuleSectorBounds(address, stepDegrees) { const step = Math.max(EPSILON, finite(stepDegrees, 1)); return { west: Math.max(-180, address.longitudeIndex * step), east: Math.min(180, (address.longitudeIndex + 1) * step), south: Math.max(-90, address.latitudeIndex * step), north: Math.min(90, (address.latitudeIndex + 1) * step), }; } 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); if (Math.abs(rawEast - rawWest) >= 360 - EPSILON) return [{ west: -180, east: 180 }]; const normalizedWest = normalizeLongitudeDegrees(rawWest); const normalizedEast = normalizeLongitudeDegrees(rawEast); if (Math.abs(normalizedWest - normalizedEast) < EPSILON && Math.abs(rawEast - rawWest) > EPSILON) { return [{ west: -180, east: 180 }]; } if (normalizedWest <= normalizedEast) return [{ west: normalizedWest, east: normalizedEast }]; return [ { west: normalizedWest, east: 180 }, { west: -180, east: normalizedEast }, ]; } export function alignedGridValues(minimum, maximum, step, options = {}) { const safeStep = Math.max(EPSILON, finite(step, 1)); const min = finite(minimum, 0); const max = finite(maximum, min); const includeMaximum = options.includeMaximum !== false; const first = Math.ceil((min - EPSILON) / safeStep); const last = includeMaximum ? Math.floor((max + EPSILON) / safeStep) : Math.ceil((max - EPSILON) / safeStep) - 1; const values = []; for (let index = first; index <= last; index += 1) values.push(canonicalZero(index * safeStep)); return values; } export function boundedAngularParts(start, end, maximumSpanDegrees = 90) { const minimum = finite(start, 0); const maximum = finite(end, minimum); const maximumSpan = clamp(finite(maximumSpanDegrees, 90), 0.1, 90); if (maximum <= minimum) return []; const parts = []; for (let partStart = minimum; partStart < maximum; partStart += maximumSpan) { parts.push({ start: partStart, end: Math.min(maximum, partStart + maximumSpan) }); } return parts; } export function graticuleGranularity(stepDegrees, clampToGround) { const radians = clamp(finite(stepDegrees, 1), 0.1, 180) * Math.PI / 180; // Cesium's PolylineGeometry consumes angular granularity, while // GroundPolylineGeometry consumes a surface distance in metres. return clampToGround ? radians * WGS84_EQUATORIAL_RADIUS_METERS : radians; } export function graticuleLinePlan({ south, north, longitudeIntervals, stepDegrees }) { const step = clamp(finite(stepDegrees, 1), 0.1, 180); const safeSouth = clamp(finite(south, -89.9), -89.9, 89.9); const safeNorth = clamp(finite(north, 89.9), -89.9, 89.9); const intervals = longitudeIntervals?.length ? longitudeIntervals : [{ west: -180, east: 180 }]; const parallels = alignedGridValues(safeSouth, safeNorth, step) .filter((latitude) => latitude > -90 + EPSILON && latitude < 90 - EPSILON); const meridians = []; const seen = new Set(); for (const interval of intervals) { for (const longitude of alignedGridValues(interval.west, interval.east, step, { includeMaximum: interval.east < 180 })) { const normalized = normalizeLongitudeDegrees(longitude); const key = decimalToken(normalized, 9); if (seen.has(key)) continue; seen.add(key); meridians.push({ longitude: normalized, interval }); } } return { stepDegrees: step, south: safeSouth, north: safeNorth, longitudeIntervals: intervals, parallels, meridians }; }