Skip to content

Latest commit

 

History

History
147 lines (99 loc) · 16.2 KB

File metadata and controls

147 lines (99 loc) · 16.2 KB

CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

Project Overview

azMap is a C application that renders an interactive map projection using OpenGL. It supports two projection modes — azimuthal equidistant (full Earth) and orthographic (hemisphere sphere view) — toggled via a UI button. Given a center lat/lon, it projects the world map and draws a line to a target location, showing distance and azimuth information.

Build

Dependencies: GLFW3, GLEW, shapelib, libcurl, OpenGL 3.3+

# Install dependencies (Arch/Manjaro)
sudo pacman -S glfw shapelib glew curl

# Build
mkdir -p build && cd build && cmake .. && make

# Install to ~/.local (default prefix)
cmake --install .

# Or install system-wide
cmake -DCMAKE_INSTALL_PREFIX=/usr ..
sudo cmake --install .

Run

# From the build directory
./azmap <center_lat> <center_lon> <target_lat> <target_lon> [options]
./azmap <target_lat> <target_lon> [options]   # center from config

# Example: center on Madrid, line to Paris
./azmap 40.4168 -3.7038 48.8566 2.3522 -c Madrid -t Paris

# With config file providing center:
./azmap 48.8566 2.3522 -t Paris

Config File

Optional. Place at ~/.config/azmap.conf:

# azMap configuration
name = Madrid
lat = 40.4168
lon = -3.7038

# QRZ.com credentials (optional, for callsign lookup)
qrz_user = YOURCALL
qrz_pass = yourpassword

Lines starting with # are comments. Whitespace around = is ignored. lat and lon must both be present to be used. qrz_user and qrz_pass enable QRZ callsign lookup. CLI args always override config values.

Options

  • -c NAME — Center location name (displayed as label; overrides config name)
  • -t NAME — Target location name (displayed as label)
  • -d DETAIL — Station detail string (station|freq|country|site|lang|target), parsed into sidebar info
  • -s PATH — Shapefile path override

For backward compatibility, a bare fifth argument is still accepted as the shapefile path.

Map data (Natural Earth 110m)

Download and extract into data/:

Controls

  • Scroll: zoom in/out (10 km to full Earth)
  • Left-drag / arrow keys: pan
  • R: reset view
  • Q / Esc: quit

Architecture

src/
├── main.c          Entry point, GLFW window, main loop, CLI arg parsing, label building
├── config.h/c      Config file parser (~/.config/azmap.conf)
├── projection.h/c  Map projection math (azimuthal equidistant + orthographic modes)
├── map_data.h/c    Shapefile loading (shapelib), vertex array management, reprojection
├── grid.h/c        Grid generation (range rings + radials for azeq; parallels + meridians for ortho; distance circles)
├── solar.h/c       Subsolar point calculation from UTC time
├── nightmesh.h/c   Day/night overlay mesh generation (per-vertex alpha)
├── overlay.h/c     MUF contour lines (KC2G GeoJSON) + Sporadic E contours (KC2G stations JSON, IDW + marching squares) + aurora heatmap (NOAA OVATION) + Kp/Bz indices (NOAA SWPC)
├── fetch.h/c       Threaded non-blocking HTTP fetch (libcurl + pthread)
├── cJSON.h/c       Vendored cJSON library (MIT) for JSON parsing
├── renderer.h/c    OpenGL shader compilation, VAO/VBO management, draw calls
├── camera.h/c      Orthographic view state (zoom_km, pan offset), MVP matrix
├── input.h/c       GLFW callbacks: scroll→zoom, drag→pan, popup drag, keyboard shortcuts
├── ui.h/c          UI system: buttons, draggable popup panel, text input
├── text.h/c        Vector stroke font (uppercase + lowercase + punctuation) for on-screen text
└── qrz.h/c        QRZ.com callsign lookup via XML API (libcurl)
shaders/
├── map.vert        Vertex shader (MVP transform + per-vertex alpha passthrough)
└── map.frag        Fragment shader (uniform color * vertex alpha)

Projection modes: Two modes selectable via the "Proj" UI button. Azimuthal equidistant (PROJ_AZEQ) maps the entire Earth to a disc of radius ~20015 km. Orthographic (PROJ_ORTHO) shows one hemisphere as a sphere of radius 6371 km; back-hemisphere points are clipped (return -1, coords set to 1e6 triggering the split threshold). projection_forward_clamped() is an alternative that clamps ortho back-hemisphere points to the boundary circle instead of 1e6 — used by land polygon fill and the great circle target line. projection_get_radius() returns the mode-appropriate Earth radius.

Coordinate system: The projection outputs x,y in kilometers from the center point. The camera builds an orthographic matrix mapping km-space to clip space. zoom_km controls the visible diameter (10–40030 km).

Data flow: Shapefiles are loaded once → raw lat/lon stored in map_data.c statics → projected to km via projection_forward() → uploaded to GPU as VBOs → drawn as GL_LINE_STRIP segments per polyline. Land polygons use map_data_reproject_nosplit() which clips polygon rings to the clipping boundary — hemisphere for ORTHO, 175° distance for AZEQ — bisecting edges at boundary crossings, then projects with projection_forward_clamped(). Grid geometry is generated procedurally: range rings + radials in km-space for azeq mode (grid_build()), or parallels + meridians through projection_forward() for ortho mode (grid_build_geo()). Distance circles are generated separately via grid_build_dist_circles().

Rendering layers (drawn back to front): Earth filled disc (ocean, dark blue-gray) → land fill (medium gray, stencil buffer) → Earth boundary circle (dark blue) → grid rings+radials (dim) → distance circles (blue-gray, km-space) → night overlay (semi-transparent, smooth gradient) → aurora overlay (green heatmap, per-vertex alpha) → DRAP overlay (red-orange heatmap, per-vertex alpha) → country borders (gray) → coastlines (dark gray) → MUF contour lines (per-segment color) → Sporadic E contour lines (two-pass glow: wide translucent + narrow bright core) → target line (yellow, great circle arc) → center marker (white filled circle) → target marker (red outline circle) → north pole triangle (white) → location labels (cyan/orange, pixel-space) → distance circle labels (muted blue, pixel-space) → sidebar legend sections (MUF swatches, foEs swatches, GEOMAG indices, DRAP peak HAF, each with header + separator line, pixel-space) → HUD text overlay (white, pixel-space).

Land fill: Uses stencil buffer inversion technique for non-convex polygon fill. Step 1: mark the earth disc in stencil bit 7. Step 2: draw each land polygon ring as GL_TRIANGLE_FAN with GL_INVERT on lower stencil bits, restricted to disc area via stencil test. Step 3: draw disc with land color where stencil > 0x80 (disc + odd inversions). This handles concave polygons and holes (Caspian Sea, etc.) via the odd-even rule. In ortho mode, map_data_reproject_nosplit() clips polygon rings to the front hemisphere: edges crossing the boundary are bisected to find the intersection lat/lon, back-hemisphere vertices are discarded, and boundary crossing points are inserted. After projection, shortcut edges between boundary crossings are replaced with 12-point arc segments along the hemisphere boundary circle to prevent incorrect stencil fan sweeps. In AZEQ mode, the clipping boundary is a 175° angular distance threshold from the projection center (via projection_distance()); edges crossing this boundary are clipped using the same bisection approach as ORTHO. The unified is_back_vertex() function abstracts the ORTHO/AZEQ boundary difference. Boundary arc insertion works for both modes, using clip_max_dist (175° in km) as the arc radius for AZEQ. Per-segment segment_clamped flags track which segments were skipped.

Distance circles (grid.c): Concentric circles showing great-circle distance from the center (source) location, drawn at 2000 km intervals (DIST_CIRCLE_STEP_KM) up to ~20000 km. Built by grid_build_dist_circles() which computes 90 destination points per circle using the great-circle forward formula (given center lat/lon, distance, and bearing), then projects each point via projection_forward(). Works in both AZEQ and ORTHO modes with back-hemisphere clipping. Stored in a separate MapData with dedicated dist_vao/dist_vbo in the renderer. Distance labels ("2000 km", "4000 km", etc.) are positioned at the due-north point of each circle, projected to pixel-space via the MVP matrix each frame, and rendered as vector text. Circles and labels are rebuilt on projection center change and mode toggle.

Target line: The center-to-target line is rendered as a great circle arc (101-point GL_LINE_STRIP). Intermediate points are computed via spherical linear interpolation (slerp) and projected through projection_forward_clamped(). In azeq mode centered on the origin, the great circle naturally appears as a straight line. In ortho mode, it appears as a curved arc.

Day/night overlay: solar.c computes the subsolar point from system UTC time. nightmesh.c generates a polar mesh (180x60) covering the Earth disc using projection_get_radius() for the disc extent (with a small inset to avoid float-precision boundary misses at the limb); each vertex gets a per-vertex alpha based on solar zenith angle using a smoothstep function (transparent at zenith<=80°, max opacity at zenith>=108°). The mesh is regenerated every 60 seconds (and on projection toggle). The vertex shader passes per-vertex alpha to the fragment shader, which multiplies it with the uniform color alpha. Non-night geometry uses a default vertex alpha of 1.0 via glVertexAttrib1f(1, 1.0f).

Text rendering: Uses a built-in vector stroke font (text.c) — uppercase A–Z, lowercase a–z (x-height 0.4–1.0, ascenders to 0.0, descenders to 1.2), digits, and punctuation are defined as line segments, rendered with GL_LINES using the same shader. No external font dependencies.

HUD text: The top-center overlay shows distance/azimuth info and live local/UTC clocks (using reentrant gmtime_r/localtime_r), rebuilt every second in the main loop. Distance and azimuth are always computed from the original center coordinates (CLI args or config), not from the panned view center (input.center_lat/lon). This ensures consistent readings regardless of map panning.

Labels: Location labels are rebuilt each frame by projecting marker km-positions through the MVP to screen coordinates, then rendered in pixel-space. The center label is cyan and the target label is orange.

UI popup: The UIPopup struct stores position offset (offset_x, offset_y) for drag support. The popup is centered on show (ui_show_popup() resets offsets) and can be dragged by its title bar. input.c detects title-bar presses via hit-testing the top 30px of the popup bounds, then accumulates cursor deltas into the offset during drag. The popup_dragging flag in InputState distinguishes popup drags from map pans. Clicks outside the popup pass through to button hit-testing; clicks inside are consumed.

Sidebar panel: The sidebar is always visible (no toggle). It displays UTC/local clocks, station info from the swl dashboard (received via FIFO or -d CLI arg), and distance/azimuth readouts. The bottom of the sidebar has two sections with labeled button groups: "LAYERS" (Aurora, E's, MUF, DRAP) and "SOURCE" (QRZ, DIGI, SWL). Section labels and horizontal divider lines are rendered as part of the button text geometry in full-window coordinates. Above the LAYERS label, up to three legend sections are rendered bottom-to-top: MUF (colored swatches + MHz labels), foEs (Sporadic E swatches + MHz labels), and GEOMAG (Kp + Bz indices). Each section has a header title and separator line. Sections only appear when their respective overlay is active and has data.

Button system: Buttons use rounded rectangles (emit_rounded_rect() for fill, emit_rounded_rect_outline() for border) with per-button vertex offsets/counts. Three visual states: normal (dark), hovered (lighter), active (blue highlight). Active state is indicated by a bitmask (btn_active_mask) so multiple buttons can be active simultaneously (e.g. PROJ in ortho mode + Aurora + E's + MUF). The PROJ button is always in the upper-left of the map; HOME is bottom-center of the map; sidebar buttons are positioned in two rows at the bottom of the sidebar. Buttons are drawn in a separate full-window viewport pass (renderer_draw_buttons()) after the sidebar, so they can span both map and sidebar areas.

Mode buttons: QRZ opens a popup for callsign entry; on successful lookup the popup closes and results (call, name, location, grid, coordinates) display in the sidebar station_info fields, with target line/marker/dist/az updating. DIGI opens a popup (placeholder). SWL clears all info. All three clear previous station info, distance/azimuth, target line, target marker, and target label when clicked.

MUF overlay (overlay.c): Fetches GeoJSON from KC2G (prop.kc2g.com/renders/current/mufd-normal-now.geojson). Each LineString feature has level-value (MHz) and stroke (hex color). Parsed into MufData with raw lat/lon for reprojection. Segments split at >5000 km jumps. Legend entries (unique MHz/color pairs, sorted ascending) displayed in sidebar. Reprojected on center/mode change.

Sporadic E overlay (overlay.c): Fetches station JSON from KC2G (prop.kc2g.com/api/stations.json). Extracts foEs (sporadic E critical frequency) values from ionosonde stations. Interpolates sparse station data onto a 2° regular grid using inverse distance weighting (IDW, power=2, max radius 2500 km). Grid validity is tracked with a separate int array (not float sentinels). Contour lines at 3/5/7/10/14 MHz are extracted via marching squares with saddle case disambiguation (cell center average). Edge-crossing fragments are chained into polylines by endpoint matching. Reuses the MufData struct for storage and muf_reproject() for projection. Rendered with a two-pass glow effect (wide translucent pass + narrow bright core). Legend entries displayed in the foEs sidebar section.

Aurora overlay (overlay.c): Fetches JSON from NOAA OVATION (services.swpc.noaa.gov/json/ovation_aurora_latest.json). Grid of [lon, lat, probability] triplets at 1° resolution stored in AuroraGrid (360×181 array). Mesh built same as nightmesh (180×60 polar grid) with probability→alpha mapping (0–5%→0, 5–50%→ramp, 50–100%→max). Rebuilt on center/mode change.

DRAP overlay (overlay.c): Fetches D-Region Absorption Prediction text from NOAA SWPC (services.swpc.noaa.gov/text/drap_global_frequencies.txt). Tabular grid of Highest Affected Frequency (HAF) in MHz at 4° lon × 2° lat resolution (90×90 grid). Parsed from text format (comment lines starting with #, longitude header row, then latitude + frequency data rows). Stored in DrapGrid with bilinear interpolation lookup. Mesh built same as aurora (180×60 polar grid) with HAF→alpha mapping (0–1 MHz→0, 1–5→ramp to 0.25, 5–15→ramp to 0.5, 15–30→ramp to 0.7). Rendered as red-orange heatmap. Legend shows peak HAF value. Rebuilt on center/mode change.

Async fetch (fetch.c): Non-blocking HTTP GET via libcurl in a detached pthread. fetch_start() spawns thread, fetch_check() polls status each frame (mutex-protected), fetch_take_response() transfers ownership. All four overlays (MUF, Sporadic E, Aurora, DRAP) plus Kp/Bz indices auto-refresh every 15 minutes (OVERLAY_UPDATE_SEC) while active. In-flight fetches are cleaned up at exit.

FIFO IPC: azMap creates a named pipe at /tmp/azmap-target.fifo (O_RDWR | O_NONBLOCK) for receiving target updates from the swl dashboard. Wire format: lat,lon,name|station|freq|country|site|lang|target\n. The |-delimited detail fields after the name are parsed into ui.station_info[] for sidebar display. The -d CLI option provides the same detail string at launch time (used by swl when spawning a new azMap instance).