Tags · NASA-AMMOS/MMGIS · GitHub
Skip to content

Tags: NASA-AMMOS/MMGIS

Tags

5.2.5

Toggle 5.2.5's commit message

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
security: XSS prevention, CI hardening, geodataset validation, log sa…

…nitization (#1006)

* perf: re-enable Numba JIT after benchmarking (saves ~6s per frame)

Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>

* debug: re-add timing instrumentation to sightmap response

Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>

* perf: compute grid convergence analytically, skip GDAL TransformPoints

_compute_directions was calling _pixels_to_geo_batch on all 940K cells
(969x969 grid) to get each cell's longitude for the convergence angle.
This took ~1s (29% of total).

Now computes convergence directly from projected coordinates using
atan2(x - false_easting, sign*(y - false_northing)) — pure numpy
math, no GDAL coordinate transform. Expected: ~0.01s.

Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>

* perf: skip kernel unloading + increase coarse subgrid step to 50

- Remove spiceypy.unload() calls — process exits immediately after
  response so OS cleanup is sufficient. Saves ~0.235s.
- Increase COARSE_AZEL_STEP from 10 to 50 — reduces coarse subgrid
  points from ~9400 to ~400 for sun az/el interpolation. Sun position
  varies slowly across the DEM so fewer sample points suffice.

Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>

* chore: remove timing debug instrumentation from sightmap.py

Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>

* fix: interpolate sun azimuth in sin/cos space to prevent wrap artifacts

When coarse subgrid has azimuth values near the 360°/0° boundary
(e.g. 355° and 5°), linear interpolation produces ~180° — completely
wrong direction that flips shadows. Now interpolates sin(az) and
cos(az) separately, then recovers the angle via atan2. This handles
the circular wrap correctly.

Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>

* feat: viewport-aware sightmap — clip DEM read to visible area at native resolution

Frontend sends current map viewport bounds (projected coords) with each
static sightmap request. Backend clips the DEM read to the viewport
intersection, reading at native resolution (capped at maxOutputDim).

When zoomed in, this means the sightmap covers just the visible area
but at much higher resolution than the full-DEM downsampled version.
When zoomed out, viewport encompasses the full DEM and behavior is
unchanged.

For a 30993x30993 DEM zoomed to 1/10th coverage, instead of reading
the full raster decimated to 1600x1600, we read only ~3000x3000 native
pixels for the visible window — higher res and faster.

Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>

* fix: compute viewport projected bounds from container corners, not lat/lng bbox

In polar stereographic CRS, the lat/lng bounding box from
getBounds() maps to a distorted region in projected space.
Now samples all 4 container pixel corners, projects each through
the CRS, and takes the envelope for correct projected bounds.

Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>

* fix: position sightmap overlay using projected NW/SE corners directly

In polar/rotated CRS, L.latLngBounds normalises by min/max lat/lng,
which shuffles corners — getNorthWest() and getSouthEast() return
points that don't correspond to the projected rectangle's NW and SE.
The overlay is then mispositioned and stretched.

New _projImageOverlay() helper overrides the overlay's _reset method
to compute pixel position from the projected NW (xmin, ymax) and
SE (xmax, ymin) corners via latLngToLayerPoint, bypassing the
normalisation entirely.  Applied to all three overlay creation paths:
static sightmap, heatmap, and sweep frame.

Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>

* fix: override _animateZoom on projected overlay to prevent zoom jump

The default _animateZoom reads the normalised L.latLngBounds which
has wrong corners in polar CRS, causing the overlay to briefly jump
to the top-left during zoom transitions.  Now _animateZoom uses the
projected NW corner via _latLngToNewLayerPoint for correct animation.

Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>

* feat: wire viewport bounds through batch/sweep mode

Frontend sweep call now sends viewportBounds.  Backend
compute_sightmap_batch accepts and passes viewport_bounds
to open_dem so playback also clips to the visible DEM region
at native resolution.

Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>

* fix: equalize sweep/static resolution + show 'Sweeping' on progress button

- _resolutionToMaxDim now returns 800 for both modes (was 400 for sweep)
- ProgressButton label shows children text alongside percentage
- SightlineElement shows 'Sweeping'/'Generating' while loading

Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>

* fix: actually commit _resolutionToMaxDim change to equalize sweep/static res

Was missed from the previous commit — sweep was still getting 400 instead of 800.

Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>

* fix: re-fetch horizon profile after pan so charts stay in sync

When the user panned, invalidateHorizonCache set _horizonCache to null
but never triggered a re-fetch.  Subsequent scrub or playback frame
changes found the cache empty and either skipped the horizon redraw or
drew the visibility timeline with no profile (marking everything not
visible).

- _onPanEnd now calls invalidateAndRefetch() which re-fetches the
  horizon profile if the graph panel is open.
- _scrubToFrame and updatePlaybackFrame fall back to fetchAndDrawHorizon
  when the cache is null instead of drawing with missing data.

Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>

* fix: correct horizon profile azimuth for projected CRS (polar stereo)

HorizonProfile.py was marching along pixel/raster-space directions,
treating pixel-up as north.  In polar stereographic the grid north
axis is rotated from true north by the grid convergence angle, so the
profile azimuths were offset and the terrain silhouette appeared
rotated in the chart.

Added _grid_convergence() which computes the convergence at the
observer's projected coordinates via atan2(x - FE, -(y - FN)).  Each
geographic azimuth is now rotated by the convergence before marching
in pixel space, making the profile azimuths true geographic azimuths.
No change for geographic (unprojected) CRS.

Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>

* fix: correct horizon profile convergence formula for polar stereo

Two bugs in the previous convergence fix:

1. _grid_convergence used atan2(x, -y) unconditionally, but for
   south-pole stereo the sign should be +1 (north is away from
   pole = positive y). Now uses north_sign = -1 for north-pole,
   +1 for south-pole, matching sightmap.py exactly.

2. The convergence was subtracted (geo_az - convergence) when it
   should be added (geo_az + convergence), matching sightmap.py's
   convention where convergence rotates from grid north to true
   north.

Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>

* refactor: extract rate limiters into shared scripts/rateLimiters.js module

- Create scripts/rateLimiters.js exporting apilimiter, authLimiter, computeLimiter
- Remove inline rateLimit() definitions from scripts/server.js
- Remove authLimiter/computeLimiter from the 's' setup object
- Update Users/routes/users.js: import authLimiter directly, replace late-binding wrapper
- Update Users/setup.js: remove router._authLimiter assignment
- Update Utils/routes/utils.js: import computeLimiter directly, replace all 8 late-binding wrappers
- Update Utils/setup.js: remove router._computeLimiter assignment
- Update unit tests to import from the shared module

Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>

* chore: bump version to 5.0.26-20260609 [version bump]

* fix: use geodesic destination for azimuth lines instead of angle rotation

Replace the _localNorthAngle + screen-space rotation approach with a
direct forward geodesic method: compute a destination lat/lng 1° along
the desired azimuth, project it through Leaflet, and draw the line.

This avoids potential compound angle errors and works correctly for
any CRS because it uses Leaflet's own projection pipeline end-to-end
rather than computing a screen-space north-offset angle and rotating.

Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>

* chore: convert reference mission DEMs to Cloud Optimized GeoTIFF (COG)

Both the Earth (USGS SF Hill) and Lunar South Pole (LRO LOLA 4000m)
DEMs are now tiled COGs with deflate compression and overviews.
This ensures consistent behavior with the sightmap COG requirement
and enables fast overview-based reads at any resolution.

Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>

* feat: add lunar LSMT to chronice, fix TimeUI indicator cleanup and observer time sync

- chronice.py: add lunar LSMT support using SPICE et2lst with observer
  longitude; format: LDAY-NNNNNLHH:MM:SS; reverse conversion via iterative
  refinement
- utils.js: pass optional lng param through to chronice.py
- SightlineTool.js: remove TimeUI indicator on mode switch, cancel sweep,
  resweep start, and pan-end; pass lng from observer point for LSMT observers
- SightlineElement.jsx: update global TimeControl when observer time inputs
  are changed (blur/Enter), fixing Mars SOL time not updating the TimeUI
- Lunar ref mission config: add Moon (LSMT) observer with type=lsmt

Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>

* fix: LSMT lng fallback to map center, hide empty DEM dropdown, Enter key on observer time

- _getObserverLng: fall back to map center when indicatorLastDragPoint is null
- SightlineElement: hide DEM dropdown when no data options configured
- SightlineElement: add onKeyDown Enter handler on observer time inputs

Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>

* Improve Mars Reference Mission

* chore: bump version to 5.0.28-20260610 [version bump]

* fix: sightmap overlay CRS mismatch for non-custom projections, restore config descriptions

- Only use _projImageOverlay and viewport clipping when the mission uses
  a custom projected CRS (projection.custom=true). For standard longlat/
  Mercator missions (like Mars), the DEM's projected bounds are in a
  different CRS than the map, causing misplaced overlays.
- Restore Layer-specific DEMs config row and improve field descriptions
  in sightline tool config.json (lost during ShadeTool→SightlineTool rename).
- Add sweepColorRamps and observer type examples to descriptionFull.

Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>

* fix: restore sightline config descriptions from ShadeTool, remove data row, clear default name

- Remove Layer-specific DEMs config row (previously asked to remove)
- Restore detailed descriptions from old ShadeTool config:
  - Sources: documents name/value properties, dropdown usage, kernel path
  - Observers: documents name/value/frame/body, chronos setup path
  - Default Height: full description of height parameter behavior
  - Observer Time Placeholder: documents format string usage
  - Frame/Body fields: proper SPICE reference descriptions
- Remove 'Sightline N' default element name (now empty)

Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>

* fix: observer time input 7hr drift — chronice result parsed as local instead of UTC

Root cause: chronice lmst→utc returns '2026-05-30T21:36:57.975' (no Z suffix).
The old ShadeTool correctly did: result.replace(' ', 'T') + 'Z'
The new code used a regex chain that failed when milliseconds were present
without a trailing Z, leaving the string timezone-ambiguous. new Date()
then parsed it as local time (UTC-7), adding ~7 hours.

Fix: strip milliseconds then unconditionally append Z, matching the old
ShadeTool approach. The /ZZ$/ → Z guard prevents double-Z if chronice
ever returns a Z-suffixed result in the future.

Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>

* fix: sightmap sun direction for cylindrical projections, 1s time drift, tab-switch regen, add editable time field

1. Sightmap sun direction: _compute_directions now only applies convergence
   rotation for azimuthal projections (stereo/gnomonic). For cylindrical
   projections (Equidistant Cylindrical, Mercator), grid north = geographic
   north so convergence = 0. Previously applied polar-stereo formula to all
   projected CRS, giving ~90deg rotation on Mars DEM.

2. Observer time 1-second drift: restored _lastConvertedMs pattern from old
   ShadeTool. Saves sub-second precision from observer->UTC conversion and
   re-attaches it in UTC->observer reverse conversion for exact round-trips.

3. Tab-switch regeneration: _onTimeChange now tracks _lastGeneratedTime and
   skips if unchanged, preventing redundant sightmap computation when
   TimeControl re-broadcasts the same time on tab refocus.

4. Editable time field (vstOptionTime): restored from old ShadeTool. Shows
   current end time in configured format (DOY, etc), editable on blur/Enter.
   Parses via utcTimeFormat if configured, else appends Z directly.

Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>

* fix: match old ShadeTool vstOptionTime styling and DOY format

- CSS matches old ShadeTool exactly: full-width centered input, bold 14px,
  color-p0 bg, color-a1-5 text, transparent border that shows color-c on focus
- Clock icon positioned absolute right (pointer-events: none) as in original
- Structure uses flexbetween wrapper matching old jQuery markup
- Mars reference mission utcTimeFormat changed to DOY: '%Y-%j %H:%M:%S'
  giving output like '2026-150 21:36:57' instead of ISO format

Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>

* ui: hide observer start time in static mode, show only 'Time' field

Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>

* fix: horizon profile rotation for cylindrical CRS + visibility chart text non-selectable

1. HorizonProfile.py _grid_convergence: same fix as sightmap.py — only
   apply convergence for azimuthal projections (stereo/gnomonic). For
   cylindrical projections (Mars Equidist. Cylindrical), convergence = 0,
   so horizon terrain profile azimuths are now correct.

2. Visibility chart (.sightlineVisWrap): added user-select: none so
   dragging the timeline scrubber doesn't accidentally highlight text.

Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>

* fix: horizon profile aspect ratio for geographic CRS + crosshair styling/lag

- HorizonProfile.py: compute per-axis pixel scales (px_scale_x, px_scale_y)
  so the march direction accounts for longitude compression at observer
  latitude. For geographic CRS at 38°N, 1° lon ≈ 0.79 × 1° lat in meters;
  without this the march traces wrong physical angles, distorting azimuths.
  Also computes correct per-step physical distance instead of using the
  averaged pixel_scale.

- Crosshair restyled: smaller (8px circle, 5px arms), lime green with
  black borders (box-shadow outline).

- Crosshair converted from raw DOM element to Leaflet DivIcon marker.
  Leaflet handles positioning in its own transform pipeline, eliminating
  the lag that occurred when updating CSS left/top on the move event.

Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>

* feat: add lime center dot at visible map center while sightline tool is open

Small 6px lime green dot with 1px black border, always at 50%/50% of
the map container (CSS-only positioning, no event tracking needed).
Added on make(), removed on destroy().

Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>

* fix: update crosshair position immediately when sweepCenter is set

Previously the crosshair only corrected its position on the next pan
event. Now _updateCrosshairPosition() is called right after sweepCenter
is stored for both static sightmap and batch/sweep completion.

Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>

* fix: sightmap route security + batch limits + test fix (Devin Review)

- Add SAFE_NAME_RE validation on target, obsRefFrame, obsBody to prevent
  directory traversal via SPICE kernel paths (matches /ll2aerll_bulk).
- Add MAX_TIMES=200 cap on sightmap batch to prevent resource exhaustion.
- Fix E2E test: batch response is a raw JSON array, not { results: [...] }.

Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>

* fix: add error handler + headersSent guards on sightmap spawn

Match ll2aerll_bulk pattern: handle child.on('error') and
child.stdin.on('error') to prevent hung responses if Python
fails to start. Add !res.headersSent guards on all response
paths in the close handler.

Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>

* fix: get_pixel_scale uses actual array rows instead of ds.RasterYSize

After open_dem decimates a large DEM, gt[5] is scaled but ds still has
the original RasterYSize. Using ds.RasterYSize with the decimated gt
produces a wrong mid_lat for geographic CRS pixel scale. Now accepts
dem_rows directly from dem.shape.

Also: encodeURIComponent the chronice lng argument to match the other
CLI args (consistency with unquote() on the Python side).

Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>

* fix: correct East vector cross product order in sun_azel_at_cell

cross(normal, north) yields West, not East. Changed to
cross(north, normal) to match the batch version _sun_azel_batch.
Currently unused at runtime but prevents future bugs.

Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>

* refactor: remove dead code from sightmap.py

Removed unused scalar functions that were superseded by vectorized
equivalents: sun_azel_at_cell (replaced by _sun_azel_batch),
is_nodata (replaced by _vectorized_is_nodata), geo_to_pixel (never
called). Also removed the unused ds parameter from open_dem return
value and _compute_bounds signature — ds was only kept alive for
get_pixel_scale which no longer needs it after the dem_rows fix.

Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>

* refactor: remove dead code from SightlineTool frontend

SightlineTool.js: removed showSightlinemapLayers, showSweepLayers,
refreshAllHeatmaps, _nextPow2 — all defined but never called.

SightlineTool_Algorithm.js: removed the entire old client-side
sightline algorithm (sightline, processUp/Down, mask, curveData,
isNoData, compositeResults, calcHeight*, initializeGrids, perOctant)
and their unused imports (jquery, F_, L_, G_). Only
cumulativeVisibility is called externally; all other methods were
from the pre-backend era and superseded by sightmap.py.

SightlineTool_Graphs.js: removed _localNorthAngle, replaced by
the geodesic _destinationPoint + _azimuthEndpoint approach.

Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>

* fix: color picker/ramp dropdown clipping + reorder default colors

Remove overflow:hidden from vstSightlineItem, vstSweepCard, and
vstSweepCardsSection so absolutely-positioned color picker palettes
and color ramp dropdowns are no longer clipped by their parent
containers. Add border-radius to headers directly to preserve
rounded corners.

Bump vstColorPalette z-index from 100 to 10000 to match the
ColorRampPicker popup z-index.

Reorder MULTI_SOURCE_COLORS: yellow -> blue -> red -> green
(swapped blue and red positions).

Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>

* fix: improve LSMT inverse conversion precision from ~30s to <1s

et2lst returns integer (hr, mn, sc) so one lunar second spans ~29 ET
seconds. The old iterative loop converged to ±1 lunar second, giving
~30s UTC precision. Now uses binary search after the coarse loop to
find the exact ET boundary where the second ticks over, narrowing to
<0.5 ET seconds. Result is placed at the midpoint of the lunar
second window for minimal round-trip error.

Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>

* fix: color picker palette uses fixed positioning to escape overflow

The Collapsible panel has overflow:hidden for its open/close
animation, which clips the color picker dropdown. Changed the
palette to position:fixed, computed from the swatch's bounding
rect on click, so it escapes all overflow containers.

Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>

* fix: revert fixed positioning, use overflow:visible on open panels

Reverts position:fixed approach. Instead overrides overflow to
visible on open Collapsible panels inside sightlineTool via
[data-open] selector, so the color palette can extend past the
panel boundary while keeping overflow:hidden during animations.

Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>

* feat(sightline): implement 6 improvements to Sightline tool

1. Combine Playback and Composite Sweeps: always build both heatmap
   and atlas after sweep; mode switching is instantaneous without
   re-running the sweep.

2. Min/Max Distance options: add UI inputs and pass minDistance/
   maxDistance through to sightmap.py ray-march kernel and
   HorizonProfile.py. Includes curvature-based early termination.

3. Better Color Ramps + Fix Transparency Bug: expose sweepColorRamps
   in admin config, reorder defaults, fix evalColor/evalColorWithStops
   discrete bin index calculation (Math.floor -> Math.round).

4. Draggable Horizon Profile Point: crosshair marker is now interactive
   and draggable; on dragend updates indicatorLastDragPoint and refetches
   horizon profile at the new location.

5. Vis Chart - Remove Gradients: remove gradient transition logic from
   drawVisibilityTimeline; uncertain regions shown as occluded.

6. Document Algorithms: add detailed algorithm documentation to
   sightmap.py and HorizonProfile.py headers.

Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>

* chore: bump version to 5.1.1-20260611 [version bump]

* fix: resolve merge conflict in configure/package.json

Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>

* fix(sightline): set mode before sweepShowAllFrames, add distance to horizon cache key

- switchElementMode now sets sightlineMode='playback' before calling
  sweepShowAllFrames, which filters by sightlineMode.
- Horizon profile cache now includes maxDist/minDist so changing distance
  parameters invalidates the cache and triggers a re-fetch.

Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>

* fix(sightline): address 4 review issues

1. Default color ramps: add inferno + viridis to front of defaults;
   remove sweepColorRamps from all reference mission configs so they
   use the built-in defaults.

2. Mode switching: keep Results section open when switching between
   composite/playback (don't collapse if sweep data exists); call
   sweepShowFrame directly for the element when switching to playback.

3. Crosshair dragging: use Leaflet.Editable (enableEdit/disableEdit)
   instead of L.marker draggable option; listen on 'editable:dragend'.
   Map click handler now only triggers static sightline when element
   is in static mode - prevents unwanted resweep in playback mode.

4. maxDistance fix: backend route (utils.js) was not passing
   minDistance/maxDistance through to the Python sightmap.py stdin
   payload. Added both fields to payloadObj.

Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>

* fix(sightline): hardcode 6 color ramps, revert crosshair to non-draggable

1. Color ramps: remove custom/configurable sweepColorRamps entirely.
   Hardcode exactly 6 ramps:
   - [transparent, color]
   - [transparent, color, transparent]
   - Inferno
   - Viridis
   - Red → Green (RdYlGn)
   - Black → White (Greys)

2. Crosshair: revert to original non-draggable behavior (interactive: false).
   Remove indicatorLastDragPoint from store and all references.
   Horizon profile and visibility chart now always use the current map
   center — they auto-update on pan end via the existing
   invalidateAndRefetch() call in _onPanEnd.

Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>

* fix(sightline): reverse B&W ramp to White→Black, single color stop

Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>

* fix(sightline): anchor horizon profile to sweep center when available

Revert the horizon profile to use sweepCenter when a sweep exists,
falling back to the current map center when no sweep has been run.
This keeps the horizon chart, entity arcs, and visibility timeline
all consistent with the sweep observer location.

Skip horizon invalidation on pan when anchored to sweep center
to avoid unnecessary backend calls.

Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>

* perf(sightline): add timing instrumentation to sightmap pipeline

Python backend (sightmap.py):
  - Timing for: kernel loading, SPICE az/el, DEM open, grid precompute,
    per-frame sun grid + march + tolist, json.dumps, total
  - Batch response now returns {results, _timing} with per-frame arrays
  - DEM/output dimensions logged for context

Node.js route (utils.js):
  - Log total spawn-to-close time, JSON parse time, stdout size

Frontend (SightlineTool.js):
  - Log API round-trip, grid parsing, heatmap compute, atlas build,
    total frontend processing
  - Console output tagged [Sightmap Timing] / [Sightmap Sweep]

Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>

* TEMP: strip grid data from sightmap response for timing debug

Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>

* perf(sightmap): cache frame-invariant data in batch mode

Pre-compute and cache across all frames:
- Coarse grid lat/lng (coordinate transform done once, not per-frame)
- Bilinear interpolation weights (indices + weight arrays)
- Ellipsoid geometry for az/el (cell positions, normals, north/east vectors)
- Grid convergence angle for azimuthal projections

Per-frame now only computes:
- Source direction vector subtraction on ~119 coarse points
- 3x bilinear weight-apply (fast matmul, no index recomputation)
- sin/cos for direction on full grid

Expected speedup: source_grid from ~116ms/frame to ~15-25ms/frame
(~5-8x faster for 145 frames: ~17s → ~2-4s)

Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>

* perf(sightmap): eliminate full-grid trig via angle addition formula

Instead of: bilinear interp sin/cos → arctan2 → +convergence → radians → sin/cos
Now:        bilinear interp sin/cos → multiply by cached sin/cos(convergence)

Removes 6 numpy operations on the full 240K-cell grid per frame
(arctan2, where, add, radians, sin, cos) and replaces them with
4 multiply + 2 add operations (cheaper element-wise math).

Also computes coarse az as normalized sin/cos components directly
from the dot products, skipping degrees/radians conversions on
the coarse grid too.

Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>

* feat(sightline): add resolution dropdown + restore grid data

Add viewport-relative resolution control (1×, 0.5×, 0.25×, 0.125×)
to the Display section. Default is 0.25× (medium). The scale factor
multiplies the viewport's longest pixel dimension to produce
maxOutputDim sent to the backend.

- 1× = native (no decimation beyond viewport crop)
- 0.5× = half viewport dims
- 0.25× = quarter (default, balanced speed/quality)
- 0.125× = eighth (fastest, coarsest)

Server-side clamp raised from 800 to 4096 to support 1× on large
viewports.

Also restores grid data in sightmap responses (reverts the TEMP
debug strip).

Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>

* feat(sightmap): scale frame limit by resolution

1× (maxDim≥800): 256 frames
0.5× (maxDim≥400): 512 frames
0.25× (maxDim≥200): 1024 frames
0.125× (maxDim<200): 2048 frames

Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>

* refactor(sightmap): replace times array with startTime/endTime/stepSeconds

Send 3 scalar values instead of potentially 2000+ timestamp strings.
Backend generates timestamps internally from the range.

- Frontend sends ISO timestamps (e.g. '2026-06-11T19:53:00Z') and
  stepSeconds (e.g. 60)
- Node.js route validates the range and computes frame count for
  limit checking
- Python parses ISO start/end, generates time strings with timedelta
- Frame limit raised to 2048 max (at 0.125× resolution)
- Shrinks request payload from ~100KB to ~500B for large sweeps

Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>

* feat(sightline): range circles, max distance infinity toggle, viewport padding

- Draw dashed min/max distance circles on map when values are non-zero
  (orange for min, blue for max). Circles update on value change and
  center on sweep observer position when available.

- Add ∞ toggle button on Max Dist row. When active, sends maxDistance=-1
  to backend which skips viewport cropping and reads the full DEM (at
  the appropriate overview level for the resolution setting).

- Add Tooltip on Max Dist label explaining viewport-only vs full DEM
  behavior.

- Backend: when maxDistance > 0, pad viewport bounds by that distance
  in all directions so shadow-casting terrain beyond the visible extent
  is included. When maxDistance=-1 (infinity), viewport_bounds is set
  to None so the full DEM is read.

- Store: add maxDistInfinity per-element field (default false).

Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>

* fix: range circles use projected polygon, infinity mode preserves viewport resolution

Range circles:
- Replace L.circle (broken on custom planetary CRS) with L.polygon
  built from 64 vertices computed in projected coordinates via
  crs.project/unproject. Works correctly on polar stereo and any
  custom L.Proj.CRS.

Infinity mode resolution fix:
- Separate DEM read extent from output grid extent. When infinity
  mode is active (maxDistance=-1), the full DEM is loaded at overview
  resolution for shadow tracing, but the output grid is sized to the
  viewport subset only — preserving the same cell density as normal
  mode.
- When maxDistance > 0 (finite padding), DEM read bounds are padded
  by maxDistance in all directions. Output grid still viewport-only.
- Both compute_sightmap and compute_sightmap_batch support
  output_offset_row/col parameters that offset output grid cells
  within the loaded DEM. Coarse subgrid, precompute, and batch
  context all respect the offset so rays trace through the full
  loaded DEM while only evaluating viewport cells.
- Add _compute_bounds_from_proj() to compute geographic bounds from
  projected viewport coords for correct response bounds.

Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>

* refactor: replace min/max distance fields with DEM Extent dropdown

Remove minDistance, maxDistance, maxDistInfinity fields and range
circles. Replace with a simple 'DEM Extent' dropdown (Viewport / Full
DEM) in the Display section, defaulting to Viewport.

- Viewport: uses only the DEM visible on screen (fast)
- Full DEM: reads the entire raster for distant shadow casting (slower,
  sends maxDistance=-1 to backend)

Removes ~120 lines of range circle rendering code that is no longer
needed.

Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>

* Revert "refactor: replace min/max distance fields with DEM Extent dropdown"

This reverts commit b3d6cc503a170a522f48bdd7f5b21d723b9612e7.

* Revert "fix: range circles use projected polygon, infinity mode preserves viewport resolution"

This reverts commit 6b265aa823e9634e13d5857119826b174360d384.

* Revert "feat(sightline): range circles, max distance infinity toggle, viewport padding"

This reverts commit fd0c5c2262d9c2bccffa69102709be759333c1ad.

* feat(sightline): add Shadow Reach LOD field for distant shadow casting

Add a 'Shadow Reach' input (km) to the Display section that extends
terrain loaded for shadow computation beyond the visible viewport.
When set, the backend reads the viewport DEM at full resolution and
a padded border region at a coarser COG overview level, composites
them into a single array, and marches rays through the full composite
while outputting only the viewport portion.

- New open_dem_composite() reads viewport + low-res border
- Output grid offset support in _precompute_grid_arrays,
  _compute_sun_grid, _precompute_batch_grid_context
- Replaces minDistance/maxDistance with single shadowReach parameter
- Tooltip explains the viewport-extension concept
- Default 0 = viewport only (no performance impact)

Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>

* fix(sightline): prevent OOM on large shadow reach, clamp to curvature, blur-only regen

- Redesigned open_dem_composite(): single DEM read of the padded
  region at a managed resolution (scales working_dim by viewport/pad
  ratio, capped at 4× max_working_dim) instead of building a massive
  array at viewport pixel scale then upsampling.
- Shadow reach clamped to planetary curvature limit: sqrt(2*R*h_max)
  where h_max=10km. For Moon (R=1737km) this caps at ~186km.
- Shadow Reach input only triggers sightmap regen on blur (unfocus),
  not on every keystroke.

Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>

* fix(sightline): Enter key triggers regen on Shadow Reach input

Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>

* fix(sightline): respect drag order for sightmap z-index, fix drop indicator position

- After rendering a sightmap overlay, re-apply z-order based on the
  current element order in the store so the panel ordering is respected
  regardless of which sightmap finishes loading first.
- Fix drop indicator: changed border-bottom to border-top so the line
  appears above the target element (matching where the drop will place
  the dragged item).

Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>

* fix(sightline): position-aware drag-and-drop indicator (above/below)

The drop indicator now shows border-top when hovering the upper half
of a target element (insert above) and border-bottom when hovering
the lower half (insert below). The drop logic uses the cursor's Y
position relative to the element midpoint to determine placement.

Applied to both SightlinePanel element cards and SweepSection cards.

Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>

* fix(sightline): visibility timeline uses sightmap grid pixels, not horizon profile

- Visibility chart now checks the observer's pixel in each frame's
  sightmap grid (lit=1/2 → visible, shadow=0 → occluded) instead of
  comparing source az/el against the horizon profile.
- Observer pixel computed from projected coordinates (using CRS
  projection) when available, falling back to geographic bounds.
- Replaces horizon profile interpolation in drawVisibilityTimeline
  with direct grid pixel lookup via centerVisible.

Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>

* chore(sightline): remove timing logs and fix graph slider performance

- Remove all timing instrumentation from sightmap.py (timing dict,
  perf_counter calls, stderr serialization log, import time)
- Remove _timing from sightmap API responses (single + batch)
- Remove all console.log timing calls from SightlineTool.js
- Fix graph time slider performance: _scrubToFrame was redrawing
  horizon + visibility canvases synchronously AND triggering the
  same redraws again via the scrub callback (sweepShowAllFrames →
  updatePlaybackFrame → requestAnimationFrame). Now _scrubToFrame
  just sets the index and calls the callback, letting
  updatePlaybackFrame handle all redraws in a single rAF.

Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>

* feat(sightline): add distance-based fog shading to horizon profile

Backend (HorizonProfile.py):
- Track distance (meters) to the horizon point at each azimuth
- Output now includes [az, el, dist_m] per sample

Frontend (_drawHorizonCanvas):
- Parse distance from profile data (backward-compatible if missing)
- Replace uniform terrain fill with per-strip vertical fills
- Each strip's opacity mapped via log scale from distance:
  close horizon = opaque, far horizon = transparent
- Falls back to uniform fill if no distance data available

Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>

* fix(sightline): increase horizon profile fog opacity variation

Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>

* fix(sightline): horizon profile now refreshes after new sweep completes

updatePlaybackFrame was using _horizonCache directly without validating
it against the current sweep center. After a new sweep set a different
sweepCenter, the stale cached profile was still drawn. Now routes through
fetchAndDrawHorizon which validates cache keys (lat/lng/height) and
refetches when the center has changed.

Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>

* feat(sightline): add hover tooltip to horizon profile showing azimuth, elevation, and distance

Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>

* fix(sightline): increase horizon profile max radius from 5km to 50km

5km was too short to reach far crater rims on coarser DEMs, causing
the horizon profile to report deeply negative elevation angles where
the ray never found the actual skyline.

Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>

* fix(sightline): increase horizon profile max radius to 250km

Also raised the backend cap from 100km to 500km to accommodate.

Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>

* perf(sightline): add log stepping + early termination to horizon profile

Ray march now uses logarithmic step size (1px near, ~11px at 2500px)
so distant terrain is sampled more coarsely. Early termination stops a
ray once the maximum plausible terrain peak (10km, minus curvature)
at the current distance can't beat the already-found max elevation
angle. Together these reduce samples from ~2500/ray to ~200-600/ray
for 250km radius at 100m/pixel.

Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>

* feat(sightline): add header with dual-handle log-scale horizon distance slider

Adds a 'Sightline Graphs' title and a dual-handle range slider to the
bottom panel header. The slider controls min/max horizon lookup distance
on a log scale (1m–250km). Dragging either handle invalidates the cache
and refetches the horizon profile with the new range. Default: 100m–250km.

Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>

* fix(sightline): default horizon range to 1m–250km and add tippy tooltip

Changed min distance default from 100m to 1m. Added tippy tooltip on
the 'Horizon:' label explaining the dual-handle slider controls.

Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>

* fix(sightline): increase crosshair circle and center dot radius by 1px

Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>

* fix(sightline): deduplicate visibility chart time ticks and add user-select:none

When numTicks > frame count, multiple tick positions could round to the
same frame index, producing duplicate labels (e.g. two 'Jan 14 12:00').
Now skips any tick whose frameIdx matches the previous one. Also added
user-select: none to the time labels row.

Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>

* fix(sightline): align visibility ticks with red slider position

- Cap numTicks to frame count (no more ticks than frames)
- Position ticks using frameIdx/(frameCount-1) — same formula as
  the red time slider — so they always line up exactly

Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>

* fix(sightline): set vstTimeStep width to 76px

Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>

* refactor(sightline): extract Sightline into dedicated backend module

Move sightmap and horizon profile endpoints from API/Backend/Utils/ into
a new API/Backend/Sightline/ module with its own setup.js, routes, and
scripts directory.

- POST /api/utils/sightmap → POST /api/sightline/sightmap
- POST /api/utils/gethorizonprofile → POST /api/sightline/horizonprofile
- private/api/sightmap.py → API/Backend/Sightline/scripts/sightmap.py
- private/api/HorizonProfile.py → API/Backend/Sightline/scripts/HorizonProfile.py
- Extract validateMissionsPath into shared API/validateMissionsPath.js
- Update frontend calls.js and E2E tests to use new paths

Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>

* fix(sightline): update SPICE kernel relative path after script relocation

The script moved from private/api/ to API/Backend/Sightline/scripts/,
so the relative path to spice/kernels/ needs 4 parent traversals
instead of 2.

Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>

* docs(sightline): update help with detailed algorithm descriptions

- Document sightmap viewshed algorithm: source position via SPICE,
  DEM composite, tangent-plane projection, ray-march viewshed, output grid
- Document horizon profile algorithm: per-azimuth ray march, elevation
  angle tracking, curvature correction, distance recording
- Add parameter tables for both endpoints
- Document performance methods: log stepping, early termination,
  managed resolution, batch streaming, frame limits
- Update SPICE paths to reflect new spice/ directory
- Update visibility timeline description (now pixel-based)
- Add fog shading, hover tooltip, and range slider to horizon profile docs

Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>

* refactor(sightline): extract modules for Indicators, Export, Horizon, Visibility

- Extract SightlineTool_Indicators.js (~689 lines): Az/El canvas gauges, sky dome, mini RAE
- Extract SightlineTool_Export.js (~488 lines): PNG, GIF, CSV, Grid export functions
- Extract SightlineTool_Horizon.js (~574 lines): horizon profile canvas, fog shading, tooltip
- Extract SightlineTool_Visibility.js (~278 lines): visibility timeline bar chart
- Extract RangeSlider reusable component to src/design-system/components/RangeSlider/
- SightlineTool.js reduced from 3082 to 1919 lines (thin delegates to new modules)
- SightlineTool_Graphs.js reduced from 1532 to 981 lines (delegates to Horizon/Visibility)
- Fix shadowReach isFinite validation bug (Devin Review feedback)

Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>

* feat(sightline): invert color ramp to black→white, add horizon polygon overlay

- Invert WhiteBlack color ramp to BlackWhite (black→white gradient)
- Add faint horizon polygon on 2D map showing the horizon profile outline
  - Updates whenever horizon profile is fetched/redrawn
  - Removed when sightline graphs panel is closed
  - Very faint styling: white outline 0.35 opacity, fill 0.06 opacity

Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>

* feat(sightline): add horizon polygon toggle checkbox, increase polygon opacity

- Add 'Polygon:' checkbox in graphs header bar (right of horizon range slider)
- Default off; toggling on shows the horizon polygon overlay on the 2D map
- Polygon is more visible: outline 0.6 opacity, fill 0.12 opacity, weight 1.5
- Checkbox state resets to off when graph panel closes
- Uses existing mmgis-checkbox component pattern

Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>

* feat(sightline): thicker polygon border (weight 3), add tippy on Polygon label

Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>

* fix(sightline): composite hover uses bounds-based lookup instead of tile projection

The _onCompositeHover function was using Globe_.litho.projection tile
coordinates (topLeftTile, tileResolution) which don't exist in the
sightmap data model. Replaced with bounds-based row/col computation
using data._bounds [west, south, east, north] which is what the
sightmap API actually returns.

Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>

* fix(sightline): update azimuth lines on map pan/zoom

Azimuth SVG overlay uses pixel coordinates from latLngToContainerPoint.
When the map pans, these coordinates become stale but the overlay wasn't
being redrawn. Now listens for 'move' events on the map while the graph
panel is open and redraws the source azimuth lines on every move.
Listener is removed on panel close.

Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>

* fix(sightline): composite hover uses projBounds for projected CRS maps

When the map uses a custom projected CRS, Leaflet e.latlng coordinates
are in projected meters, not geographic degrees. The hover function now
uses data._projBounds (projected) when in a projected CRS and
data._bounds (geographic) when in standard geographic CRS.

Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>

* fix(sightline): update polygon tippy text, add user-select:none to labels

Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>

* fix(sightline): project mouse coords before comparing to projBounds

Leaflet's e.latlng gives geographic lat/lng even in projected CRS maps.
For projected CRS (lunar south pole stereographic), we need to convert
these to projected coordinates via crs.project() before comparing
against data._projBounds. This was causing the hover to always produce
out-of-range row/col on projected CRS maps.

Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>

* perf(sightline): progressive log2 stepping in sightmap ray march

Replace fixed-step march with distance-based progressive stepping:
step = base * max(1, log2(r+1)), combined with margin-based acceleration.

Near the observer every pixel is sampled. At r=1000px the step grows
to ~10x base; at r=25000px to ~15x base. This dramatically reduces
iterations for high-resolution DEMs (e.g. 10m USGS) where rays can
span 25,000+ pixels. Expected ~4-8x fewer samples per ray at distance
with negligible accuracy loss (distant terrain must be very tall to
affect the elevation angle).

Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>

* Revert "perf(sightline): progressive log2 stepping in sightmap ray march"

This reverts commit 97d29eecbbf9c8e7ede869fa5e8bfa4745871ed0.

* Revert "Revert "perf(sightline): progressive log2 stepping in sightmap ray march""

This reverts commit 975c772101aa0ebb4d161b82156100d77596e348.

* perf(sightline): add in-march early termination + reduce working DEM size

Two optimizations on top of the progressive log2 stepping:

1. In-march early termination (#4): at each sample, checks if the
   best-case terrain angle (MAX_TERRAIN_H minus curvature drop) at the
   current distance is below the source elevation. If so, no further
   terrain can block the source and the ray stops immediately. Most
   effective for high-elevation sources.

2. Match working DEM to output resolution (#5): reduced working_dim
   from 2x output to 1x output. Rays march through a ~400px array
   instead of ~800px, halving samples per ray and DEM I/O. Shadow
   accuracy is preserved since the output grid resolution is unchanged.

Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>

* feat(sightline): add 2-minute timeout to sightmap.py

Uses signal.SIGALRM on Unix and a threading.Timer fallback on Windows.
On timeout, raises TimeoutError which is caught by the existing error
handler and returns a JSON error response. Timeout is cancelled on
successful completion.

Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>

* fix(sightline): prevent server crash on oversized sightmap output

Add a 256 MB stdout buffer cap with try-catch around string
concatenation. If the Python process output exceeds the limit,
the child process is killed and a 413 error is returned instead
of crashing the Node.js server with RangeError: Invalid string length.

This can happen at native resolution on large DEMs (e.g. 30993x30993)
where the output grid JSON exceeds Node's string size limit.

Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>

* feat(sightline): binary grid encoding, NDJSON streaming, delta compression, gzip

Implement 7 output size reduction optimizations:
1. Binary encoding: uint8 grid → zlib compress → base64 (gridB64z field)
2. RLE via delta: batch frames encoded as XOR diffs (deltaB64z field)
3. Frontend decode: DecompressionStream → Uint8Array → 2D grid
4. Delta reconstruction: XOR previous flat with decoded delta
5. Precision: uint8 (0/1/9) instead of JSON number arrays
6. gzip: Node route compresses single-frame JSON; batch streams through zlib.createGzip()
7. NDJSON streaming: batch mode streams one JSON line per frame via chunked transfer

Backend (sightmap.py):
- compute_sightmap() returns gridB64z instead of grid array
- compute_sightmap_batch() streams NDJSON lines to stdout
  First frame: full gridB64z, subsequent: deltaB64z (XOR vs prev)
- Helper functions _encode_grid() and _encode_grid_delta()

Node route (sightmap.js):
- Batch: pipes child stdout through optional gzip to response stream
- Single: buffers stdout, optionally gzip-compresses before sending
- Content-Type: application/x-ndjson for batch, application/json for single

Frontend (SightlineTool.js):
- _decodeGridB64z(): atob → DecompressionStream('deflate') → Uint8Array → 2D grid
- _applyDelta(): XOR reconstruct from previous frame flat array
- Batch: uses fetch() with ReadableStream to parse NDJSON line-by-line
- Progressive progress updates as NDJSON frames arrive during streaming

Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>

* fix(sightline): remove Python-side timeout, Node manages 3min single / scaled batch

Python no longer has an internal 120s timeout that would kill long batch jobs.
Node.js is the sole timeout authority:
- Single-frame: 3 minutes (180s)
- Batch: max(3min, frameCount × 30s), capped at 30 minutes

Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>

* fix(sightline): cap batch timeout at 5 minutes

Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>

* docs: fix stale comment — batch timeout cap is 5 min, not 30

Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>

* feat(sightline): replace CSV export with GeoTIFF for all modes

- Static: single-band uint8 GeoTIFF (0=shadow, 1=lit, 9=nodata)
- Playback: multi-band uint8 GeoTIFF (one band per frame)
- Composite: single-band float32 GeoTIFF (0.0-1.0 visibility fraction)

Uses the existing geotiff.js writeArrayBuffer with WGS 84 geographic CRS,
ModelTiepoint and ModelPixelScale from the sightmap bounds. Output is a
proper georeferenced TIFF openable in QGIS, ArcGIS, etc.

Removes the old CSV export which caused RangeError on large grids
(400x400 x 24 frames = 3.84M rows). A 24-frame multi-band GeoTIFF is
~4MB vs ~190MB CSV.

Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>

* fix(sightline): fix GeoTIFF export — use correct writeArrayBuffer input format

Single-band (static/composite): pass flat TypedArray directly (not wrapped
in array) so geotiff.js uses the flat code path with height/width metadata.

Multi-band (playback): pass native 2D arrays [band][row][col] so geotiff.js
can read dimensions from array structure.

Verified all three modes produce valid GeoTIFFs via gdalinfo.

Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>

* revert(sightline): remove GeoTIFF exports, drop playback CSV export

GeoTIFF exports had issues (geotiff.js bugs with float32/custom CRS).
Reverts to original CSV exports for static and composite modes.
Removes the playback CSV export entirely (caused RangeError on large grids).

Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>

* chore: bump version to 5.1.2-20260615 [version bump]

* feat: Phase 1 plugin restructure — unified /plugins/ directory

- Move 16 tools from src/essence/Tools/ → plugins/core/tools/
- Move 13 backends from API/Backend/ → plugins/core/backend/
- Move 1 component from src/essence/Components/ → plugins/core/components/
- Add plugins/core/plugin.json manifest
- Implement discoverPluginsUnified() three-level hierarchy scanner
- Update updateTools.js, setups.js, resolve-plugin-deps.js to use unified scan
- Update tool/component config.json paths for new locations
- Add plugins/ to webpack babel-loader include
- Replace 6 gitignore patterns with /plugins/* and !/plugins/core/
- Update AGENTS.md, CONTRIBUTING.md for new plugin structure
- Update test helpers and specs for new layout

Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>

* fix: correct relative require paths in moved backends

Backend files moved from API/Backend/ to plugins/core/backend/ need
updated relative paths to reach API/ modules (logger, connection, etc).
Also fix JSDoc comment containing '*/' in glob pattern that broke parser.

Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>

* chore: bump version to 5.1.3-20260615 [version bump]

* fix: escape glob pattern in JSDoc comment that broke parser

The pattern 'plugins/*/tools/' in a block comment contains '*/' which
prematurely terminates the /* */ comment block.

Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>

* fix: correct rootDir path depth and plugin.json version

- utils.js and sightmap.js rootDir needs 5 levels of ../ (not 4) to
  reach repo root from plugins/core/backend/X/routes/
- Sync plugin.json version with package.json (5.1.3-20260615)

Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>

* chore: bump version to 5.1.4-20260616 [version bump]

* fix: update relative import paths for plugin directory restructure

All tools and components moved from src/essence/Tools/ and
src/essence/Components/ to plugins/core/tools/ and
plugins/core/components/ in the Phase 1 restructure. This commit
updates ~250 relative import paths in those files so they resolve
correctly from the new directory depth.

Also fixes:
- mmgisAPI LegendTool import (now points to plugins/core/tools/Legend/)
- missionTemplates test require path (now plugins/core/backend/Utils/)
- setups.js and resolve-plugin-deps.js reverted to unified scan only

Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>

* fix: correct SPICE kernel path depth in sightmap.py and address review findings

- sightmap.py PATH_TO_KERNELS: needs 5 '../' levels from new location
  (plugins/core/backend/Sightline/scripts/ is 5 deep, was 4)
- plugin.json version: sync to 5.1.4-20260616 matching package.json
- New Tool Template: move to plugins/core/tools/ with updated imports

Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>

* feat(Phase 2): Standardize plugin manifests, split backend lifecycle, co-locate tests

- Rename config.json → plugin.json for all tool/component plugins
- Split backend setup.js into plugin.json (metadata) + plugin.js (lifecycle)
- Add Phase 2 manifest fields: uuid, id, version, type, tier, overridable, aliases, engines, peerDependencies
- Update pluginValidation.js with new field schema and type checks
- Enforce overridable:false in updateTools.js and setups.js
- Add engines.mmgis compatibility check using semver
- Implement semver-aware dependency conflict resolution (semver.intersects)
- Add peerDependencies validation via checkPeerDependencies()
- Simplify resolve-plugin-deps.js backend dep reading
- Move tool E2E tests into plugins/core/tools/X/tests/
- Move backend API tests into plugins/core/backend/X/tests/
- Update Playwright config to scan both tests/ and plugins/**/tests/
- Update CONTRIBUTING.md with Phase 2 manifest documentation
- Update test fixtures from config.json to plugin.json
- Add unit tests for Phase 2 validation, semver resolution, and peerDeps

Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>

* chore: remove /notes directory

Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>

* fix: overridable check inspects registered plugin, not incoming; fix config.json doc reference

- setups.js: Store setupManifests map to track already-registered plugin's
  manifest; check that when an override is attempted (matches updateTools.js
  pattern where registry[name].overridable is checked)
- CONTRIBUTING.md: Fix remaining config.json reference to plugin.json

Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>

* feat: Phase 3 — Plugin CLI and git-based registry system

- plugins/plugin-cli.js: CLI tool with list, install, remove, enable,
  disable, update, validate, deps, info, registry commands
- plugins/plugin-registries.json: git-based registry configuration
- plugins/plugin-state.json: enable/disable state (gitignored)
- Integrate plugin-state.json into discoverPluginsUnified() to skip
  disabled non-core plugins during discovery
- Core plugins are protected from removal and disabling
- npm run plugins maps to the CLI
- plugins/README.md: full user-facing documentation
- plugins/AGENTS.md: AI agent context for the plugin system
- CONTRIBUTING.md: added Plugin CLI section
- 17 unit tests for CLI commands and state integration

Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>

* fix: address Devin Review findings for Phase 3

- discoverPluginsUnified: validate plugin-state.json has 'plugins' key
  to prevent TypeError when state file is valid JSON but lacks the key
- plugins/README.md: fix tier values (core/community/private, not extended)
- plugins/README.md: display_name is not required for tools
- plugins/AGENTS.md: fix tools require name+paths, not display_name
- Add test for state file without plugins key

Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>

* fix: CONTRIBUTING.md template walkthrough references setup.js instead of plugin.js

Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>

* fix: CLI deps command uses claim.entry for pip/conda conflicts, not claim.version

Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>

* fix: add description to backend KNOWN_FIELDS for consistency

Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>

* fix: mergeNpm picks highest-lower-bound range; docs mark type/version as Recommended

- mergeNpm now sorts compatible ranges by semver.minVersion() descending
  to pick the most restrictive range, not insertion-order last-seen
- AGENTS.md and README.md updated to mark type and version as Recommended
  to match actual validation behavior (optional-but-typed)

Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>

* feat: support "version": "core" — auto-resolves to MMGIS version

Core plugins now use "version": "core" in their plugin.json manifests.
The CLI resolves this to the current MMGIS version from package.json
when displaying plugin info (e.g. "5.1.4-20260616 (core)").

This eliminates the need to manually sync plugin versions with MMGIS
releases — core plugin versions are always the MMGIS version by
definition.

Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>

* feat: add author, license, repository, keywords fields to plugin manifest

- Added to COMMON_FIELDS in pluginValidation.js (no warning for these)
- All 31 core plugin.json files: author=NASA-AMMOS/MMGIS,
  license=Apache-2.0, repository=https://github.com/NASA-AMMOS/MMGIS
- CLI info command displays author, license, repository, keywords
- README.md manifest table updated with new fields

Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>

* feat: CLI visual overhaul + fix Devin Review bugs

CLI visual improvements (zero dependencies):
- ANSI colors throughout (cyan plugin names, yellow versions,
  green/red status, dim metadata, bold headers)
- Box-drawing layout for 'info' command
- Column-aligned output for 'list' command with summary bar
- Step indicators [1/3] for install/remove/update
- Colored help with version display
- --no-color flag (also respects NO_COLOR env per no-color.org)
- --json flag for machine-readable output (list, info)

Bug fixes (from Devin Review):
- checkPeerDependencies now resolves "version": "core" to the
  actual MMGIS version before semver checks (was silently skipped)
- cmdDeps now includes conda dependencies alongside pip

Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>

* fix: revert info to flat layout, add type headers to list

- info command: back to simple labeled rows with colors (no box-drawing)
- list command: group plugins under type sub-headers (Tools, Backend,
  Components) with per-type colors (cyan, magenta, blue)
- Update test assertions for new output format

Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>

* fix: use high-contrast colors for dark/light terminal support

Replace blue (34) and magenta (35) which are hard to read on dark
backgrounds. New palette uses only high-contrast ANSI colors:
- Tools: cyan, Backend: yellow, Components: green
- Repository URLs: cyan instead of blue
- Registry URLs: white instead of blue

Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>

* feat: add webpack aliases for plugin-friendly imports

Add 5 webpack resolve aliases so tools/components can import shared
modules without fragile relative paths:
  @basics  → src/essence/Basics/
  @essence → src/essence/
  @design  → src/design-system/
  @pre     → src/pre/
  @external → src/external/

Before: import L_ from '../../../../src/essence/Basics/Layers_/Layers_'
After:  import L_ from '@basics/Layers_/Layers_'

Also adds jsconfig.json for IDE go-to-definition support.
Refactors all 233 deep relative imports across 49 tool/component files.

Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>

* feat: local install copies by default, --link for symlink

Local path installs now copy the directory instead of symlinking.
This avoids Windows EPERM errors with symlinks.

  npm run plugins -- install /path/to/plugins      # copies
  npm run plugins -- install --link /path/to/plugins  # symlink

With --link, if symlink fails (EPERM on Windows), falls back to
junction automatically.

Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>

* refactor: consolidate plugin docs, remove legacy templates

Merge plugins/AGENTS.md into plugins/README.md with full plugin
templates (tool, backend, component). Remove:
- API/Backend/setupTemplate.js (template now in README)
- plugins/AGENTS.md (merged into README)
- plugins/core/tools/New Tool Template.js (template now in README)

Add deprecation warnings in discoverPluginsUnified() when plugins
have legacy config.json or setup.js instead of plugin.json/plugin.js.

Update CONTRIBUTING.md, docs, and .knowledge to reference new locations.

Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>

* refactor: rename discoverPluginsUnified to discoverPlugins, improve warnings

Remove legacy two-level discoverPlugins() (no callers remain). Rename
discoverPluginsUnified() to discoverPlugins() across all callers, tests,
and docs.

Add deprecation warnings to CLI discoverAll() for config.json/setup.js.
Add flat-repo structure warning during install when no tools/backend/
components subdirectory is found.

Fix stale references: updateTools.js Kinds error message (config.js ->
plugin.json), Development.md paths (API/Backend -> plugins/core/backend,
setup.js -> plugin.js).

Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>

* feat: add activate command, auto-activate on plugin changes, fix backend engines check

Add 'activate' CLI command that regenerates src/pre/tools.js and
src/pre/components.js without a full webpack build. Auto-called after
install, remove, enable, disable, and update commands.

Add engines.mmgis compatibility check to API/setups.js for backend
plugins, matching the existing check in updateTools.js for tools
and components.

Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>

* refactor: activate prints diff only, suppresses verbose logger output

Instead of dumping every loaded tool/component, activate() now
compares src/pre/tools.js and src/pre/components.js before and
after regeneration, printing only added/removed entries. Logger
console output is suppressed during the regeneration step.

Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>

* fix: color-code 'no changes' in activate based on context

Yellow when changes were expected (install/remove/enable/disable/update),
dim gray for standalone activate command.

Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>

* fix: red 'Discovered 0 plugin(s)' on install, green otherwise

Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>

* fix: restore console output if activate throws (try/finally)

Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>

* feat: add 'create' command to scaffold new plugins

Usage: npm run plugins -- create <tool|backend|component> <Name> --container <name>

Scaffolds plugin.json (minimal required fields), entry point, CSS, and test
spec. Auto-activates frontend plugins after creation. Supports core and
external containers.

Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>

* feat: --json flag for all CLI commands + create scaffold command

- --json now works on all commands (install, remove, enable, disable, update,
  activate, validate, deps, create) — not just list/info
- create command scaffolds tool/backend/component with plugin.json, entry point,
  CSS, and test spec; requires --container flag
- activate() returns diff data and accepts silent mode for JSON consumers

Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>

* docs: update README with create command, --json for all, --container flag

Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>

* fix: puzzle-outline fallback icon, tool header scaffold, Configure page CLI pointers

- Default icon: puzzle-outline fallback in Tools.js, Components.js, ToolModal.js
- Scaffold: tool template includes mmgisToolHeader with close button
- Scaffold: plugin.json includes defaultIcon: puzzle-outline
- Configure page: replaced outdated 'Custom Tools/Components' cards with Plugin
  CLI usage pointers
- Fix: typeof null guard in pluginDiscovery.js (Devin Review)
- Docs: path convention comment in updateTools.js (Devin Review)
- Recreated plugins/AGENTS.md with current state

Co-Authored-By: tariq.k.soliman <tariqksolim…

5.0.15

Toggle 5.0.15's commit message

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
Release version 5.0.15 (#994)

4.2.34

Toggle 4.2.34's commit message

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
Update github workflow: docker-build.yml (#917)

* Update github workflow: docker-build.yml

* chore: bump version to 4.2.34-20260328 [version bump]

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>

4.1.0

Toggle 4.1.0's commit message
Update CHANGELOG.md for 4.1.0

4.0.0

Toggle 4.0.0's commit message
Update CHANGELOG.md

3.0.0

Toggle 3.0.0's commit message
3.0.0 Changelog

2.12.0

Toggle 2.12.0's commit message

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
Update CHANGELOG.md

2.11.0

Toggle 2.11.0's commit message

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
Bump 2.11 (#515)

2.9.0

Toggle 2.9.0's commit message

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
v2.9.0 (#424)

* Make sure to remove time tab in time disabled

* IFrame examples

* #279 Time Improvements (#280)

* Configure global start end time, Partial local vector time filter

* #279 Time Improvements

* InfoTool checks for links in metadata and renders anchor tags

* Fix environment variable in sample.env for enabling websockets (#281)

* Fix environment variable in sample.env for enabling websockets

* Update docs

* Bug Fix: Don't drawing uncertainty ellipse if either axis is 0

* ts-282 Composite Time Tiles, Time Tiles with range (#283)

* Update Time Tile path structure

* #285 init-db (#286)

* #290 Upgrade config page jquery, fix i closing tag (#291)

* #292 Controlled Time Layers only make initial query, refactored to remove a conflicting setTimeout (#293)

* Fix crash when Time is completely disabled

* #296 Fix WMS url query (#297)

* #298 TimeUI Pan and Zoom and Improvements (#299)

* #298 Early TimeUI Pan Zoom

* #298 Pan, zoom, steps, modes, mostly done

* TimeUI Improvements and verifications

* #298 Fix Point Play bug

* #298 Final Touchups

* Make marker bearings projection-aware

* Update Docs, Add redundant stopGuests check

* Fix Invalid Date when using deep linked timestamps, improve mmgisAPI docs

* Fix typo

* #294 SameSite None env and login improvements (#302)

* #294 SameSite First pass

* #294 Fix session and use postgres as session store

* Use a default session db port

* #294 Regenerate session on login failure too

* Add feature to reconnect WebSocket from client to server (#295)

* Add materialize-css package

* Update WebSocket on client side

* Double reconnect attempt interval every time websocket reconnection attempt fails

* Update package-lock.json

* Move toast to be ~5 pixels from the right side

* Only show 'Successfully Connected' toast if websocket connection has previously failed

* Fix toast display time

* #304 Fix bug in latest too

* Layer UUIDs (#308)

* #306 UUIDs part 1

* #306 Layer UUIDs UI touchups, still missing Config API

* #306 Layer UUIDs for Config API

* #309 Controlled Raster for initial query only (#311)

* Fix function call order bug in addNewLayer function (#310)

* Account for non-uuid deep link names

* Fix toggleLayer bug (#260)

* Fix Node 18+ build (#312)

* #317 Fix time vectors not applying correctly (#319)

* Fix bug where initially on annotations features have no click events

* Add function to overwrite elements displayed in LegendTool (#320)

* Add function to overwrite elements displayed in LegendTool

* Add more checks to overwriteLegends function

* Add overwriteLegends to mmgisAPI

* #322 Add ROOT_PATH, Deprecate PUBLIC_URL (#323)

* #322 ROOT_PATH, doesn't work fully for /configure yet

* #322 ROOT_PATH for runtime subpaths

* #322 Remove PUBLIC_URL and fix ROOT_PATH css

* Update README.md

* Update README.md

* #324 Configurable Context Menu Actions (#325)

* #324 Coordinates Config Tab Raw Variables

* #324 Configurable context menu actions

* Add docker build workflow

* Make sure user input UUIDs through REST API are unique (#326)

* Make sure user input UUIDs through REST API are unique

* Fix overwriting existing UUID issue

* #327 Assorted UI Improvements (#328)

* #313 Expose as much as possible to the mmgisAPI (#330)

* #313 Add Formulae_

* #313 More F_ docs

* #313 Finish up F_ docs

* #313 mmgisAPI addLayer and removeLayer

* #313 Add logged_in endpoint and early Backend API docs

* #331 Websocket aware configure page (#332)

* #331 Websocket aware configure

* Remove console.log

* Update addLayer and removeLayer endpoints in REST API (#337)

* Working on updating REST API

* Do not update config if there are duplicate or bad UUIDs

* Update docs

* #303 Photosphere Targets and Target Pairing (#340)

* #303 Pairings attachments, photosphere targets not working yet

* #303 AzEl computed markers in Photosphere, deleted old docs

* Add back in swagger docs

* #303 Use originOFfset

* Refactor with the uuid upgrade, Update docs

* #341 Export working config, disable override (#342)

* Add xml2js and proj4 packages. (#343)

* add xml2js

* Fix DrawTool Upload Loading style

* Draw Edit properties

* #335 Feature Request: GetMissions (#345)

* #335 /configure/mission full param

* #335 touchup docs

* Fix typo

* #346 Deep Link additional encodeURI

* Fix missing uuids from configure

* WMS TILESIZE param, features without uncertainty properties won't draw in Map, github link of docs page

* Fix clearOnMapClick function for layers with no fill (#348)

* Fix angleUnit on image attachment

* Optional Info button for projects that need a link to project info #350

* Support existing az el properties for pairing attachments

* Fix bug in Photosphere where points with matching names get stuck

* Add dontFloor to mod function

* Updates to info and help buttons

- Hide info and help icons if a url is not configured
- Default the info button to be off in the configuration
- Change to info icon with a circle

* #352 [New Feature]: DrawTool - Feature Property Templates (#353)

* #352 Templating 1

* #352 Templating 2

* #352 Templating 3

* #352 Update Modal for templating

* #352 Migrate DrawTool Templating

* #352 Templating touchups

* #354 Add MAIN_MISSION ENV (#355)

* Do not append time parameters to wmts tile layers

* Fix configure layer cloning

* Remove typo ; in MAIN_MISSION env

* Remove outdated firefox-specific css fix

* Add blob to default-src csp

* #356 DrawTool Templating - Incrementer field (#357)

* #358 - DrawTool - Grouping editing should only update changed template fields (#359)

* #360 Expose all endpoints to longtermtokens (#362)

* #363 Support file_owner in webhook body. Include body for onFileDelete (#364)

* Fix layer uuids showing up in bottom-right coords

* Expose ToolController_

* #366 Don't copy uuid on layer clone (#367)

* #369 Refresh vector layers, also some WEBSOCKET_ROOT_PATH (#370)

* #368 Fix Websocket usage with ROOT_PATH (#373)

* #369 Refresh vector layers, also some WEBSOCKET_ROOT_PATH (#370) (#371)

* #368 Websockets use ROOT_PATH

* #368 Fix configure websocket

* Fix bad Configure/websockets json writing

* Fix time memory leak from improper cleanup of tooltips and slider

* chore(config): Inline import from same namespace (#375)

* #378 LegendTool as Popup (#379)

* #378 Separate legend tool

* #378 Legend formatting, reacts to layer toggles

* #380 DrawTool - Fix error saving templateless features (#381)

* Reset stroke styles too

* fix(imports): destructure modules (#384)

* chore(naming): use standardized name (#385)

* Add event listener for toggleSeparatedTool in MMGIS API (#388)

* Prioritize Map splitscreen size when resizing

* #372 calls to updateVectorLayer and appendLineString cause selected feature to be deselected when using info tool (#389)

* #372 Reselect feature on updateVectorLayer and appendLineString

* #372 Non-async updateVectorLayer, fix Pairings update

* Remove description transition

* Persist active feature on appendLineString

* #390 InfoTool - Clicks Intersect Polygons (#391)

* #387 DrawTool - Group Editing (#392)

* #387 DrawTool - Group edit files Part 1

* #387 DrawTool - Group edit files Part 2

* DrawTool / Group Editing - Let other users save changes to features

* Added an option to the LegendTool to automatically display on start

* LegendTool needs to have hasVars set to true for the last commit

* Added max-width to legend to prevent long text from filling screen

* Added a .nvmrc file with node version

* Added ellipsis and title for overflow legend text

* #386 Local Login Issue (#396)

* #386 Add setLoginToken to mmgisAPI

* #386 Fix variable path

* #386 SKIP_CLIENT_INITIAL_LOGIN env + mmgisAPI.initialLogin

* #383 Support raster option on gdal2tiles_3.5.2/gdal2customtiles (#397)

* #383 gdal2tiles_3.5.2_v2 for raster support

* #383 Cleanup gdal2tiles scripts and improve documentation

* #383 gdal2customtiles extentworld working, overview tiles not working

* #383 Fix gdal2customtiles rasters with different pixel scales

* gdal2customtiles - fix width calc, still slight offset

* gdal2customtiles raster - fix rounding issues

* Fix mmgisAPI featuresContained when a failed to load layer = null

* #399 Crashes if Layers are valid JSON but not valid GeoJSON (#400)

* #399 Add geojson validater to map

* #399 allow overloaded coordinates in geojson for extended geojson

* #401 Extend CSSO timeout check to main client (#402)

* #401 Main site timeout notification part 1

* #401 Main site timeout notification part 2

* #401 Main site timeout notification - warn at 20 min

* #403 rightClickMenuActions - Polygons and WKT (#404)

* #403 - Bugfixes and rightclick finds features

* #403 ContextMenu, Actions on features, WKT link populate

* #405 DrawTool - Template - Incrementer - Move to backend (#406)

* Mini fix to allow [] as empty geojson layer data

* Fix DrawTool Incrementer self collision bug

* Convert layer data [<FeatureCollection>] to geojson

* #407 Click intersects on Query and Draw layers (#408)

* #407 Include Query layers in click intersections

* #407 Support intersects on DrawTool layers too

* Allow the legend tool to be right justified on the screen

* #409 Added example config in description for LegendTool

* #410 Draw Tool - Time Integration (#411)

* ts-410 DrawTool - Temporal part 1

* #410 DrawlTool - Temporal Integration

* #410 Clearer DrawTool Template Date Start/End Icons

* #412 Add Configuration Option to set TimeUI Extent (#413)

* #414 IdentifierTool works again with human layer names (#415)

* #422 Removed event blocking popup functions (#423)

* Bump to 2.9.0

---------

Co-authored-by: ac-61 <ac-61@users.noreply.github.com>
Co-authored-by: Joe Roberts <joe.t.roberts@jpl.nasa.gov>
Co-authored-by: Even Stensberg <evenstensberg@gmail.com>

2.8.0

Toggle 2.8.0's commit message
Bump to v2.8.0