All files / map rings.ts

100% Statements 37/37
100% Branches 2/2
100% Functions 12/12
100% Lines 37/37

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108                              4x   4x       4x         4x       4x   4x 4x   4x 4x 4x     4x 8x 8x 8x 52x 1x       4x       7x     4x 8688x 8688x     4x 48x 8688x 8688x     4x       50x       4x         8x 48x 48x 48x 48x                   4x   4x     5x 30x              
import { Array, Option } from 'effect'
 
import type { RangeRings } from '../core/domain'
import type { LatLon } from '../core/geo/ellipsoid'
import { METRES_PER_NAUTICAL_MILE, destination } from '../core/geo/geodesic'
import type { Box, GridLabel, Position, RGBA } from '../core/geo/geometry'
import { CUE_COLOR } from './cue'
import { LineRank, lineFeature } from './features'
 
type RingUnit = RangeRings.RingUnit
 
/**
 * Range rings around the cue: geodesic circles on WGS84 at an even spacing, like radar
 * range rings. 'Auto' picks a 1-2-5 spacing from the view, as a radar picks a range scale.
 */
export const RING_COUNT = 6
 
const unitMetres: Record<RingUnit, number> = {
  NauticalMiles: METRES_PER_NAUTICAL_MILE,
  Kilometres: 1000,
}
const unitSymbol: Record<RingUnit, string> = {
  NauticalMiles: 'NM',
  Kilometres: 'km',
}
 
const NICE_STEPS: Array.NonEmptyReadonlyArray<number> = [
  0.1, 0.25, 0.5, 1, 2, 5, 10, 25, 50, 100, 250, 500,
]
 
const METRES_PER_DEGREE_LATITUDE = 111_320
/** The outer ring reaches this share of the distance from the centre to the top of the view. */
const AUTO_FILL = 0.85
const BEARING_STEP_DEGREES = 2
 
const RING_ALPHA = 170
const [cueRed, cueGreen, cueBlue] = CUE_COLOR
const RING_COLOR: RGBA = [cueRed, cueGreen, cueBlue, RING_ALPHA]
 
/** Largest 1-2-5 step that keeps all rings inside the view. */
export const autoStep = ([, south, , north]: Box, unit: RingUnit): number => {
  const halfHeight = ((north - south) * METRES_PER_DEGREE_LATITUDE) / 2
  const target = (AUTO_FILL * halfHeight) / RING_COUNT / unitMetres[unit]
  return Option.getOrElse(
    Array.findLast(NICE_STEPS, step => step <= target),
    () => Array.headNonEmpty(NICE_STEPS),
  )
}
 
export const ringStep = (
  spacing: RangeRings.RingSpacing,
  bounds: Box,
  unit: RingUnit,
): number => (spacing === 'Auto' ? autoStep(bounds, unit) : Number(spacing))
 
/** Keep longitudes continuous around the cue so a ring that crosses the antimeridian does not jump. */
const unwrapAround = (centre: number, lon: number): number => {
  const delta = ((((lon - centre) % 360) + 540) % 360) - 180
  return centre + delta
}
 
export const ringPath = (cue: LatLon, radiusMetres: number): Array<Position> =>
  Array.makeBy(360 / BEARING_STEP_DEGREES + 1, index => {
    const point = destination(cue, index * BEARING_STEP_DEGREES, radiusMetres)
    return [unwrapAround(cue.lon, point.lon), point.lat]
  })
 
export const ringLabelText = (
  index: number,
  step: number,
  unit: RingUnit,
): string => `${Number((index * step).toFixed(2))} ${unitSymbol[unit]}`
 
type Ring = Readonly<{ path: Array<Position>; label: GridLabel }>
 
export const buildRings = (
  cue: LatLon,
  step: number,
  unit: RingUnit,
): ReadonlyArray<Ring> =>
  Array.makeBy(RING_COUNT, offset => {
    const index = offset + 1
    const radius = index * step * unitMetres[unit]
    const north = destination(cue, 0, radius)
    return {
      path: ringPath(cue, radius),
      label: {
        position: [cue.lon, north.lat],
        text: ringLabelText(index, step, unit),
        size: 11,
      },
    }
  })
 
const RING_WIDTH = 1.25
 
export const ringFeatures = (
  rings: ReadonlyArray<Ring>,
): ReadonlyArray<GeoJSON.Feature<GeoJSON.LineString>> =>
  Array.map(rings, ring =>
    lineFeature(ring.path, {
      layer: 'range-rings',
      color: RING_COLOR,
      width: RING_WIDTH,
      rank: LineRank.Rings,
    }),
  )