Sync with upstream NASA-AMMOS/development#73
Draft
slesaad wants to merge 104 commits into
Draft
Conversation
* Fix bug in viewer_open kind * chore: bump version to 4.2.10-20260217 [version bump] --------- Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
* Add config option to set initial zoom in mobile mode * chore: bump version to 4.2.11-20260217 [version bump] --------- Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
* Add font types as asset to webpack * chore: bump version to 4.2.12-20260226 [version bump] --------- Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
* Make the toolbar icons larger in mobile mode * Make height of LayerTool, LegendTool, and InfoTool dynamic in mobile mode * Hide Locate button in LayersTool * Fix toolbar bug * chore: bump version to 4.2.13-20260226 [version bump] --------- Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
* Fix the height of the topbar in mobile mode * chore: bump version to 4.2.14-20260227 [version bump] * Fix size of TimeUI input boxes so it works with smaller screens in mobile mode * Update bg color and move css declarations --------- Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
* Security fixes * chore: bump version to 4.2.16-20260302 [version bump] --------- Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
* Fix security issues 2 * chore: bump version to 4.2.17-20260305 [version bump] --------- Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
* Improve login page for smaller screens * chore: bump version to 4.2.17-20260304 [version bump] * Bump package --------- Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
* Fix bug in viewer_open kind * chore: bump version to 4.2.17-20260304 [version bump] * Use correct variable * Bump version --------- Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
* Remove redundant urlencoded * chore: bump version to 4.2.21-20260310 [version bump] --------- Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
* Update time and timetype metaconfigs * chore: bump version to 4.2.22-20260310 [version bump] --------- Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
* Upgrade Adjacent Servers and sample ENVs * chore: bump version to 4.2.25-20260318 [version bump] --------- Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
* Add .gitattributes * chore: bump version to 4.2.27-20260319 [version bump] --------- Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Ensure _docker-entrypoint.sh uses LF line endings.
* Fix image loading in OpenSeadragon * chore: bump version to 4.2.19-20260318 [version bump] * Fix bug * Bump version * Bump version --------- Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
* feat: Add 3D extrusion for vectortile layers + 3D Tiles support
Adds two new 3D rendering capabilities to the Cesium globe renderer:
1. Vector tile extrusion: vectortile layers can now render extruded 3D
buildings on the globe via a new "3D Extrusion" tab in the layer
config. A CesiumMVTLayer class manages tile lifecycle (load, decode,
evict) with batched Cesium.Primitive rendering and per-feature color
support from the OpenMapTiles schema. No Cesium ion token required —
works with any MVT source (OpenFreeMap, Versatiles, self-hosted).
2. 3D Tiles layer type: new "3dtiles" layer type for Cesium3DTileset
URLs, with configurable LOD, memory limits, height offset, and
style expressions.
Also:
- New auxiliary/resolve-tile-url CLI utility to resolve TileJSON
endpoints to concrete tile URLs (keeps MMGIS provider-agnostic).
- Scene lighting enabled with a fixed sun angle (summer solstice, 10am
EDT) for consistent, readable building shading.
- L.vectorGrid sublayer filtering in Map_.js to hide MVT sublayers not
explicitly styled (prevents default blue rendering of roads, water,
etc.).
- Backend validation (API/Backend/Config/validate.js) accepts the new
3dtiles type.
* feat: terrain-aware building placement + MVT simplification
- CesiumMVTLayer now samples terrain height at each tile center so
extruded buildings sit on the ground surface instead of at elevation 0.
Tries globe.getHeight() first (fast, synchronous), falls back to
fetching the Mapzen Terrarium tile directly and decoding heights from
the PNG. Cached per terrain tile to avoid re-fetching.
- Added SimplifiedVectorGrid: subclass of L.VectorGrid.Protobuf that
applies Douglas-Peucker simplification to polygon rings after decode,
before SVG rendering. Reduces vertex counts ~50-80% on dense sources
like OSM buildings with no perceptible visual change. Opt-in via a
simplifyTolerance option; inline algorithm, no new dependencies.
- Map_.js wires SimplifiedVectorGrid into the vectortile flow for
layers with extrusion enabled (default tolerance: 4 MVT units).
* Fix clearGradientHoverPoint missing in mockLitho * chore: bump version to 4.3.29-20260505 [version bump] --------- Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Added minimum specifications for system requirements.
* Fix DEM tile corruption by using nearest-neighbor resampling for RGBA-encoded float tiles
When --dem flag is used, float32 elevation values are encoded into RGBA bytes
via IEEE 754 binary representation. The scale_query_to_tile function was using
gdal.RegenerateOverview with 'average' resampling, which averages the raw RGBA
byte values. Since these bytes represent IEEE 754 float encoding, averaging them
produces garbage float values (e.g. 6.54e+27 instead of ~150m elevation).
This is especially visible at tile edges where valid RGBA-encoded pixels neighbor
transparent (0,0,0,0) nodata pixels.
Fix: when options.isDEMtile is True, use 'near' (nearest-neighbor) resampling
instead of 'average' in scale_query_to_tile. This preserves the RGBA byte
encoding integrity during the querysize-to-tile_size downscale step.
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* chore: bump version to 4.3.19-20260422 [version bump]
* Regenerate Reference-Mission RGBA DEM tiles with fixed gdal2customtiles.py
Tiles regenerated at zoom levels 10-14 using the corrected script that uses
nearest-neighbor resampling for DEM tiles instead of average. All 23,660 data
pixels verified to decode to valid elevations within the source DEM range.
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* feat: add production fail-safe checks, test DB credential separation, and AI agent safety rules
- Add NODE_ENV=production and DATABASE_URL production-indicator checks
to tests/global-setup.js and tests/test-db-clean.js
- Support DB_USER_TEST / DB_PASS_TEST env vars for least-privilege
test database credential separation
- Add Database Safety Rules section to AGENTS.md and AI-GETTING-STARTED.md
- Create .cursorrules with database safety guidelines for AI agents
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* chore: bump version to 4.3.26-20260427 [version bump]
* remove .cursorrules — not used
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix: handle promise rejection from clean() safety checks
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix: remove DATABASE_URL check, update error wording, require explicit test creds in test-db-clean
- Remove DATABASE_URL production check (no such ENV exists)
- Change 'destructive test operations' to 'test operations' in error message
- test-db-clean.js now requires DB_USER_TEST/DB_PASS_TEST with no fallback
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* feat: add mmgis-stac-test DB isolation and STAC_DB_NAME env var
- Make STAC DB name configurable via STAC_DB_NAME in API/connection.js
and scripts/init-db.js (defaults to 'mmgis-stac')
- global-setup.js creates mmgis-stac-test when STAC services are enabled
and passes STAC_DB_NAME to the test server
- Adjacent server .env files rewritten to use mmgis-stac-test
- test-db-clean.js drops mmgis-stac-test alongside mmgis-test
- Add DB_USER_TEST, DB_PASS_TEST, STAC_DB_NAME to sample.env and ENVs.md
- Update safety rules in AGENTS.md and AI-GETTING-STARTED.md
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix: comment out empty env vars in sample.env, fix STAC cleanup independence
- Comment out DB_USER_TEST, DB_PASS_TEST, STAC_DB_NAME in sample.env to
prevent dotenv from setting them to empty/whitespace values
- Fix early return in test-db-clean.js so mmgis-stac-test cleanup runs
independently of whether mmgis-test exists
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix: move test env vars to Optional Variables section in ENVs.md
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix: require DB_USER_TEST/DB_PASS_TEST in global-setup.js (no fallback)
- Remove fallback to DB_USER/DB_PASS in global-setup.js credential resolution
- Add DB_USER_TEST/DB_PASS_TEST to CI workflow .env setup
- Update ENVs.md to reflect these are now required for tests
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix: remove STAC_DB_NAME env var, hardcode mmgis-stac and mmgis-stac-test
- Revert API/connection.js to hardcoded 'mmgis-stac'
- Revert scripts/init-db.js to hardcoded 'mmgis-stac'
- Test infrastructure uses hardcoded 'mmgis-stac-test' constant
- Remove STAC_DB_NAME from sample.env and ENVs.md
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix: update sample.env comment — test creds required in both files, no fallback
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix: add windowsHide to suppress console windows on Windows
Prevents execSync and spawn calls in global-setup.js from flashing
empty terminal windows on Windows machines.
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* Re-apply 36 UI improvements on development's React architecture
Adapted all 36 UI tasks from PR #47 to work with development's full
React component architecture (Toolbar, Splitter, BottomBarReact, etc.)
instead of PR #45's null-stub + jQuery layout.
Layout & Positioning (Tasks 1, 4, 5, 11, 16, 17, 20, 27):
- BottomElementPositioner: immediate positioning with toolPanelWidth offset
- Scalebar/compass: 12px permanent right push + tool panel width
- Toolbar: flex-direction column layout
Controls & Navigation (Tasks 2, 9, 10):
- Map zoom: Home button resets to configured initial view
- BottomBarReact: reduced to About + Copy Link only
- TopBar: kebab menu with Screenshot, Fullscreen, Hotkeys, Settings
Tool System (Tasks 3, 6, 7, 13):
- Tool buttons: CSS classes (.toolButtonActive) instead of inline styles
- Tool headers: standardized with mmgisToolHeader/mmgisToolTitle classes
- MeasureTool: close button at top, reset/download at bottom
Visual Polish (Tasks 8, 14, 18, 25, 26, 29, 30, 32, 34, 36):
- Splitter: invisible by default, accent color on hover
- Compass: smooth left transition
- Coordinates: background, separator between mouse/pick
- mmgislogo: border-right matching topbar border-bottom
- toolsWrapper: no border in bottomFloatingBar
TimeUI & Settings (Tasks 15, 23, 24, 28):
- toggleTimeUI button removed from coordinates
- TimeUI toggle added to Settings modal
- Popover positioning uses window.innerHeight - bcr.top
Modals & Config (Tasks 12, 19, 21):
- About modal: MMGIS logo, version, attributions, markdown content
- Modal close: proper click handler with stopPropagation
- Attributions: removed from DOM, collected for About modal
Panel Sync (Task 31):
- TopBar panel toggles sync with Zustand store via subscribe
Dependencies & Config:
- Added dompurify for markdown sanitization
- Added aboutModalContent to config template and Reference Mission
- Added 9 help markdown files for tools
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* Add theming system (6 presets), floating tool panel, CSS variable support
- Port design-system: themes.js (6 presets), applyTheme.js, themeApplier.js, useTheme.js
- Wire theme into Zustand store (themeName + setTheme action)
- Wire theme into UserInterfaceBridge init/fina and Stylize.js
- Add theme dropdown to configure page (look.theme field)
- Update mmgisUI.css to use CSS variables for theme support
- Make tool panel float over map instead of docking/pushing
- Hide splitter drag handles by default
- Fix Coordinates.css margin for removed TimeUI button
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* Fix useTheme.js for React 16 compatibility (useState/useEffect instead of useSyncExternalStore)
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* Fix layout to match PR #47: floating tool panel, bottom bar, toolbar, topbar
- ToolPanel: 12px inset margins from toolbar/topbar, border-radius 10px,
backdrop-filter blur(20px), opacity transition on open/close
- Toolbar: box-shadow, z-index 101, 34x34 tool buttons with border-radius 8px,
starts below topbar (top: topSize), Toolbar.css imported
- TopBar: solid background + border-bottom via themeApplier, box-shadow via CSS
- Bottom floating bar: new BottomFloatingBar wraps toolsWrapper + timeUIDock
inside #splitscreens with 12px margins, backdrop blur, border-radius
- TimeUI reparented into floating bar via MutationObserver
- BottomElementPositioner: recalculated offsets based on floating bar height
- Separated tools: offset by toolPanelWidth + 24px when tool panel is open
- Splitter arrow buttons hidden (panel toggles in TopBar replace them)
- FloatingElements.css: backdrop blur for TimeUI, coordinates, compass, zoom
- SplitScreens.css, BottomBarReact.css, ToolPanel.css, Toolbar.css added
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* Fix TopBar: stays full-width when tool panel opens (matches PR #47)
- Remove toolPanelWidth/toolsWrapperRawWidth reactive styles that shifted
TopBar right when tool panel opened
- TopBar now always uses left:0, width:100%, padding-left:40px (from CSS)
- Tool panel floats underneath TopBar (z-index 2005 > 1400)
- Add theme colors for toggle buttons, user avatar via CSS variables
- Remove redundant z-index override in TopBar.css
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* Fix ToolPanel drag handle offset: use panelLeftOffset instead of hardcoded 10
The drag handle's left position uses panelLeftOffset (52px on desktop)
but the drag calculation was using 10 as offset, causing width to
inflate by 42px after each drag-resize.
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* Fix TopBar solid background and splitscreens positioning below TopBar
- Remove minimalist(true) call from fina() — matches PR #47 which also
removed it. Splitscreens now correctly starts at top:40px (topSize)
instead of top:0px which caused map to render under the TopBar.
- Add background: var(--color-a) and border-bottom: 1px solid var(--color-a1)
to #topBar CSS so it's solid from initial render (themeApplier.js
overrides on theme change).
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* Port separated tools and Legend changes from PR #47
- SeparatedTools.jsx returns null — separated tool rendering moved to
ToolController_.js jQuery DOM construction (matches PR #47)
- ToolController_.init() now creates floating glassmorphism panels for
separated tools (Legend, Identifier) with header/close buttons
- Separated tool buttons appear in toolbar below a divider
- LegendTool.js header updated to use mmgisToolHeader/mmgisToolTitle
classes with Help integration
- tab-ui-config.json: removed Default Tool section, renamed Colors to
Advanced Color Overrides
- Maker.js: removed defaulttooldropdown component type
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* Show MMGIS logo by default (was hidden behind minimalist() call)
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* Add tool panel close button, separated tool improvements
- Add injectCloseButton() to ToolController_ with X button in upper right
of tool panel (both vertical and horizontal tools)
- Call injectCloseButton() after tool.make() in makeTool()
- Add tippy tooltips to separated tool buttons (Legend, Identifier)
- Add divider background color using CSS variable
- Add isMobile check to skip separated tools on mobile (matching PR #47)
- Order Legend button last in separated tools section (matching PR #47)
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* Fix bottom bar layout for all 4 tool/TimeUI combinations
- Remove direct CSS manipulation from TimeUI._updateBottomUIHeight(),
delegate to centralized BottomElementPositioner via Zustand store
- Add React close button to BottomFloatingBar for horizontal tools
(replaces jQuery injection that was wiped by React re-renders)
- Skip jQuery close button injection for horizontal tools since React
BottomFloatingBar now handles it
- Add leaflet-bottom-left positioning to BottomElementPositioner
- All 4 combos verified: no tool/collapsed, no tool/expanded,
measure/collapsed, measure/expanded
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* Fix 13 UI/layout issues in PR #48
1. Remove redundant loginDiv and loginoutButton (hidden via useEffect)
2. mmgislogo border-right now uses var(--color-a1) matching topbar border-bottom
3. About button moved below Copy Link in toolbar bottom
4. Separated tools (Legend, Identifier) now appear in toolbar via SepToolsContainer
5. Map zoom/home controls use solid background matching topbar/toolbar
6. bottomFloatingBar z-index raised to 1500 (above toolpanel 1400)
7. Compass offset reduced from +38 to +8 (closer to scalebar)
8. mmgisToolHeader now flex row so MeasureTool [Title ? ... undo reset] layout works
9. Toolbar z-index raised to 2006 (above topbar 2005, prevents box-shadow overlap)
10. Light theme text contrast: CoordinatesDiv, topBarMain, TimeUI buttons, DrawTool
11. TimeUI timeline colors now update with theme changes via themeApplier
12. DrawTool tabs visible: #drawToolNotLoggedIn now starts at top:81px below nav
13. Bottom floating bar left offset accounts for tool panel width
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* Fix 16 UI issues (#14-29)
14. Bottom bar no longer pushed by vertical tool panel
15. LayersTool header restored (reverted mmgisToolHeader flex, fix MeasureTool inline)
16. Separated Legend panel pushed right when vertical tool opens
17. Legend appears before Identifier in toolbar
18. loginDiv/loginoutButton hidden via CSS !important
19. Horizontal tool panel width fixed (always 100% of bottom bar)
20. Identifier tool button no longer auto-activates on load
21. Splitters use theme accent color (--color-mmgis)
22. Vertical toolpanel drag edge styled like splitters
23. Horizontal toolpanel drag handle (6px accent bar at top)
24. Compass positioning simplified (flows with leaflet container)
25. Light theme text: Coordinates.css uses var(--color-f), themeApplier strengthened
26. Globe toggle: Globe_.init() instead of non-existent lazyInit()
27. Separated Legend panel theme: headers and content text use theme colors
28. InfoTool null guard on destroy (MMGISInterface may be null)
29. Splitter drag no longer offset by tool panel width (panel floats now)
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* Fix computeToolsSplitMoveResult min clamp to preserve splitterSize/4 fallback
The test expects splitterSize/4 as minimum when toolNativeHeight is not set.
Use Math.max(toolNativeHeight, splitterSize/4) so both constraints apply.
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* Fix 8 UI issues (#30-37)
30. Remove duplicate close button (React BottomFloatingBar one removed; tool's own stays)
31. Splitters/drag handles transparent by default, accent color on hover/drag only
32. Horizontal tool splitter follows mouse (disable CSS transition during drag via isDraggingSplitter flag)
33. Legend pushed right by extra 12px gap when vertical tool panel open
34. Compass (leaflet-bottom-left) pushed right when vertical tool panel opens
35. TimeUI mode dropdown z-index raised above tool panel
36. Glassy backdrop-filter on floating elements (CoordinatesDiv container transparent)
37. CoordinatesDiv: height 30px, bottom/right 12px permanent offset
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* React 18 upgrade + Base UI adoption + CSS Modules migration
- Upgrade react and react-dom from ^16.13.1 to ^18.2.0
- Upgrade react-app-polyfill to ^3.0.0, react-resize-detector to ^9.1.0
- Migrate entry point (src/index.js) to createRoot API
- Fix ReactDOM.render in CurtainTool.js and PDFViewer.js
- Fix ReactDOM.createPortal import in UserInterfaceLayout.jsx
- Install @base-ui-components/react@1.0.0-rc.0
Design system wrapper components (src/design-system/components/):
- Button.jsx + Button.module.css (primary/secondary/ghost variants)
- IconButton.jsx + IconButton.module.css
- Dropdown.jsx + Dropdown.module.css (wraps Base UI Menu)
- Toggle.jsx + Toggle.module.css (panel toggle groups)
- Modal.jsx + Modal.module.css (wraps Base UI Dialog)
- Tooltip.jsx + Tooltip.module.css (wraps Base UI Tooltip)
CSS Modules migration for shell components:
- TopBar: Rewritten with Toggle and Dropdown components, CSS Modules
- ToolPanel: CSS Modules, replaced hardcoded rgba with color-mix()
- SplitScreens: CSS Modules, replaced hardcoded rgba with theme vars
- Splitter: Added Splitter.module.css
- Toolbar: Migrated to CSS Modules
- BottomBarReact: Migrated to CSS Modules
- UserInterfaceLayout: Migrated to CSS Modules
themeApplier.js cleanup:
- Removed imperative selectors for #topBar, #toolPanel, #bottomFloatingBar,
#topBarTitleName, #topBarRight icons, #topBarMain (now CSS Modules)
- Kept selectors for jQuery/Leaflet/TimeUI elements
webpack.config.js: Added namedExport: false for css-loader v7 compatibility
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* Upgrade react-chartjs-2 to ^4.3.1 for React 18 peer dep compatibility
react-chartjs-2@3.3.0 only supported react ^16.8.0 || ^17.0.0.
v4.3.1 adds react ^18.0.0 support while keeping chart.js ^3.5.0 compat.
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* Fix #topBarInfo ID conflict: rename About button to #bottomBarAbout
The About button was reusing the #topBarInfo ID, which caused:
1. UserInterfaceBridge.js visibility logic hiding it unless look.info/infourl set
2. Stylize.js jQuery click handler opening infourl simultaneously with About modal
Renamed to #bottomBarAbout so it doesn't conflict with the legacy #topBarInfo
jQuery handler. The original #topBarInfo info-URL behavior remains untouched.
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* Address Devin Review findings: infourl, theme override, dead code
- Add look.infourl as a row in the About modal so it remains accessible
- Remove dead jQuery click handler for #topBarInfo in Stylize.js
- Remove duplicate setTheme() in UserInterfaceBridge.fina() that was
clobbering individual color overrides already applied by Stylize.js
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* Fix config visibility toggles for kebab menu items + clean up dead handlers
- Add lookConfig to Zustand store, set from UserInterfaceBridge.fina()
- TopBar: conditionally render Screenshot, Fullscreen, Settings, Copy Link
dropdown items based on lookConfig (screenshot, fullscreen, settings, copylink)
- BottomBarReact: conditionally render Copy Link button based on lookConfig
- Remove dead jQuery click handlers for #topBarHelp and #topBarInfo in Stylize.js
- Remove dead DOM getElementById calls in UserInterfaceBridge for elements that
no longer exist (topBarScreenshot, topBarFullscreen, bottomBarSettings, etc.)
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* Fix toggleTimeUI() reading state from removed #toggleTimeUI element
The #toggleTimeUI button was removed from the Coordinates markup but
toggleTimeUI() still read active state from it. Since the element doesn't
exist, $('#toggleTimeUI').hasClass('active') always returns false, causing:
- Map_.map._fadeAnimated always set to false (fade animations never re-enabled)
- L_._onTimeUIToggleSubscriptions always told TimeUI is being turned ON
Fix: read state from #timeUI (which exists and tracks the active class)
instead of #toggleTimeUI.
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* Improve Base UI adoption and reduce :global() overuse
- TopBar: use IconButton for sign-in/kebab buttons, use Dropdown for user card
popup (replaces hand-rolled outside-click + manual popup with Base UI Menu
for keyboard nav, focus trap, accessibility)
- BottomBarReact: use IconButton + Tooltip instead of raw <i> + tippy.js
- Toolbar: use Tooltip instead of tippy.js for tool button tooltips
- Reduce :global() from 33 to 22 instances — all remaining are justified by
external jQuery/imperative code references:
- SplitScreens: convert #viewerScreen/#globeScreen to scoped classes
- Toolbar: convert #toolcontroller_incdiv to scoped class
- Splitter: convert .splitterV to scoped class
- UserInterfaceLayout: consolidate TopBar styles into TopBar.module.css
- BottomBarReact: fully scoped (IconButton handles all button styling)
Design system usage: 5 of 6 wrappers now imported across 3 files
(Toggle, Dropdown, IconButton, Tooltip — only Button/Modal unused,
which is appropriate since there are no standalone button/modal cases
in the main site shell).
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* Fix: remove stale setShowUserCard call in handleLogout
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* Migrate Ancillary UI components from jQuery to React/native DOM
- Modal.js: React-based with imperative API bridge (Modal.set/remove)
so existing jQuery callers (BottomBar, DrawTool, AnimationTool, etc.)
work without changes. Uses React state + createRoot for rendering.
- ConfirmationModal.js: Removed jQuery dependency, uses native DOM
event listeners with the React Modal backend.
- Help.js: Removed jQuery dependency, uses native fetch() + DOM
event listeners instead of $.get() and $().on().
- ContextMenu.js: Removed jQuery dependency entirely. Uses native
DOM APIs (createElement, addEventListener, querySelectorAll) for
building and managing the right-click context menu.
- Compass.js: Removed jQuery dependency. Uses native DOM APIs
(getElementById, querySelector) for compass element creation
and bearing updates.
- MapLogo.js: Removed jQuery dependency. Uses native DOM APIs
for logo element creation and Leaflet container insertion.
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* Migrate Ancillary components to true React + Base UI
- Modal.js: React component using Base UI Dialog with shared state store,
imperative Modal.set()/remove() API preserved for backwards compatibility.
Accepts both HTML strings (legacy callers) and React elements (new callers).
- ConfirmationModal.js: React JSX component using design-system Button for
Yes/No actions, rendered through Modal service. Same prompt() API.
- Help.js: React JSX component using native fetch + showdown for markdown
rendering, rendered through Modal service. Same getComponent/finalize API.
- ContextMenu.js: Full React component with createRoot, JSX menu items with
proper event handlers. Same init()/remove() API.
- Compass.js: React component rendered via createRoot into Leaflet
bottom-left container. SVG compass with bearing rotation on map events.
- MapLogo.js: React component rendered via createRoot into Leaflet
bottom-right container. Configurable size and link support.
All components use CSS Modules. Old plain CSS files removed.
Also fixes:
- About modal now respects look.help/look.info config flags
- Cleaned up dead #toggleTimeUI references in Coordinates.js
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* Fix modal, tooltip, measure, and context menu bugs
- Modal: Fix About modal not opening due to async createRoot in React 18.
ModalHost now initializes from shared state and syncs on mount.
Blur management moved to ModalHost (React state-driven), fixing
persistent blur after modal close.
- Tooltip/Dropdown: Use render prop on Trigger to avoid nesting
<button> inside <button> (IconButton is already a button element).
- MeasureTool: Migrate from deprecated ReactDOM.render to createRoot.
Register Chart.js scales via Chart.register(...registerables) to fix
'linear is not a registered scale' error with react-chartjs-2 v4.
- ContextMenu: Fix crash when right-clicking on LithoSphere scene
(native events lack originalEvent). Store element and handler refs
for proper cleanup, preventing listener leaks.
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* Fix Modal: remove close button, fix blur persistence, add fade animation
- Remove the Dialog.Close button (curved bottom-left border-radius)
- Drop Base UI Dialog wrapper entirely — it was fighting with the
imperative Modal.set/remove API. Now uses simple divs with CSS
transitions, matching the original jQuery modal behavior exactly.
- Blur management is now purely imperative via _applyBlur() called
synchronously in set() and remove(). Removed async useEffect approach.
- Add 500ms CSS opacity fade-in/fade-out transition matching original.
- Closing state: Modal.remove() marks modal as closing (triggers
opacity 0 transition), then removes from DOM after 500ms.
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* Rename Ancillary React components to .jsx, fix modal blur via Zustand store, fix IconButton forwardRef, add Help.finalize calls
- Rename Modal, ConfirmationModal, Help, ContextMenu, Compass, MapLogo from .js to .jsx
- Route modal blur through Zustand modalBlurCount instead of imperative DOM manipulation
- Remove conflicting jQuery blur animation in Layers_.js
- Wrap IconButton with React.forwardRef to fix Tooltip/Menu trigger warnings
- Add Help.finalize() calls to ChemistryTool, DrawTool, IsochroneTool
- Change aboutModalContent config type from 'markdown' to 'textarea'
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* Revert aboutModalContent type back to 'markdown' — Maker.js already handles it
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* Fix Help.jsx: check res.ok on fetch, sanitize HTML with DOMPurify
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* Fix ContextMenu WKT null guard, fix Modal blur timing during fade-out
- Add null check for feature in handleActionClick WKT placeholder handling
- Delay blur removal until after 500ms fade-out completes (blur stays in sync with backdrop opacity)
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* Reorganize folder structure: dissolve Ancillary, nest components
- Dissolve src/essence/Ancillary/ entirely
- UI components → UserInterface_/components/ (Modal, ConfirmationModal, Help,
ContextMenu, Compass, MapLogo, CursorInfo, Attributions, Login)
- Layout chrome → UserInterface_/components/ (Description, Coordinates, Search,
ScaleBar, ScaleBox)
- Pure services → essence/services/ (DataShaders, LocalFilterer, QueryURL, Sprites)
- Stylize.js → design-system/ (theme bridge alongside themeApplier)
- Delete unused Swap.js
- Nest all components into own folders (ComponentName/ComponentName.ext pattern):
- UserInterface_/components/: TopBar/, Toolbar/, ToolPanel/, SplitScreens/,
Splitter/, BottomBar/, BottomElementPositioner/, Layout/, Panels/
- design-system/components/: Button/, IconButton/, Dropdown/, Toggle/, Modal/,
Tooltip/
- Update ~70+ import paths across codebase
- Build verified locally
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* Fix Modal.set() race condition and themeApplier CSS variable override issue
- Modal.set() onAddCallback: Replace 50ms setTimeout with MutationObserver
that waits for the modal element to appear in DOM before firing callback.
Prevents silent jQuery binding failures on slower devices.
- themeApplier: Use Proxy to read computed CSS custom properties (set by
Stylize.js per-mission overrides) instead of hardcoded theme object values.
Stylize.js now calls refreshThemeDOM() after setting CSS variables so
inline styles reflect mission-specific color overrides.
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* Sync toggleTimeUI DOM state to Zustand store
toggleTimeUI() now calls setTimeUIActive() and setTimeUIExpanded()
so BottomFloatingBar visibility and BottomElementPositioner offsets
reflect actual TimeUI state.
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* Restore #toggleTimeUI element in Coordinates markup
The element was accidentally dropped during the folder restructure move.
TimeUI.js and DrawTool.js check $('#toggleTimeUI').hasClass('active')
to gate histogram rendering and time-filter toggle visibility.
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* Fix #toggleTimeUI: restore click handler, tippy, active class; remove redundant jQuery positioning
Restored pieces lost during folder restructure:
- Click handler in init() and off handler in remove()
- Tippy tooltip for the time toggle button
- display:none when time is not enabled
- $('#toggleTimeUI').toggleClass('active') so TimeUI.js can check it
- $('#CoordinatesDiv > #toggleTimeUI').remove() on mobile
Removed jQuery CSS positioning from toggleTimeUI() since
BottomElementPositioner now reactively handles all bottom-anchored
element offsets via the Zustand store.
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* Fix CurtainTool.destroy() using undefined ReactDOM.unmountComponentAtNode
Use the stored _reactRoot.unmount() instead, matching the React 18
createRoot pattern already used in make().
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* Fix tooltips, scale indicator position, modal blur, help close button
Tooltips:
- Reduce Base UI Tooltip delay from 600ms (default) to 200ms
- Restyle tooltip popup to match tippy blue theme (var(--color-c2))
- Add Tooltip wrappers to TopBar panel toggles (Viewer/Map/Globe)
- Wrap Toggle with forwardRef so Tooltip render prop can attach ref
- Remove title attrs that conflicted with custom tooltips
Scale indicator:
- Remove scalefactor-specific positioning from BottomElementPositioner
(it moves naturally with .leaflet-bottom.leaflet-left container)
- Position scalefactor to the left of compass at same bottom level
Modal blur:
- Call _applyBlur() immediately when marking modal as closing
so blur clears during fade-out instead of persisting 500ms
Help modal:
- Add close (X) button in title bar matching other modal patterns
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* Remove dead CSS: delete tools.css, clean ~600 lines from mmgisUI.css and mmgis.css
- Delete tools.css entirely (both selectors #CurtainToolList and
.searchToolSelect are unreferenced anywhere in the codebase)
- Remove from mmgisUI.css: .mmgisRadioBar3/4/Vertical (140 lines),
.mmgispureselect (104 lines), blink/condemned_blink_effect (38 lines),
.slidecontainer/.slider (41 lines), .ar_slider (91 lines),
.verticalSlider (91 lines), .mmgisMultirange_elev (19 lines),
.ui-corner-all/bottom/right/br (9 lines)
- Remove from mmgis.css: #nodeenv, empty #topBar{}, #topBarInfo,
#topBarHelp, #topBarFullscreen, #toggleUI, #logoGoBack
- Keep #topBarLink (used in BottomBarReact.jsx), #webgl-error-message
(used by vendored THREE.js)
- All selectors verified with repo-wide grep before removal
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* UI fixes: tooltips, splitter hover, mobile toolbar, color schemes
- Replace Base UI Tooltip with simple React portal tooltip (200ms delay,
tippy-matching style) — fixes missing tooltips for toolbar/topbar/bottom buttons
- Add cursor + hover highlight to vertical splitters (was missing because
module CSS didn't inherit global .splitterV styles)
- Add hover highlight to tool panel drag handle
- Remove mdi-drag-vertical icon from tool panel drag
- Add mobile toolbar horizontal layout via @media query overrides
- Add 4 new color schemes: High Contrast (a11y), Dark Mars, Dark Midnight,
Light Warm (total: 10 themes)
- Previous fixes also included in working tree:
- timeUI border moved to toolsWrapper border-bottom (conditional)
- #toggleTimeUI button removed entirely
- CoordinatesDiv: vertical centering, unified background, 12px right offset
- barBottom padding-bottom: 8px
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* Fix assignment operator used instead of comparison in TimeControl.fina()
Pre-existing bug: `TimeControl.enabled = true` was assigning instead of
comparing. Changed to `TimeControl.enabled === true`.
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* Restructure Configure page UI tab: add all themes, Custom mode, enableWhenField
- Added all 10 theme presets to dropdown (was missing Dark Mars, Dark Midnight,
Light Warm, High Contrast)
- Added 'Custom' option: skips preset theme, uses only color picker values
- Moved Theming section directly under Rebranding
- Nested 'Custom Color Options' under Theming with subdescription
- Added enableWhenField support to Maker.js: disables color pickers unless
theme is set to Custom
- Renamed color options with clearer names and improved descriptions:
Primary → Surface Color, Secondary → Deep Background Color,
Tertiary → Text Color, Body → Page Body Color, Highlight → Feature Highlight
- Stylize.js: skip setTheme() when theme is 'Custom'
- Rebuilt configure page
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* Revert tooltips to tippy.js, fix dropdown menus, redesign About modal
1. Tooltip: Replaced custom React portal tooltip with tippy.js wrapper.
Uses the existing tippy.js dependency and 'blue' theme for consistency.
2. Dropdown: Replaced Base UI Menu with native portal dropdown.
Base UI's nested Menu.Trigger + BaseButton composition was swallowing
click events, breaking userAvatar and menuBtn menus. New implementation
uses simple state + createPortal with proper outside-click dismissal.
3. About modal: Professional redesign with centered MMGIS ASCII art header,
proper GitHub SVG logo link, clean metadata section, centered link
buttons, attributions section, and NASA-AMMOS footer.
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* Restore .mmgisHelpButton base styles lost during Help.css module migration
The global .mmgisHelpButton styles (yellow color, compact 18x18px sizing,
0.7 opacity) were removed when Help.css was converted to Help.module.css.
Since Help.getComponent() emits raw HTML strings for jQuery-rendered tool
headers, it cannot use CSS Module scoped classes. Restored the base styles
in mmgis.css alongside the related .mmgisToolHelpBtn definition.
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* Fix session logout regression, About modal refinements, High Contrast theme, Stylize.js, Default Tool config
- Login: skip session.regenerate() for token-based re-auth (useToken:true)
so reloading the main page no longer invalidates the configure page session
- About modal: replace ASCII art with mmgis.png logo, rename Attributions to
Map Layer Attributions, remove footer logo, link NASA-AMMOS to ammos.nasa.gov
- High Contrast theme: change accent from #ffff00 to #ffd700 (gold) for better
contrast ratios against dark backgrounds
- Stylize.js: color overrides only apply when theme is Custom or unset,
preventing preset themes from being clobbered by stale config values
- Restore Default Tool config section in tab-ui-config.json (accidentally
removed during Theming section reorganization)
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* Restore defaulttooldropdown case handler in Maker.js
The case was accidentally removed during the Configure page UI tab
restructure (d7f96c50). Without it, the Default Tool dropdown in the
Configure page rendered as nothing despite the config referencing it.
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* High Contrast tooltip text, panel toggle styling, scale position swap
- High Contrast theme: tooltips now use black text on yellow background
via --color-c2-text variable (white for all other themes)
- About modal links use var(--color-f) for consistent theme text color
- Panel toggle buttons: 11px uppercase with 600 weight for better
visibility
- Mapping scale button moved to bottom-right of compass (was top-left)
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* Reposition Viewer and Globe panel buttons to top-right
- Viewer: dropdown selector at top-right edge, OSD buttons stacked
vertically below it; settings panel opens to the left
- Globe: home, exaggerate, observe, walk, link controls moved from
TopLeft to TopRight corner via addControl 4th arg
- Style consistency: OSD buttons and LithoSphere controls now match
Leaflet zoom controls (var(--color-a) bg, var(--color-f) text,
var(--color-mmgis) hover, 30px size, 3px border-radius)
- Viewer settings sliders use var(--color-a3) instead of hardcoded
#444444
- Az/el indicator stays at bottom center (exception per design)
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* Consistent modal theming + session security fix
Modal theming:
- All modals now share consistent styling: backdrop-filter blur, semi-transparent
background via --color-a-rgb, 10px border-radius, header divider line, box-shadow
- Updated: loginModal, Help, ConfirmationModal, Settings, Hotkeys, About modals
- Tool panel backgrounds changed from opaque var(--color-k) to transparent so the
ToolPanel's existing backdrop-filter effect shows through
- Legend tool header updated to match consistent 44px height with divider
- applyTheme.js now auto-derives --color-a-rgb from theme's --color-a hex value
- Modal service wrapper gets backdrop-filter: blur(12px)
Session security (Devin Review fix):
- Token re-auth now calls req.session.regenerate() with data preservation to
prevent session fixation while maintaining multi-tab compatibility
- Token is rotated via crypto.randomBytes on every re-auth (was being reused)
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* UI fixes: viewer settings, button sizing, menu contrast, coords, login header
1. Viewer OSD settings moved to top of button stack, panel opens downward
2. Overlay buttons consistent 30x30px (OSD line-height fix, home button)
3. Menu/icon contrast improved: Dropdown items and IconButtons use --color-a5
(was --color-a3) with --color-f on hover for better dark theme legibility
4. CoordinatesDiv fixed to 30px height, pickLngLat button centered
5. Login modal now has a header bar with 'Log In' title and close X button;
title toggles to 'Sign Up' when switching modes
Also reverts session regeneration for token re-auth (Devin Review feedback):
token-based re-auth now refreshes session data in-place without regeneration
or token rotation, preserving multi-tab compatibility.
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* UI polish: nav popover, sep tools, compass, zoom controls, status indicator, toast
1. Description nav popover: added z-index:9000 so menu appears above map panels
2. Separated tools: default color changed from accent to --color-f; fixed CSS
selector from .toolButtonSep to .toolSep to match actual class names
3. Compass + mapping scale shifted left by 30px for better positioning
4. Map zoom/home controls: use --color-f instead of accent --color-c to reduce
visual prominence; hover still highlights with accent color
5. Status indicators (reload/ws disconnect/layer update) moved from Leaflet
control to TopBar with soft pulsing fade animation and tooltip on hover
6. WebSocket retry toast: rounded corners, glass background with backdrop-filter,
border-left accent for failure state instead of solid red background
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* Fix 14 tool UI issues: headers, backgrounds, layout, functional bugs
Header alignment:
- LayersTool: align-items center on #filterLayers, gap between right icons
- InfoTool: align-items center on #infoToolHeader, 44px height
- ViewshedTool: align-items center, restructured header with left/right divs
- IsochroneTool: restructured flat header into nested mmgisToolHeader pattern
- ShadeTool: align-items center on #vstHeader children
Icon spacing:
- LayersTool: increased right margin to 28px + gap 2px
- ViewshedTool: #vstNew padding-right 30px (clear of close button)
- IsochroneTool: #iscNew padding-right 30px
Missing components:
- SitesTool: added Help import + help icon via mmgisToolHeader pattern
- AnimationTool: added full mmgisToolHeader with title and help icon
Background fixes:
- InfoTool: changed toolsContainer background from transparent to var(--color-a)
- DrawTool: changed toolsContainer background from transparent to var(--color-a)
Layout fixes:
- DrawTool: #drawToolContents top 81px, height calc(100%-81px), #drawToolNav margin-right 0
- MeasureTool: removed padding-left:0 override from mmgisToolHeader child selector
Functional fixes:
- InfoTool: updated jQuery selectors from #InfoTool to #toolButtonInfo (React toolbar IDs changed)
- CurtainTool: deferred OpenSeadragon init with requestAnimationFrame (React 18 async render)
- CurtainTool: curtainToolBar justify-content flex-end (icons at bottom)
Security:
- TopBar StatusIndicator: escape HTML in layer names to prevent XSS via addLayerQueue
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* Fix mapToolBar pointer events, login padding, default tool, About modal order
1. mapToolBar: set pointer-events:none on both #mapToolBar and direct children
so clicks pass through to the map; leaf elements still get auto via
.childpointerevents rule
2. #loginModalBody: padding changed to 40px 0px 0px
3. Default tool: deferred click to requestAnimationFrame so React toolbar
has rendered before getElementById runs
4. About modal: moved mainInfoModalCustom to right below mainInfoModalHero
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* Fix tool headers to 40px, ViewshedTool subheader, AnimationTool header, InfoTool close btn, statusIndicator position
1. All tool panel headers: changed from 44px to exactly 40px height
- Global .mmgisToolHeader and .mmgisToolTitle in mmgis.css
- InfoTool.css, ViewshedTool.css, IsochroneTool.css, ShadeTool.css
2. LayersTool #filterLayers: height 40px, .right > div height unset,
.right margin-right 30px, .right > div margin 0px 3px
3. ViewshedTool: restructured header — title+help in mmgisToolHeader row,
vstToggleAll (left) and vstNew (right) on a new #vstSubHeader row below
4. AnimationTool: removed old #animationToolHeader CSS (padding 15px 20px,
white background, 18px font), now uses standard mmgisToolHeader class.
Fixed color from var(--color-a) (background) to var(--color-f) (text)
5. InfoTool close X: re-inject close button after use() rebuilds content
via TC_.injectCloseButton() (toolsContainer.empty() was removing it)
6. StatusIndicator: moved to left of topBarTitle in JSX render order.
Added align-items:center to #topBarMain for vertical alignment.
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* Remove title attr from StatusIndicator (conflicts with tippy tooltip)
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* Fix TimeUI dropdown z-index above tool panel, reposition toasts to top-center
1. TimeUI dropdowns: added z-index 10000 to all dropy content ul elements
so they render above the vertical tool panel (z-index 1400). Also set
timeUIDock to position:relative with z-index 10000 and overflow:visible.
2. Toast notifications: repositioned #toast-container from bottom-right to
top-center just below the topbar (top: 44px, centered with transform).
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* Fix StatusIndicator spacing, CurtainTool close btn, header title font consistency
1. StatusIndicator: use display:none/flex instead of opacity:0/1 so it
takes no space when there's no active status indicator.
2. CurtainTool: added close X button at top of curtainToolBar (matching
MeasureTool pattern) with flex spacer pushing other buttons to bottom.
3. Header title font consistency: InfoTool, ShadeTool, CurtainTool titles
now match mmgisToolTitle standard (font-weight:600, padding-left:10px,
height:40px for CurtainTool which was 34px).
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* Legend empty state, scalefactor position, sep-tool-header unbold
1. Legend: show 'No active layers with legends' when no legend items
are present. Also fixed container height calc(100% - 40px).
2. Mapping Scale (.leaflet-control-scalefactor): shifted 10px right
(left 26→36px) and 1px down (bottom 30→29px).
3. sep-tool-header span: font-weight changed from 600 to 400.
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* Guard Legend empty state message to only show when panel is active
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* Fix TimeUI dropdown covered by toolPanel: remove splitscreens stacking context
#splitscreens had z-index:1 which created a stacking context, confining
its children (including bottomFloatingBar at z-index:1500) to that context.
Since ToolPanel (z-index:1400) was a sibling outside splitscreens, it
painted above all splitscreens children regardless of their internal
z-index values.
Fix: change #splitscreens z-index from 1 to auto so it no longer creates
a stacking context. Now bottomFloatingBar (1500) participates in the
same stacking context as ToolPanel (1400), and 1500 > 1400 means the
TimeUI dropdown correctly paints above the tool panel.
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* Migrate ~69 CursorInfo toast-like calls to proper Toast component
- Replace CursorInfo.update() toast-like calls with Toast.info/success/warning/error
- Preserve message colors: blue→info, green→success, yellow→warning, red→error
- Keep 12 legitimate cursor-following CursorInfo calls unchanged
- Files migrated: DrawTool.js, DrawTool_Files.js, DrawTool_FileModal.js,
DrawTool_Templater.js, DrawTool_SetOperations.js, DrawTool_Drawing.js,
DrawTool_Editing.js, DrawTool_Shapes.js, LayersTool.js, ShadeTool.js,
chemistrychart.js
- Fix Devin Review: Change misleading 'Bad token' to 'Login failed' in users.js
- Normalize line endings (CRLF→LF) in affected files
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* Fix Toast.js missing from git, CoordinatesDiv z-index, Legend duplicate ID, topBar padding
- Add Toast.js to version control (was untracked, causing webpack module error)
- Bump CoordinatesDiv z-index from 20 to 1001 (was hidden behind splitscreens
children after z-index:auto change)
- Fix Legend duplicate ID: separated tool icon was #LegendTool, same as content
container div, causing empty message to appear in button instead of panel
- Add hasStatus class to #topBarMain when statusIndicator is active, setting
#topBarTitleName padding-left to 0
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* Fix Legend empty message: scope selector to content container via targetId
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* Move Toast.js to design-system, fix --color-a3 text contrast, update AGENTS.md
- Move Toast.js from UserInterface_/components/Toast/ to design-system/components/Toast/
(generic component belongs in design-system, not MMGIS-specific UserInterface_)
- Update all 11 Toast import paths to new location
- Bump --color-a3 in 5 dark themes to pass WCAG AA 4.5:1 contrast for text:
Dark Default #747c81→#81888d, Dark Blue #64748b→#738399,
Dark Warm #8b7a5e→#908064, Dark Mars #8a6a60→#98796f,
Dark Midnight #606088→#7a7a9e
- Update AGENTS.md: document design-system/ vs UserInterface_/ distinction
in project structure and Key Directories
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* Fix topBarTitleName padding override: increase specificity and add !important
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* Fix IdentifierTool deactivation: update icon ID reference in separateFromMMWebGIS
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* Hide toolPanelDrag when no tool is open
- Add setToolPanelDragVisible(false) to closeActiveTool() (was only in makeTool toggle-off path)
- Also guard drag handle display on isOpen (toolPanelWidth > 0) as safety net
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* Add hover effect to MMGIS logo (opacity + brightness transition)
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* Fix MMGIS logo hover: keep full opacity, use subtle background highlight instead
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* Selective tile fade: fade on pan/zoom, instant on refresh/reload
- Remove blanket _fadeAnimated toggle from toggleTimeUI (was killing fade
for all tiles while TimeUI was open)
- Monkey-patch GridLayer.redraw, TileLayer.setUrl, and GridLayer._tileReady
to suppress fade via a transient _suppressTileFade map flag
- Set _suppressTileFade in reloadTimeLayers for time-driven reloads
- Flag auto-clears after 300ms so pan/zoom tiles still get the nice fade
- Install pbf dependency (required by CesiumMVTLayer from #942 merge)
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* Per-layer fade control: time-enabled + shade/viewshed layers never fade
- Replace transient map-level _suppressTileFade with per-layer _noFade flag
- Patch GridLayer._tileReady to check _noFade on the layer instance
- Set _noFade on time-enabled tile layers and data/GL layers at creation
- Set _noFade on Shade and Viewshed tool GL layers
- Non-time-enabled base imagery tiles still fade normally on pan/zoom
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix: mobile UI improvements — move hamburger to right menu, show panel toggles, isMobile-driven toolbar layout, desktop-matching scalebar/compass
- Remove left hamburger menu (#topBarMenu), move BottomBar items into
top-right kebab dropdown menu for both mobile and desktop
- Show panel toggles (Viewer/Map/Globe) and account/login UI in mobile
topbar's #topBarRight
- Move toolbar horizontal layout CSS from @media breakpoints to
UserInterfaceMobile_.css (loaded only when isMobile flag is true)
- Remove #mapTopBar @media rule from mmgis.css, add to mobile CSS
- Remove mobile-only simplified scalebar rendering; use full desktop
scalebar with both large and small axes on all viewports
- Remove display:none on .leaflet-control-scalefactor in mobile CSS
- Remove #loginDiv display:none from mobile CSS (React overlay handles it)
- Simplify BottomBarReact container styles (no more absolute positioning)
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* chore: bump version to 4.3.28-20260430 [version bump]
* fix: mobile toolbar 40px height, bottomFloatingBar flush, timeUI toggle, scalebar position, hide hotkeys
- Toolbar height 40px, toolButton width 40px, no border-bottom
- toolcontroller_incdiv: no padding-bottom, overflow-y hidden
- bottomFloatingBar: no border-radius, left/right/bottom = 0
- Add MobileTimeUIToggle button on far right of toolbar
- Hide Keyboard Shortcuts from kebab menu on mobile
- Fix scalebar positioning (remove top:48px override in UserInterfaceBridge)
- Set mobileTopSize/topSize to 40 (splitscreens top = 40px, not 50px)
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix: mobile topBar padding-left 34px
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix: update MobileCoordButton topBar paddingLeft from 80px to 34px
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix: MobileTimeUIToggle — inline toggle logic, float right, hide from settings on mobile
- Replace broken Coordinates.toggleTimeUI() call with direct jQuery/store toggle
- Float time button right in toolbar
- Hide Time UI toggle from settings modal on mobile (toolbar has it)
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix: push scalebar/compass/scale up 40px on mobile, keep #timeUI in DOM
- BottomElementPositioner: position mapToolBar, leaflet-bottom-left/right
40px above bottom on mobile (above toolbar)
- Stop removing #timeUI from DOM on mobile so MobileTimeUIToggle works
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix: mobile TimeUI — only show endtime, always expanded
- Hide #mmgisTimeUIStartWrapper and StartWrapperFake on mobile via CSS
- Force expanded state (addClass expanded + show) when toggling TimeUI on
- CSS ensures #timeUI.active always shows expanded content on mobile
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix: mobile TimeUI opens in tool panel with header, end time, expanded rows
- MobileTimeUIToggle now opens/closes the tool panel via ToolController_
- Closes any active tool before showing TimeUI
- Forces expanded state when opening
- CSS hides start time inputs, positions expanded content properly
- Overrides absolute positioning of expanded content for tool panel flow
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* feat: rewrite separated tools system from jQuery to React components
- Add separatedToolsList/activeSeparatedTools state to Zustand uiStore
- Rewrite SeparatedTools.jsx with glassmorphism panels, CSS Module styling
- Replace SepToolsContainer (setInterval hack) with SepToolButton/SepToolsSection
- Remove ~170 lines of jQuery DOM construction from ToolController_.js
- Fix hardcoded rgba(26,26,27,0.88) to theme-aware var(--color-a-rgb)
- Remove separated tool entries from themeApplier.js
- Remove separated tool overrides from FloatingElements.css
- Move Legend CSS overrides from Toolbar.module.css to SeparatedTools.module.css
- Remove jQuery active-state manipulation from IdentifierTool.js
- Add store sync in Map_.js displayOnStart logic
- Preserve all DOM IDs for backward compatibility (mmgisAPI, tool make())
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* chore: bump version to 4.3.28-20260501 [version bump]
* fix: TimeUI mobile checks — use Zustand store instead of L_.UserInterface_
L_.UserInterface_ is null when TimeUI.init() runs (TimeControl.init is called
before L_.link sets UserInterface_). All 16 isMobile checks now read from
useUIStore.getState().isMobile which is set at startup.
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* chore: bump version to 4.3.29-20260501 [version bump]
* fix: move displayOnStart logic from Map_.js to ToolController_.finalizeTools()
- Map_ no longer references specific tools (LegendTool)
- displayOnStart is now handled generically for all separated tools
- Added DOM element polling (tryMake) to handle React render timing
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* revert: remove all TimeUI-related mobile changes
Reverts TimeUI.js and BottomBar.js to development base.
Restores #timeUI DOM removal in UserInterfaceBridge.fina().
Removes MobileTimeUIToggle component from Toolbar.jsx.
Removes TimeUI mobile CSS overrides from UserInterfaceMobile_.css.
Non-TimeUI refinements (toolbar height, scalebar positioning, etc.) preserved.
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* simplify: remove DOM polling, use simple setTimeout(0) for auto-open
LegendTool handles its own content lifecycle via subscribeOnLayerToggle.
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* feat: mobile TimeUI — fix isMobile detection, staging container, toolbar toggle
- TimeUI.js: import useUIStore and replace all 16 L_.UserInterface_?.isMobile
checks with useUIStore.getState().isMobile (L_.UserInterface_ is null when
TimeUI.init() runs, so mobile conditionals were dead code)
- TimeUI.js: stage mobile #timeUI in hidden #timeUIMobileStaging instead of
placing directly in #tools (which gets cleared by other tools)
- UserInterfaceBridge.js: stop removing #timeUI from DOM on mobile
- Toolbar.jsx: add MobileTimeUIToggle that moves #timeUI between staging and
#tools, opens/closes tool panel via ToolController_
- BottomBar.js: hide TimeUI toggle from settings modal on mobile
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix: rescue #timeUI back to staging when another tool opens
Subscribe to activeToolName changes — when a tool becomes active while
TimeUI is showing, move #timeUI back to #timeUIMobileStaging before
the new tool's make() clears #tools.
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* chore: remove separatedTool/justification config toggles, fix review issues
- Remove separatedTool checkbox and justification dropdown from Legend
and Identifier config.json (these are always separated, not configurable)
- Remove justification property/code from LegendTool.js, IdentifierTool.js
- Simplify Globe_.js separated tool count (no justification filter)
- Remove justification from Reference-Mission config blueprint
- Update LegendTool help docs and Legend.md documentation
- Add --color-a-rgb fallback (29,31,32) in SeparatedTools.module.css
- Add display:none !important to .panelIdentifier to prevent 12px gap
- Update e2e test comment
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix: circular import in TimeUI.js, toolbar/bottomFloatingBar position sync
- TimeUI.js: replace top-level useUIStore import with lazy _getUIStore()
accessor to avoid 'Cannot access useUIStore before initialization'
circular import error at _remakeTimeSlider
- SplitScreens.jsx: skip #timeUI reparenting observer on mobile (mobile
uses MobileTimeUIToggle to manage #timeUI placement in #tools)
- BottomElementPositioner.jsx: unify mobile transition to 0.3s (matches
toolsWrapper and toolbar), guard pxIsTools against undefined
- Toolbar.jsx: align toolbar transition to 0.3s ease-out
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* LegendTool fix empty message
* chore: remove separated tools offset logic from Globe_.js
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix: skip _makeHistogram on mobile (no timeline slider, timestamps unset)
_makeHistogram renders inside the timeline slider which doesn't exist
on mobile. Without it, _timelineStartTimestamp is NaN, causing
'Invalid time value' RangeError at toISOString().
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix: mobile TimeUI — populate expanded rows, fix Invalid date, fix panel height
- TimeUI.js attachEvents: use _initialStart/_initialEnd on mobile (same
as desktop) instead of L_.TimeControl_ which isn't set yet at init time.
Fixes 'Invalid date' in start/end time inputs.
- TimeUI.js fina: set expanded=true on mobile and call _populateExpandedRows()
so year/month/day/hour rows actually render. Removed position:absolute and
pointer-events:none overrides.
- Toolbar.jsx: set tool panel height to 217px (TimeUI.height) instead of
45% viewport — matches actual TimeUI content height.
- UserInterfaceMobile_.css: expanded content flows naturally (position:relative),
hide start time inputs, allow overflow scroll, flex-wrap topbar.
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix: mobile TimeUI justify-content center, restore toolbar border-bottom
- Add justify-content: center to #mmgisTimeUIMain on mobile
- Remove border-bottom: none override so toolbar keeps its default border
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix: mobile TimeUI overflow hidden, scalebar/compass fixed at 40px offset
- #timeUI overflow-y: hidden (was auto, causing 2px scroll)
- Scalebar/compass/map controls stay at fixed 40px offset (above toolbar)
regardless of tool panel state — no longer shift up by pxIsTools
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* Implement multi-tier knowledge architecture
- Restructure AGENTS.md from 745 lines to 106 lines (Tier 1: essential context)
- Create knowledge/ directory with 30+ wiki-style documentation files (Tier 2: deep knowledge)
- Create knowledge/reference/ with 8 detailed reference files (Tier 3: lookup material)
- Move AI-GETTING-STARTED.md and AI-DEVELOPMENT.md to knowledge/
- Update all file references in .specify/templates and blueprints
- Create knowledge/README.md as the full knowledge base index
- Create knowledge/reference/README.md as reference material index
Three-tier knowledge discovery system:
Tier 1: AGENTS.md (~106 lines) - scannable in <2 minutes
Tier 2: knowledge/*.md - deep knowledge on architecture, tools, APIs, DB, infra
Tier 3: knowledge/reference/*.md - coding conventions, API reference, troubleshooting
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* chore: bump version to 4.3.29-20260501 [version bump]
* fix: mobile toolbar active button style matches desktop, fix icon alignment
- All mobile toolbar buttons (ToolButton, MobileCoordButton, MobileTimeUIToggle)
now use display:flex with align-items/justify-content center for proper
vertical icon centering
- MobileCoordButton: changed 'active' class to 'toolButtonActive' to match
the global CSS active style (color-mmgis + color-i background)
- Removed inline color overrides so CSS .toolButtonActive takes effect
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* Add Devin knowledge notes from past MMGIS sessions
Include curated lessons learned from past Devin sessions:
- CI/CD: ignore build-arm64/amd64 failures, focus on required checks
- Child sessions: no separate PRs when consolidating
- ENV triple-update rule (.env, sample.env, ENVs.md)
- Error handling: use logger with infrastructure_error for fatal startup errors
- Path traversal security: stay within /Missions, handle subpath serving
- Database initialization architecture and migration patterns
- API authentication behavior across AUTH modes
- Auto-generated MMGIS concept index
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix: mobile toolbar active button style, icon alignment, tool deactivation
- Active toolbar buttons get desktop-matching margin (1px 0) and
border-radius (8px) via .toolButton.toolButtonActive CSS rule
- Removed line-height: 40px from .toolButton (flex centering handles
vertical alignment, line-height was pushing icons up)
- MobileCoordButton now watches activeToolName store and deactivates
when another tool opens (fixes coords staying active)
- MobileTimeUIToggle sets activeToolName='MobileTimeUI' when opening
so coords/other buttons can detect it and deactivate
- MobileTimeUIToggle clears activeToolName when closing
- Both custom buttons skip self-deactivation via name check
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* Fix cross-references: convert backtick refs to markdown links, add Devin knowledge notes
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix: mobile toolbar icon height 40px, button margins for active padding
- #toolbar .toolButton i: height 40px fixes icon vertical alignment
- #toolbar .toolButton: margin 0 2px gives spacing between buttons
- #toolbar .toolButton.toolButtonActive: margin 1px 2px so active
background has visual padding around the icon
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* Rename knowledge/ to .knowledge/ for consistency with .specify/ convention
Dot-prefix signals agent infrastructure (not source code), consistent with
.specify/, .github/, .vscode/ conventions. All cross-references updated.
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix: mobile toolbar icon line-height 40px, active button padding via height
- Coord and TimeUI button <i> icons get line-height: 40px
- Active buttons: height 34px (vs 40px toolbar) creates visual padding
around the active background, centered by flex align-items
- Buttons get margin: 0 1px for horizontal spacing
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* Fix broken cross-reference: 06.2 -> 06.1-configure-rest-api.md
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix: close active tool + cancel deferred cleanup in MobileCoordButton/TimeUI
- MobileCoordButton: call closeActiveTool() before opening, destroy
_pendingCloseTool if set, increment _closeSeq to cancel deferred
tools.innerHTML clear
- MobileTimeUIToggle: same _pendingCloseTool + _closeSeq fix after
closeActiveTool() to prevent 420ms deferred cleanup from wiping
#timeUI after it's placed in #tools
- Removed redundant closeActiveTool() from MobileCoordButton close path
(was being called after destroy, not needed)
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix: active mobile toolbar buttons 34x34px (square)
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* Drastically compress .knowledge/ — keep only unique agent content
Remove 33 wiki files that duplicate docs/pages/ content.
Remove 9 reference/ files derivable from source code.
Keep only 5 files (down from 46):
- AI-GETTING-STARTED.md (agent setup walkthrough)
- AI-DEVELOPMENT.md (spec-kit workflow)
- conventions-and-gotchas.md (naming, code style, common issues)
- 12-devin-knowledge-notes.md (CI, auth, DB init, security gotchas)
- README.md (index pointing to docs/pages/ for everything else)
Principle: don't duplicate docs/ — only keep what's uniquely agent-optimized.
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* Rename to knowledge-notes.md, remove Devin branding and fork-specific CI section
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix: hide mmgis-map-logo on mobile
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* Restore Database Safety Rules for AI Agents section in AGENTS.md
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix: shift compass and map scale 6px to the right (both mobile and desktop)
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* Add back Important Instructions, code pattern templates, and detailed project structure
- Important Instructions in AGENTS.md: MCP tools, hot-reload, Reference Mission
- .knowledge/code-patterns.md: full directory tree with key directory annotations,
plus copy-paste templates for Express routes, Sequelize models, Tool plugins,
and WebSocket handlers
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* Update project structure trees to reflect current filesystem
Add missing directories: tests/, .knowledge/, .specify/, .github/, views/,
pri…
…974) * Re-apply 36 UI improvements on development's React architecture Adapted all 36 UI tasks from PR #47 to work with development's full React component architecture (Toolbar, Splitter, BottomBarReact, etc.) instead of PR #45's null-stub + jQuery layout. Layout & Positioning (Tasks 1, 4, 5, 11, 16, 17, 20, 27): - BottomElementPositioner: immediate positioning with toolPanelWidth offset - Scalebar/compass: 12px permanent right push + tool panel width - Toolbar: flex-direction column layout Controls & Navigation (Tasks 2, 9, 10): - Map zoom: Home button resets to configured initial view - BottomBarReact: reduced to About + Copy Link only - TopBar: kebab menu with Screenshot, Fullscreen, Hotkeys, Settings Tool System (Tasks 3, 6, 7, 13): - Tool buttons: CSS classes (.toolButtonActive) instead of inline styles - Tool headers: standardized with mmgisToolHeader/mmgisToolTitle classes - MeasureTool: close button at top, reset/download at bottom Visual Polish (Tasks 8, 14, 18, 25, 26, 29, 30, 32, 34, 36): - Splitter: invisible by default, accent color on hover - Compass: smooth left transition - Coordinates: background, separator between mouse/pick - mmgislogo: border-right matching topbar border-bottom - toolsWrapper: no border in bottomFloatingBar TimeUI & Settings (Tasks 15, 23, 24, 28): - toggleTimeUI button removed from coordinates - TimeUI toggle added to Settings modal - Popover positioning uses window.innerHeight - bcr.top Modals & Config (Tasks 12, 19, 21): - About modal: MMGIS logo, version, attributions, markdown content - Modal close: proper click handler with stopPropagation - Attributions: removed from DOM, collected for About modal Panel Sync (Task 31): - TopBar panel toggles sync with Zustand store via subscribe Dependencies & Config: - Added dompurify for markdown sanitization - Added aboutModalContent to config template and Reference Mission - Added 9 help markdown files for tools Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Add theming system (6 presets), floating tool panel, CSS variable support - Port design-system: themes.js (6 presets), applyTheme.js, themeApplier.js, useTheme.js - Wire theme into Zustand store (themeName + setTheme action) - Wire theme into UserInterfaceBridge init/fina and Stylize.js - Add theme dropdown to configure page (look.theme field) - Update mmgisUI.css to use CSS variables for theme support - Make tool panel float over map instead of docking/pushing - Hide splitter drag handles by default - Fix Coordinates.css margin for removed TimeUI button Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Fix useTheme.js for React 16 compatibility (useState/useEffect instead of useSyncExternalStore) Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Fix layout to match PR #47: floating tool panel, bottom bar, toolbar, topbar - ToolPanel: 12px inset margins from toolbar/topbar, border-radius 10px, backdrop-filter blur(20px), opacity transition on open/close - Toolbar: box-shadow, z-index 101, 34x34 tool buttons with border-radius 8px, starts below topbar (top: topSize), Toolbar.css imported - TopBar: solid background + border-bottom via themeApplier, box-shadow via CSS - Bottom floating bar: new BottomFloatingBar wraps toolsWrapper + timeUIDock inside #splitscreens with 12px margins, backdrop blur, border-radius - TimeUI reparented into floating bar via MutationObserver - BottomElementPositioner: recalculated offsets based on floating bar height - Separated tools: offset by toolPanelWidth + 24px when tool panel is open - Splitter arrow buttons hidden (panel toggles in TopBar replace them) - FloatingElements.css: backdrop blur for TimeUI, coordinates, compass, zoom - SplitScreens.css, BottomBarReact.css, ToolPanel.css, Toolbar.css added Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Fix TopBar: stays full-width when tool panel opens (matches PR #47) - Remove toolPanelWidth/toolsWrapperRawWidth reactive styles that shifted TopBar right when tool panel opened - TopBar now always uses left:0, width:100%, padding-left:40px (from CSS) - Tool panel floats underneath TopBar (z-index 2005 > 1400) - Add theme colors for toggle buttons, user avatar via CSS variables - Remove redundant z-index override in TopBar.css Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Fix ToolPanel drag handle offset: use panelLeftOffset instead of hardcoded 10 The drag handle's left position uses panelLeftOffset (52px on desktop) but the drag calculation was using 10 as offset, causing width to inflate by 42px after each drag-resize. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Fix TopBar solid background and splitscreens positioning below TopBar - Remove minimalist(true) call from fina() — matches PR #47 which also removed it. Splitscreens now correctly starts at top:40px (topSize) instead of top:0px which caused map to render under the TopBar. - Add background: var(--color-a) and border-bottom: 1px solid var(--color-a1) to #topBar CSS so it's solid from initial render (themeApplier.js overrides on theme change). Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Port separated tools and Legend changes from PR #47 - SeparatedTools.jsx returns null — separated tool rendering moved to ToolController_.js jQuery DOM construction (matches PR #47) - ToolController_.init() now creates floating glassmorphism panels for separated tools (Legend, Identifier) with header/close buttons - Separated tool buttons appear in toolbar below a divider - LegendTool.js header updated to use mmgisToolHeader/mmgisToolTitle classes with Help integration - tab-ui-config.json: removed Default Tool section, renamed Colors to Advanced Color Overrides - Maker.js: removed defaulttooldropdown component type Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Show MMGIS logo by default (was hidden behind minimalist() call) Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Add tool panel close button, separated tool improvements - Add injectCloseButton() to ToolController_ with X button in upper right of tool panel (both vertical and horizontal tools) - Call injectCloseButton() after tool.make() in makeTool() - Add tippy tooltips to separated tool buttons (Legend, Identifier) - Add divider background color using CSS variable - Add isMobile check to skip separated tools on mobile (matching PR #47) - Order Legend button last in separated tools section (matching PR #47) Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Fix bottom bar layout for all 4 tool/TimeUI combinations - Remove direct CSS manipulation from TimeUI._updateBottomUIHeight(), delegate to centralized BottomElementPositioner via Zustand store - Add React close button to BottomFloatingBar for horizontal tools (replaces jQuery injection that was wiped by React re-renders) - Skip jQuery close button injection for horizontal tools since React BottomFloatingBar now handles it - Add leaflet-bottom-left positioning to BottomElementPositioner - All 4 combos verified: no tool/collapsed, no tool/expanded, measure/collapsed, measure/expanded Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Fix 13 UI/layout issues in PR #48 1. Remove redundant loginDiv and loginoutButton (hidden via useEffect) 2. mmgislogo border-right now uses var(--color-a1) matching topbar border-bottom 3. About button moved below Copy Link in toolbar bottom 4. Separated tools (Legend, Identifier) now appear in toolbar via SepToolsContainer 5. Map zoom/home controls use solid background matching topbar/toolbar 6. bottomFloatingBar z-index raised to 1500 (above toolpanel 1400) 7. Compass offset reduced from +38 to +8 (closer to scalebar) 8. mmgisToolHeader now flex row so MeasureTool [Title ? ... undo reset] layout works 9. Toolbar z-index raised to 2006 (above topbar 2005, prevents box-shadow overlap) 10. Light theme text contrast: CoordinatesDiv, topBarMain, TimeUI buttons, DrawTool 11. TimeUI timeline colors now update with theme changes via themeApplier 12. DrawTool tabs visible: #drawToolNotLoggedIn now starts at top:81px below nav 13. Bottom floating bar left offset accounts for tool panel width Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Fix 16 UI issues (#14-29) 14. Bottom bar no longer pushed by vertical tool panel 15. LayersTool header restored (reverted mmgisToolHeader flex, fix MeasureTool inline) 16. Separated Legend panel pushed right when vertical tool opens 17. Legend appears before Identifier in toolbar 18. loginDiv/loginoutButton hidden via CSS !important 19. Horizontal tool panel width fixed (always 100% of bottom bar) 20. Identifier tool button no longer auto-activates on load 21. Splitters use theme accent color (--color-mmgis) 22. Vertical toolpanel drag edge styled like splitters 23. Horizontal toolpanel drag handle (6px accent bar at top) 24. Compass positioning simplified (flows with leaflet container) 25. Light theme text: Coordinates.css uses var(--color-f), themeApplier strengthened 26. Globe toggle: Globe_.init() instead of non-existent lazyInit() 27. Separated Legend panel theme: headers and content text use theme colors 28. InfoTool null guard on destroy (MMGISInterface may be null) 29. Splitter drag no longer offset by tool panel width (panel floats now) Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Fix computeToolsSplitMoveResult min clamp to preserve splitterSize/4 fallback The test expects splitterSize/4 as minimum when toolNativeHeight is not set. Use Math.max(toolNativeHeight, splitterSize/4) so both constraints apply. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Fix 8 UI issues (#30-37) 30. Remove duplicate close button (React BottomFloatingBar one removed; tool's own stays) 31. Splitters/drag handles transparent by default, accent color on hover/drag only 32. Horizontal tool splitter follows mouse (disable CSS transition during drag via isDraggingSplitter flag) 33. Legend pushed right by extra 12px gap when vertical tool panel open 34. Compass (leaflet-bottom-left) pushed right when vertical tool panel opens 35. TimeUI mode dropdown z-index raised above tool panel 36. Glassy backdrop-filter on floating elements (CoordinatesDiv container transparent) 37. CoordinatesDiv: height 30px, bottom/right 12px permanent offset Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * React 18 upgrade + Base UI adoption + CSS Modules migration - Upgrade react and react-dom from ^16.13.1 to ^18.2.0 - Upgrade react-app-polyfill to ^3.0.0, react-resize-detector to ^9.1.0 - Migrate entry point (src/index.js) to createRoot API - Fix ReactDOM.render in CurtainTool.js and PDFViewer.js - Fix ReactDOM.createPortal import in UserInterfaceLayout.jsx - Install @base-ui-components/react@1.0.0-rc.0 Design system wrapper components (src/design-system/components/): - Button.jsx + Button.module.css (primary/secondary/ghost variants) - IconButton.jsx + IconButton.module.css - Dropdown.jsx + Dropdown.module.css (wraps Base UI Menu) - Toggle.jsx + Toggle.module.css (panel toggle groups) - Modal.jsx + Modal.module.css (wraps Base UI Dialog) - Tooltip.jsx + Tooltip.module.css (wraps Base UI Tooltip) CSS Modules migration for shell components: - TopBar: Rewritten with Toggle and Dropdown components, CSS Modules - ToolPanel: CSS Modules, replaced hardcoded rgba with color-mix() - SplitScreens: CSS Modules, replaced hardcoded rgba with theme vars - Splitter: Added Splitter.module.css - Toolbar: Migrated to CSS Modules - BottomBarReact: Migrated to CSS Modules - UserInterfaceLayout: Migrated to CSS Modules themeApplier.js cleanup: - Removed imperative selectors for #topBar, #toolPanel, #bottomFloatingBar, #topBarTitleName, #topBarRight icons, #topBarMain (now CSS Modules) - Kept selectors for jQuery/Leaflet/TimeUI elements webpack.config.js: Added namedExport: false for css-loader v7 compatibility Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Upgrade react-chartjs-2 to ^4.3.1 for React 18 peer dep compatibility react-chartjs-2@3.3.0 only supported react ^16.8.0 || ^17.0.0. v4.3.1 adds react ^18.0.0 support while keeping chart.js ^3.5.0 compat. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Fix #topBarInfo ID conflict: rename About button to #bottomBarAbout The About button was reusing the #topBarInfo ID, which caused: 1. UserInterfaceBridge.js visibility logic hiding it unless look.info/infourl set 2. Stylize.js jQuery click handler opening infourl simultaneously with About modal Renamed to #bottomBarAbout so it doesn't conflict with the legacy #topBarInfo jQuery handler. The original #topBarInfo info-URL behavior remains untouched. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Address Devin Review findings: infourl, theme override, dead code - Add look.infourl as a row in the About modal so it remains accessible - Remove dead jQuery click handler for #topBarInfo in Stylize.js - Remove duplicate setTheme() in UserInterfaceBridge.fina() that was clobbering individual color overrides already applied by Stylize.js Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Fix config visibility toggles for kebab menu items + clean up dead handlers - Add lookConfig to Zustand store, set from UserInterfaceBridge.fina() - TopBar: conditionally render Screenshot, Fullscreen, Settings, Copy Link dropdown items based on lookConfig (screenshot, fullscreen, settings, copylink) - BottomBarReact: conditionally render Copy Link button based on lookConfig - Remove dead jQuery click handlers for #topBarHelp and #topBarInfo in Stylize.js - Remove dead DOM getElementById calls in UserInterfaceBridge for elements that no longer exist (topBarScreenshot, topBarFullscreen, bottomBarSettings, etc.) Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Fix toggleTimeUI() reading state from removed #toggleTimeUI element The #toggleTimeUI button was removed from the Coordinates markup but toggleTimeUI() still read active state from it. Since the element doesn't exist, $('#toggleTimeUI').hasClass('active') always returns false, causing: - Map_.map._fadeAnimated always set to false (fade animations never re-enabled) - L_._onTimeUIToggleSubscriptions always told TimeUI is being turned ON Fix: read state from #timeUI (which exists and tracks the active class) instead of #toggleTimeUI. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Improve Base UI adoption and reduce :global() overuse - TopBar: use IconButton for sign-in/kebab buttons, use Dropdown for user card popup (replaces hand-rolled outside-click + manual popup with Base UI Menu for keyboard nav, focus trap, accessibility) - BottomBarReact: use IconButton + Tooltip instead of raw <i> + tippy.js - Toolbar: use Tooltip instead of tippy.js for tool button tooltips - Reduce :global() from 33 to 22 instances — all remaining are justified by external jQuery/imperative code references: - SplitScreens: convert #viewerScreen/#globeScreen to scoped classes - Toolbar: convert #toolcontroller_incdiv to scoped class - Splitter: convert .splitterV to scoped class - UserInterfaceLayout: consolidate TopBar styles into TopBar.module.css - BottomBarReact: fully scoped (IconButton handles all button styling) Design system usage: 5 of 6 wrappers now imported across 3 files (Toggle, Dropdown, IconButton, Tooltip — only Button/Modal unused, which is appropriate since there are no standalone button/modal cases in the main site shell). Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Fix: remove stale setShowUserCard call in handleLogout Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Migrate Ancillary UI components from jQuery to React/native DOM - Modal.js: React-based with imperative API bridge (Modal.set/remove) so existing jQuery callers (BottomBar, DrawTool, AnimationTool, etc.) work without changes. Uses React state + createRoot for rendering. - ConfirmationModal.js: Removed jQuery dependency, uses native DOM event listeners with the React Modal backend. - Help.js: Removed jQuery dependency, uses native fetch() + DOM event listeners instead of $.get() and $().on(). - ContextMenu.js: Removed jQuery dependency entirely. Uses native DOM APIs (createElement, addEventListener, querySelectorAll) for building and managing the right-click context menu. - Compass.js: Removed jQuery dependency. Uses native DOM APIs (getElementById, querySelector) for compass element creation and bearing updates. - MapLogo.js: Removed jQuery dependency. Uses native DOM APIs for logo element creation and Leaflet container insertion. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Migrate Ancillary components to true React + Base UI - Modal.js: React component using Base UI Dialog with shared state store, imperative Modal.set()/remove() API preserved for backwards compatibility. Accepts both HTML strings (legacy callers) and React elements (new callers). - ConfirmationModal.js: React JSX component using design-system Button for Yes/No actions, rendered through Modal service. Same prompt() API. - Help.js: React JSX component using native fetch + showdown for markdown rendering, rendered through Modal service. Same getComponent/finalize API. - ContextMenu.js: Full React component with createRoot, JSX menu items with proper event handlers. Same init()/remove() API. - Compass.js: React component rendered via createRoot into Leaflet bottom-left container. SVG compass with bearing rotation on map events. - MapLogo.js: React component rendered via createRoot into Leaflet bottom-right container. Configurable size and link support. All components use CSS Modules. Old plain CSS files removed. Also fixes: - About modal now respects look.help/look.info config flags - Cleaned up dead #toggleTimeUI references in Coordinates.js Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Fix modal, tooltip, measure, and context menu bugs - Modal: Fix About modal not opening due to async createRoot in React 18. ModalHost now initializes from shared state and syncs on mount. Blur management moved to ModalHost (React state-driven), fixing persistent blur after modal close. - Tooltip/Dropdown: Use render prop on Trigger to avoid nesting <button> inside <button> (IconButton is already a button element). - MeasureTool: Migrate from deprecated ReactDOM.render to createRoot. Register Chart.js scales via Chart.register(...registerables) to fix 'linear is not a registered scale' error with react-chartjs-2 v4. - ContextMenu: Fix crash when right-clicking on LithoSphere scene (native events lack originalEvent). Store element and handler refs for proper cleanup, preventing listener leaks. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Fix Modal: remove close button, fix blur persistence, add fade animation - Remove the Dialog.Close button (curved bottom-left border-radius) - Drop Base UI Dialog wrapper entirely — it was fighting with the imperative Modal.set/remove API. Now uses simple divs with CSS transitions, matching the original jQuery modal behavior exactly. - Blur management is now purely imperative via _applyBlur() called synchronously in set() and remove(). Removed async useEffect approach. - Add 500ms CSS opacity fade-in/fade-out transition matching original. - Closing state: Modal.remove() marks modal as closing (triggers opacity 0 transition), then removes from DOM after 500ms. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Rename Ancillary React components to .jsx, fix modal blur via Zustand store, fix IconButton forwardRef, add Help.finalize calls - Rename Modal, ConfirmationModal, Help, ContextMenu, Compass, MapLogo from .js to .jsx - Route modal blur through Zustand modalBlurCount instead of imperative DOM manipulation - Remove conflicting jQuery blur animation in Layers_.js - Wrap IconButton with React.forwardRef to fix Tooltip/Menu trigger warnings - Add Help.finalize() calls to ChemistryTool, DrawTool, IsochroneTool - Change aboutModalContent config type from 'markdown' to 'textarea' Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Revert aboutModalContent type back to 'markdown' — Maker.js already handles it Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Fix Help.jsx: check res.ok on fetch, sanitize HTML with DOMPurify Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Fix ContextMenu WKT null guard, fix Modal blur timing during fade-out - Add null check for feature in handleActionClick WKT placeholder handling - Delay blur removal until after 500ms fade-out completes (blur stays in sync with backdrop opacity) Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Reorganize folder structure: dissolve Ancillary, nest components - Dissolve src/essence/Ancillary/ entirely - UI components → UserInterface_/components/ (Modal, ConfirmationModal, Help, ContextMenu, Compass, MapLogo, CursorInfo, Attributions, Login) - Layout chrome → UserInterface_/components/ (Description, Coordinates, Search, ScaleBar, ScaleBox) - Pure services → essence/services/ (DataShaders, LocalFilterer, QueryURL, Sprites) - Stylize.js → design-system/ (theme bridge alongside themeApplier) - Delete unused Swap.js - Nest all components into own folders (ComponentName/ComponentName.ext pattern): - UserInterface_/components/: TopBar/, Toolbar/, ToolPanel/, SplitScreens/, Splitter/, BottomBar/, BottomElementPositioner/, Layout/, Panels/ - design-system/components/: Button/, IconButton/, Dropdown/, Toggle/, Modal/, Tooltip/ - Update ~70+ import paths across codebase - Build verified locally Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Fix Modal.set() race condition and themeApplier CSS variable override issue - Modal.set() onAddCallback: Replace 50ms setTimeout with MutationObserver that waits for the modal element to appear in DOM before firing callback. Prevents silent jQuery binding failures on slower devices. - themeApplier: Use Proxy to read computed CSS custom properties (set by Stylize.js per-mission overrides) instead of hardcoded theme object values. Stylize.js now calls refreshThemeDOM() after setting CSS variables so inline styles reflect mission-specific color overrides. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Sync toggleTimeUI DOM state to Zustand store toggleTimeUI() now calls setTimeUIActive() and setTimeUIExpanded() so BottomFloatingBar visibility and BottomElementPositioner offsets reflect actual TimeUI state. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Restore #toggleTimeUI element in Coordinates markup The element was accidentally dropped during the folder restructure move. TimeUI.js and DrawTool.js check $('#toggleTimeUI').hasClass('active') to gate histogram rendering and time-filter toggle visibility. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Fix #toggleTimeUI: restore click handler, tippy, active class; remove redundant jQuery positioning Restored pieces lost during folder restructure: - Click handler in init() and off handler in remove() - Tippy tooltip for the time toggle button - display:none when time is not enabled - $('#toggleTimeUI').toggleClass('active') so TimeUI.js can check it - $('#CoordinatesDiv > #toggleTimeUI').remove() on mobile Removed jQuery CSS positioning from toggleTimeUI() since BottomElementPositioner now reactively handles all bottom-anchored element offsets via the Zustand store. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Fix CurtainTool.destroy() using undefined ReactDOM.unmountComponentAtNode Use the stored _reactRoot.unmount() instead, matching the React 18 createRoot pattern already used in make(). Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Fix tooltips, scale indicator position, modal blur, help close button Tooltips: - Reduce Base UI Tooltip delay from 600ms (default) to 200ms - Restyle tooltip popup to match tippy blue theme (var(--color-c2)) - Add Tooltip wrappers to TopBar panel toggles (Viewer/Map/Globe) - Wrap Toggle with forwardRef so Tooltip render prop can attach ref - Remove title attrs that conflicted with custom tooltips Scale indicator: - Remove scalefactor-specific positioning from BottomElementPositioner (it moves naturally with .leaflet-bottom.leaflet-left container) - Position scalefactor to the left of compass at same bottom level Modal blur: - Call _applyBlur() immediately when marking modal as closing so blur clears during fade-out instead of persisting 500ms Help modal: - Add close (X) button in title bar matching other modal patterns Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Remove dead CSS: delete tools.css, clean ~600 lines from mmgisUI.css and mmgis.css - Delete tools.css entirely (both selectors #CurtainToolList and .searchToolSelect are unreferenced anywhere in the codebase) - Remove from mmgisUI.css: .mmgisRadioBar3/4/Vertical (140 lines), .mmgispureselect (104 lines), blink/condemned_blink_effect (38 lines), .slidecontainer/.slider (41 lines), .ar_slider (91 lines), .verticalSlider (91 lines), .mmgisMultirange_elev (19 lines), .ui-corner-all/bottom/right/br (9 lines) - Remove from mmgis.css: #nodeenv, empty #topBar{}, #topBarInfo, #topBarHelp, #topBarFullscreen, #toggleUI, #logoGoBack - Keep #topBarLink (used in BottomBarReact.jsx), #webgl-error-message (used by vendored THREE.js) - All selectors verified with repo-wide grep before removal Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * UI fixes: tooltips, splitter hover, mobile toolbar, color schemes - Replace Base UI Tooltip with simple React portal tooltip (200ms delay, tippy-matching style) — fixes missing tooltips for toolbar/topbar/bottom buttons - Add cursor + hover highlight to vertical splitters (was missing because module CSS didn't inherit global .splitterV styles) - Add hover highlight to tool panel drag handle - Remove mdi-drag-vertical icon from tool panel drag - Add mobile toolbar horizontal layout via @media query overrides - Add 4 new color schemes: High Contrast (a11y), Dark Mars, Dark Midnight, Light Warm (total: 10 themes) - Previous fixes also included in working tree: - timeUI border moved to toolsWrapper border-bottom (conditional) - #toggleTimeUI button removed entirely - CoordinatesDiv: vertical centering, unified background, 12px right offset - barBottom padding-bottom: 8px Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Fix assignment operator used instead of comparison in TimeControl.fina() Pre-existing bug: `TimeControl.enabled = true` was assigning instead of comparing. Changed to `TimeControl.enabled === true`. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Restructure Configure page UI tab: add all themes, Custom mode, enableWhenField - Added all 10 theme presets to dropdown (was missing Dark Mars, Dark Midnight, Light Warm, High Contrast) - Added 'Custom' option: skips preset theme, uses only color picker values - Moved Theming section directly under Rebranding - Nested 'Custom Color Options' under Theming with subdescription - Added enableWhenField support to Maker.js: disables color pickers unless theme is set to Custom - Renamed color options with clearer names and improved descriptions: Primary → Surface Color, Secondary → Deep Background Color, Tertiary → Text Color, Body → Page Body Color, Highlight → Feature Highlight - Stylize.js: skip setTheme() when theme is 'Custom' - Rebuilt configure page Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Revert tooltips to tippy.js, fix dropdown menus, redesign About modal 1. Tooltip: Replaced custom React portal tooltip with tippy.js wrapper. Uses the existing tippy.js dependency and 'blue' theme for consistency. 2. Dropdown: Replaced Base UI Menu with native portal dropdown. Base UI's nested Menu.Trigger + BaseButton composition was swallowing click events, breaking userAvatar and menuBtn menus. New implementation uses simple state + createPortal with proper outside-click dismissal. 3. About modal: Professional redesign with centered MMGIS ASCII art header, proper GitHub SVG logo link, clean metadata section, centered link buttons, attributions section, and NASA-AMMOS footer. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Restore .mmgisHelpButton base styles lost during Help.css module migration The global .mmgisHelpButton styles (yellow color, compact 18x18px sizing, 0.7 opacity) were removed when Help.css was converted to Help.module.css. Since Help.getComponent() emits raw HTML strings for jQuery-rendered tool headers, it cannot use CSS Module scoped classes. Restored the base styles in mmgis.css alongside the related .mmgisToolHelpBtn definition. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Fix session logout regression, About modal refinements, High Contrast theme, Stylize.js, Default Tool config - Login: skip session.regenerate() for token-based re-auth (useToken:true) so reloading the main page no longer invalidates the configure page session - About modal: replace ASCII art with mmgis.png logo, rename Attributions to Map Layer Attributions, remove footer logo, link NASA-AMMOS to ammos.nasa.gov - High Contrast theme: change accent from #ffff00 to #ffd700 (gold) for better contrast ratios against dark backgrounds - Stylize.js: color overrides only apply when theme is Custom or unset, preventing preset themes from being clobbered by stale config values - Restore Default Tool config section in tab-ui-config.json (accidentally removed during Theming section reorganization) Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Restore defaulttooldropdown case handler in Maker.js The case was accidentally removed during the Configure page UI tab restructure (d7f96c50). Without it, the Default Tool dropdown in the Configure page rendered as nothing despite the config referencing it. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * High Contrast tooltip text, panel toggle styling, scale position swap - High Contrast theme: tooltips now use black text on yellow background via --color-c2-text variable (white for all other themes) - About modal links use var(--color-f) for consistent theme text color - Panel toggle buttons: 11px uppercase with 600 weight for better visibility - Mapping scale button moved to bottom-right of compass (was top-left) Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Reposition Viewer and Globe panel buttons to top-right - Viewer: dropdown selector at top-right edge, OSD buttons stacked vertically below it; settings panel opens to the left - Globe: home, exaggerate, observe, walk, link controls moved from TopLeft to TopRight corner via addControl 4th arg - Style consistency: OSD buttons and LithoSphere controls now match Leaflet zoom controls (var(--color-a) bg, var(--color-f) text, var(--color-mmgis) hover, 30px size, 3px border-radius) - Viewer settings sliders use var(--color-a3) instead of hardcoded #444444 - Az/el indicator stays at bottom center (exception per design) Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Consistent modal theming + session security fix Modal theming: - All modals now share consistent styling: backdrop-filter blur, semi-transparent background via --color-a-rgb, 10px border-radius, header divider line, box-shadow - Updated: loginModal, Help, ConfirmationModal, Settings, Hotkeys, About modals - Tool panel backgrounds changed from opaque var(--color-k) to transparent so the ToolPanel's existing backdrop-filter effect shows through - Legend tool header updated to match consistent 44px height with divider - applyTheme.js now auto-derives --color-a-rgb from theme's --color-a hex value - Modal service wrapper gets backdrop-filter: blur(12px) Session security (Devin Review fix): - Token re-auth now calls req.session.regenerate() with data preservation to prevent session fixation while maintaining multi-tab compatibility - Token is rotated via crypto.randomBytes on every re-auth (was being reused) Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * UI fixes: viewer settings, button sizing, menu contrast, coords, login header 1. Viewer OSD settings moved to top of button stack, panel opens downward 2. Overlay buttons consistent 30x30px (OSD line-height fix, home button) 3. Menu/icon contrast improved: Dropdown items and IconButtons use --color-a5 (was --color-a3) with --color-f on hover for better dark theme legibility 4. CoordinatesDiv fixed to 30px height, pickLngLat button centered 5. Login modal now has a header bar with 'Log In' title and close X button; title toggles to 'Sign Up' when switching modes Also reverts session regeneration for token re-auth (Devin Review feedback): token-based re-auth now refreshes session data in-place without regeneration or token rotation, preserving multi-tab compatibility. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * UI polish: nav popover, sep tools, compass, zoom controls, status indicator, toast 1. Description nav popover: added z-index:9000 so menu appears above map panels 2. Separated tools: default color changed from accent to --color-f; fixed CSS selector from .toolButtonSep to .toolSep to match actual class names 3. Compass + mapping scale shifted left by 30px for better positioning 4. Map zoom/home controls: use --color-f instead of accent --color-c to reduce visual prominence; hover still highlights with accent color 5. Status indicators (reload/ws disconnect/layer update) moved from Leaflet control to TopBar with soft pulsing fade animation and tooltip on hover 6. WebSocket retry toast: rounded corners, glass background with backdrop-filter, border-left accent for failure state instead of solid red background Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Fix 14 tool UI issues: headers, backgrounds, layout, functional bugs Header alignment: - LayersTool: align-items center on #filterLayers, gap between right icons - InfoTool: align-items center on #infoToolHeader, 44px height - ViewshedTool: align-items center, restructured header with left/right divs - IsochroneTool: restructured flat header into nested mmgisToolHeader pattern - ShadeTool: align-items center on #vstHeader children Icon spacing: - LayersTool: increased right margin to 28px + gap 2px - ViewshedTool: #vstNew padding-right 30px (clear of close button) - IsochroneTool: #iscNew padding-right 30px Missing components: - SitesTool: added Help import + help icon via mmgisToolHeader pattern - AnimationTool: added full mmgisToolHeader with title and help icon Background fixes: - InfoTool: changed toolsContainer background from transparent to var(--color-a) - DrawTool: changed toolsContainer background from transparent to var(--color-a) Layout fixes: - DrawTool: #drawToolContents top 81px, height calc(100%-81px), #drawToolNav margin-right 0 - MeasureTool: removed padding-left:0 override from mmgisToolHeader child selector Functional fixes: - InfoTool: updated jQuery selectors from #InfoTool to #toolButtonInfo (React toolbar IDs changed) - CurtainTool: deferred OpenSeadragon init with requestAnimationFrame (React 18 async render) - CurtainTool: curtainToolBar justify-content flex-end (icons at bottom) Security: - TopBar StatusIndicator: escape HTML in layer names to prevent XSS via addLayerQueue Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Fix mapToolBar pointer events, login padding, default tool, About modal order 1. mapToolBar: set pointer-events:none on both #mapToolBar and direct children so clicks pass through to the map; leaf elements still get auto via .childpointerevents rule 2. #loginModalBody: padding changed to 40px 0px 0px 3. Default tool: deferred click to requestAnimationFrame so React toolbar has rendered before getElementById runs 4. About modal: moved mainInfoModalCustom to right below mainInfoModalHero Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Fix tool headers to 40px, ViewshedTool subheader, AnimationTool header, InfoTool close btn, statusIndicator position 1. All tool panel headers: changed from 44px to exactly 40px height - Global .mmgisToolHeader and .mmgisToolTitle in mmgis.css - InfoTool.css, ViewshedTool.css, IsochroneTool.css, ShadeTool.css 2. LayersTool #filterLayers: height 40px, .right > div height unset, .right margin-right 30px, .right > div margin 0px 3px 3. ViewshedTool: restructured header — title+help in mmgisToolHeader row, vstToggleAll (left) and vstNew (right) on a new #vstSubHeader row below 4. AnimationTool: removed old #animationToolHeader CSS (padding 15px 20px, white background, 18px font), now uses standard mmgisToolHeader class. Fixed color from var(--color-a) (background) to var(--color-f) (text) 5. InfoTool close X: re-inject close button after use() rebuilds content via TC_.injectCloseButton() (toolsContainer.empty() was removing it) 6. StatusIndicator: moved to left of topBarTitle in JSX render order. Added align-items:center to #topBarMain for vertical alignment. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Remove title attr from StatusIndicator (conflicts with tippy tooltip) Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Fix TimeUI dropdown z-index above tool panel, reposition toasts to top-center 1. TimeUI dropdowns: added z-index 10000 to all dropy content ul elements so they render above the vertical tool panel (z-index 1400). Also set timeUIDock to position:relative with z-index 10000 and overflow:visible. 2. Toast notifications: repositioned #toast-container from bottom-right to top-center just below the topbar (top: 44px, centered with transform). Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Fix StatusIndicator spacing, CurtainTool close btn, header title font consistency 1. StatusIndicator: use display:none/flex instead of opacity:0/1 so it takes no space when there's no active status indicator. 2. CurtainTool: added close X button at top of curtainToolBar (matching MeasureTool pattern) with flex spacer pushing other buttons to bottom. 3. Header title font consistency: InfoTool, ShadeTool, CurtainTool titles now match mmgisToolTitle standard (font-weight:600, padding-left:10px, height:40px for CurtainTool which was 34px). Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Legend empty state, scalefactor position, sep-tool-header unbold 1. Legend: show 'No active layers with legends' when no legend items are present. Also fixed container height calc(100% - 40px). 2. Mapping Scale (.leaflet-control-scalefactor): shifted 10px right (left 26→36px) and 1px down (bottom 30→29px). 3. sep-tool-header span: font-weight changed from 600 to 400. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Guard Legend empty state message to only show when panel is active Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Fix TimeUI dropdown covered by toolPanel: remove splitscreens stacking context #splitscreens had z-index:1 which created a stacking context, confining its children (including bottomFloatingBar at z-index:1500) to that context. Since ToolPanel (z-index:1400) was a sibling outside splitscreens, it painted above all splitscreens children regardless of their internal z-index values. Fix: change #splitscreens z-index from 1 to auto so it no longer creates a stacking context. Now bottomFloatingBar (1500) participates in the same stacking context as ToolPanel (1400), and 1500 > 1400 means the TimeUI dropdown correctly paints above the tool panel. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Migrate ~69 CursorInfo toast-like calls to proper Toast component - Replace CursorInfo.update() toast-like calls with Toast.info/success/warning/error - Preserve message colors: blue→info, green→success, yellow→warning, red→error - Keep 12 legitimate cursor-following CursorInfo calls unchanged - Files migrated: DrawTool.js, DrawTool_Files.js, DrawTool_FileModal.js, DrawTool_Templater.js, DrawTool_SetOperations.js, DrawTool_Drawing.js, DrawTool_Editing.js, DrawTool_Shapes.js, LayersTool.js, ShadeTool.js, chemistrychart.js - Fix Devin Review: Change misleading 'Bad token' to 'Login failed' in users.js - Normalize line endings (CRLF→LF) in affected files Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Fix Toast.js missing from git, CoordinatesDiv z-index, Legend duplicate ID, topBar padding - Add Toast.js to version control (was untracked, causing webpack module error) - Bump CoordinatesDiv z-index from 20 to 1001 (was hidden behind splitscreens children after z-index:auto change) - Fix Legend duplicate ID: separated tool icon was #LegendTool, same as content container div, causing empty message to appear in button instead of panel - Add hasStatus class to #topBarMain when statusIndicator is active, setting #topBarTitleName padding-left to 0 Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Fix Legend empty message: scope selector to content container via targetId Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Move Toast.js to design-system, fix --color-a3 text contrast, update AGENTS.md - Move Toast.js from UserInterface_/components/Toast/ to design-system/components/Toast/ (generic component belongs in design-system, not MMGIS-specific UserInterface_) - Update all 11 Toast import paths to new location - Bump --color-a3 in 5 dark themes to pass WCAG AA 4.5:1 contrast for text: Dark Default #747c81→#81888d, Dark Blue #64748b→#738399, Dark Warm #8b7a5e→#908064, Dark Mars #8a6a60→#98796f, Dark Midnight #606088→#7a7a9e - Update AGENTS.md: document design-system/ vs UserInterface_/ distinction in project structure and Key Directories Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Fix topBarTitleName padding override: increase specificity and add !important Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Fix IdentifierTool deactivation: update icon ID reference in separateFromMMWebGIS Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Hide toolPanelDrag when no tool is open - Add setToolPanelDragVisible(false) to closeActiveTool() (was only in makeTool toggle-off path) - Also guard drag handle display on isOpen (toolPanelWidth > 0) as safety net Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Add hover effect to MMGIS logo (opacity + brightness transition) Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Fix MMGIS logo hover: keep full opacity, use subtle background highlight instead Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Selective tile fade: fade on pan/zoom, instant on refresh/reload - Remove blanket _fadeAnimated toggle from toggleTimeUI (was killing fade for all tiles while TimeUI was open) - Monkey-patch GridLayer.redraw, TileLayer.setUrl, and GridLayer._tileReady to suppress fade via a transient _suppressTileFade map flag - Set _suppressTileFade in reloadTimeLayers for time-driven reloads - Flag auto-clears after 300ms so pan/zoom tiles still get the nice fade - Install pbf dependency (required by CesiumMVTLayer from #942 merge) Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Per-layer fade control: time-enabled + shade/viewshed layers never fade - Replace transient map-level _suppressTileFade with per-layer _noFade flag - Patch GridLayer._tileReady to check _noFade on the layer instance - Set _noFade on time-enabled tile layers and data/GL layers at creation - Set _noFade on Shade and Viewshed tool GL layers - Non-time-enabled base imagery tiles still fade normally on pan/zoom Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * fix: mobile UI improvements — move hamburger to right menu, show panel toggles, isMobile-driven toolbar layout, desktop-matching scalebar/compass - Remove left hamburger menu (#topBarMenu), move BottomBar items into top-right kebab dropdown menu for both mobile and desktop - Show panel toggles (Viewer/Map/Globe) and account/login UI in mobile topbar's #topBarRight - Move toolbar horizontal layout CSS from @media breakpoints to UserInterfaceMobile_.css (loaded only when isMobile flag is true) - Remove #mapTopBar @media rule from mmgis.css, add to mobile CSS - Remove mobile-only simplified scalebar rendering; use full desktop scalebar with both large and small axes on all viewports - Remove display:none on .leaflet-control-scalefactor in mobile CSS - Remove #loginDiv display:none from mobile CSS (React overlay handles it) - Simplify BottomBarReact container styles (no more absolute positioning) Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * chore: bump version to 4.3.28-20260430 [version bump] * fix: mobile toolbar 40px height, bottomFloatingBar flush, timeUI toggle, scalebar position, hide hotkeys - Toolbar height 40px, toolButton width 40px, no border-bottom - toolcontroller_incdiv: no padding-bottom, overflow-y hidden - bottomFloatingBar: no border-radius, left/right/bottom = 0 - Add MobileTimeUIToggle button on far right of toolbar - Hide Keyboard Shortcuts from kebab menu on mobile - Fix scalebar positioning (remove top:48px override in UserInterfaceBridge) - Set mobileTopSize/topSize to 40 (splitscreens top = 40px, not 50px) Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * fix: mobile topBar padding-left 34px Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * fix: update MobileCoordButton topBar paddingLeft from 80px to 34px Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * fix: MobileTimeUIToggle — inline toggle logic, float right, hide from settings on mobile - Replace broken Coordinates.toggleTimeUI() call with direct jQuery/store toggle - Float time button right in toolbar - Hide Time UI toggle from settings modal on mobile (toolbar has it) Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * fix: push scalebar/compass/scale up 40px on mobile, keep #timeUI in DOM - BottomElementPositioner: position mapToolBar, leaflet-bottom-left/right 40px above bottom on mobile (above toolbar) - Stop removing #timeUI from DOM on mobile so MobileTimeUIToggle works Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * fix: mobile TimeUI — only show endtime, always expanded - Hide #mmgisTimeUIStartWrapper and StartWrapperFake on mobile via CSS - Force expanded state (addClass expanded + show) when toggling TimeUI on - CSS ensures #timeUI.active always shows expanded content on mobile Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * fix: mobile TimeUI opens in tool panel with header, end time, expanded rows - MobileTimeUIToggle now opens/closes the tool panel via ToolController_ - Closes any active tool before showing TimeUI - Forces expanded state when opening - CSS hides start time inputs, positions expanded content properly - Overrides absolute positioning of expanded content for tool panel flow Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * feat: rewrite separated tools system from jQuery to React components - Add separatedToolsList/activeSeparatedTools state to Zustand uiStore - Rewrite SeparatedTools.jsx with glassmorphism panels, CSS Module styling - Replace SepToolsContainer (setInterval hack) with SepToolButton/SepToolsSection - Remove ~170 lines of jQuery DOM construction from ToolController_.js - Fix hardcoded rgba(26,26,27,0.88) to theme-aware var(--color-a-rgb) - Remove separated tool entries from themeApplier.js - Remove separated tool overrides from FloatingElements.css - Move Legend CSS overrides from Toolbar.module.css to SeparatedTools.module.css - Remove jQuery active-state manipulation from IdentifierTool.js - Add store sync in Map_.js displayOnStart logic - Preserve all DOM IDs for backward compatibility (mmgisAPI, tool make()) Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * chore: bump version to 4.3.28-20260501 [version bump] * fix: TimeUI mobile checks — use Zustand store instead of L_.UserInterface_ L_.UserInterface_ is null when TimeUI.init() runs (TimeControl.init is called before L_.link sets UserInterface_). All 16 isMobile checks now read from useUIStore.getState().isMobile which is set at startup. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * chore: bump version to 4.3.29-20260501 [version bump] * fix: move displayOnStart logic from Map_.js to ToolController_.finalizeTools() - Map_ no longer references specific tools (LegendTool) - displayOnStart is now handled generically for all separated tools - Added DOM element polling (tryMake) to handle React render timing Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * revert: remove all TimeUI-related mobile changes Reverts TimeUI.js and BottomBar.js to development base. Restores #timeUI DOM removal in UserInterfaceBridge.fina(). Removes MobileTimeUIToggle component from Toolbar.jsx. Removes TimeUI mobile CSS overrides from UserInterfaceMobile_.css. Non-TimeUI refinements (toolbar height, scalebar positioning, etc.) preserved. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * simplify: remove DOM polling, use simple setTimeout(0) for auto-open LegendTool handles its own content lifecycle via subscribeOnLayerToggle. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * feat: mobile TimeUI — fix isMobile detection, staging container, toolbar toggle - TimeUI.js: import useUIStore and replace all 16 L_.UserInterface_?.isMobile checks with useUIStore.getState().isMobile (L_.UserInterface_ is null when TimeUI.init() runs, so mobile conditionals were dead code) - TimeUI.js: stage mobile #timeUI in hidden #timeUIMobileStaging instead of placing directly in #tools (which gets cleared by other tools) - UserInterfaceBridge.js: stop removing #timeUI from DOM on mobile - Toolbar.jsx: add MobileTimeUIToggle that moves #timeUI between staging and #tools, opens/closes tool panel via ToolController_ - BottomBar.js: hide TimeUI toggle from settings modal on mobile Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * fix: rescue #timeUI back to staging when another tool opens Subscribe to activeToolName changes — when a tool becomes active while TimeUI is showing, move #timeUI back to #timeUIMobileStaging before the new tool's make() clears #tools. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * chore: remove separatedTool/justification config toggles, fix review issues - Remove separatedTool checkbox and justification dropdown from Legend and Identifier config.json (these are always separated, not configurable) - Remove justification property/code from LegendTool.js, IdentifierTool.js - Simplify Globe_.js separated tool count (no justification filter) - Remove justification from Reference-Mission config blueprint - Update LegendTool help docs and Legend.md documentation - Add --color-a-rgb fallback (29,31,32) in SeparatedTools.module.css - Add display:none !important to .panelIdentifier to prevent 12px gap - Update e2e test comment Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * fix: circular import in TimeUI.js, toolbar/bottomFloatingBar position sync - TimeUI.js: replace top-level useUIStore import with lazy _getUIStore() accessor to avoid 'Cannot access useUIStore before initialization' circular import error at _remakeTimeSlider - SplitScreens.jsx: skip #timeUI reparenting observer on mobile (mobile uses MobileTimeUIToggle to manage #timeUI placement in #tools) - BottomElementPositioner.jsx: unify mobile transition to 0.3s (matches toolsWrapper and toolbar), guard pxIsTools against undefined - Toolbar.jsx: align toolbar transition to 0.3s ease-out Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * LegendTool fix empty message * chore: remove separated tools offset logic from Globe_.js Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * fix: skip _makeHistogram on mobile (no timeline slider, timestamps unset) _makeHistogram renders inside the timeline slider which doesn't exist on mobile. Without it, _timelineStartTimestamp is NaN, causing 'Invalid time value' RangeError at toISOString(). Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * fix: mobile TimeUI — populate expanded rows, fix Invalid date, fix panel height - TimeUI.js attachEvents: use _initialStart/_initialEnd on mobile (same as desktop) instead of L_.TimeControl_ which isn't set yet at init time. Fixes 'Invalid date' in start/end time inputs. - TimeUI.js fina: set expanded=true on mobile and call _populateExpandedRows() so year/month/day/hour rows actually render. Removed position:absolute and pointer-events:none overrides. - Toolbar.jsx: set tool panel height to 217px (TimeUI.height) instead of 45% viewport — matches actual TimeUI content height. - UserInterfaceMobile_.css: expanded content flows naturally (position:relative), hide start time inputs, allow overflow scroll, flex-wrap topbar. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * fix: mobile TimeUI justify-content center, restore toolbar border-bottom - Add justify-content: center to #mmgisTimeUIMain on mobile - Remove border-bottom: none override so toolbar keeps its default border Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * fix: mobile TimeUI overflow hidden, scalebar/compass fixed at 40px offset - #timeUI overflow-y: hidden (was auto, causing 2px scroll) - Scalebar/compass/map controls stay at fixed 40px offset (above toolbar) regardless of tool panel state — no longer shift up by pxIsTools Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Implement multi-tier knowledge architecture - Restructure AGENTS.md from 745 lines to 106 lines (Tier 1: essential context) - Create knowledge/ directory with 30+ wiki-style documentation files (Tier 2: deep knowledge) - Create knowledge/reference/ with 8 detailed reference files (Tier 3: lookup material) - Move AI-GETTING-STARTED.md and AI-DEVELOPMENT.md to knowledge/ - Update all file references in .specify/templates and blueprints - Create knowledge/README.md as the full knowledge base index - Create knowledge/reference/README.md as reference material index Three-tier knowledge discovery system: Tier 1: AGENTS.md (~106 lines) - scannable in <2 minutes Tier 2: knowledge/*.md - deep knowledge on architecture, tools, APIs, DB, infra Tier 3: knowledge/reference/*.md - coding conventions, API reference, troubleshooting Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * chore: bump version to 4.3.29-20260501 [version bump] * fix: mobile toolbar active button style matches desktop, fix icon alignment - All mobile toolbar buttons (ToolButton, MobileCoordButton, MobileTimeUIToggle) now use display:flex with align-items/justify-content center for proper vertical icon centering - MobileCoordButton: changed 'active' class to 'toolButtonActive' to match the global CSS active style (color-mmgis + color-i background) - Removed inline color overrides so CSS .toolButtonActive takes effect Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Add Devin knowledge notes from past MMGIS sessions Include curated lessons learned from past Devin sessions: - CI/CD: ignore build-arm64/amd64 failures, focus on required checks - Child sessions: no separate PRs when consolidating - ENV triple-update rule (.env, sample.env, ENVs.md) - Error handling: use logger with infrastructure_error for fatal startup errors - Path traversal security: stay within /Missions, handle subpath serving - Database initialization architecture and migration patterns - API authentication behavior across AUTH modes - Auto-generated MMGIS concept index Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * fix: mobile toolbar active button style, icon alignment, tool deactivation - Active toolbar buttons get desktop-matching margin (1px 0) and border-radius (8px) via .toolButton.toolButtonActive CSS rule - Removed line-height: 40px from .toolButton (flex centering handles vertical alignment, line-height was pushing icons up) - MobileCoordButton now watches activeToolName store and deactivates when another tool opens (fixes coords staying active) - MobileTimeUIToggle sets activeToolName='MobileTimeUI' when opening so coords/other buttons can detect it and deactivate - MobileTimeUIToggle clears activeToolName when closing - Both custom buttons skip self-deactivation via name check Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Fix cross-references: convert backtick refs to markdown links, add Devin knowledge notes Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * fix: mobile toolbar icon height 40px, button margins for active padding - #toolbar .toolButton i: height 40px fixes icon vertical alignment - #toolbar .toolButton: margin 0 2px gives spacing between buttons - #toolbar .toolButton.toolButtonActive: margin 1px 2px so active background has visual padding around the icon Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Rename knowledge/ to .knowledge/ for consistency with .specify/ convention Dot-prefix signals agent infrastructure (not source code), consistent with .specify/, .github/, .vscode/ conventions. All cross-references updated. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * fix: mobile toolbar icon line-height 40px, active button padding via height - Coord and TimeUI button <i> icons get line-height: 40px - Active buttons: height 34px (vs 40px toolbar) creates visual padding around the active background, centered by flex align-items - Buttons get margin: 0 1px for horizontal spacing Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Fix broken cross-reference: 06.2 -> 06.1-configure-rest-api.md Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * fix: close active tool + cancel deferred cleanup in MobileCoordButton/TimeUI - MobileCoordButton: call closeActiveTool() before opening, destroy _pendingCloseTool if set, increment _closeSeq to cancel deferred tools.innerHTML clear - MobileTimeUIToggle: same _pendingCloseTool + _closeSeq fix after closeActiveTool() to prevent 420ms deferred cleanup from wiping #timeUI after it's placed in #tools - Removed redundant closeActiveTool() from MobileCoordButton close path (was being called after destroy, not needed) Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * fix: active mobile toolbar buttons 34x34px (square) Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Drastically compress .knowledge/ — keep only unique agent content Remove 33 wiki files that duplicate docs/pages/ content. Remove 9 reference/ files derivable from source code. Keep only 5 files (down from 46): - AI-GETTING-STARTED.md (agent setup walkthrough) - AI-DEVELOPMENT.md (spec-kit workflow) - conventions-and-gotchas.md (naming, code style, common issues) - 12-devin-knowledge-notes.md (CI, auth, DB init, security gotchas) - README.md (index pointing to docs/pages/ for everything else) Principle: don't duplicate docs/ — only keep what's uniquely agent-optimized. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Rename to knowledge-notes.md, remove Devin branding and fork-specific CI section Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * fix: hide mmgis-map-logo on mobile Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Restore Database Safety Rules for AI Agents section in AGENTS.md Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * fix: shift compass and map scale 6px to the right (both mobile and desktop) Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Add back Important Instructions, code pattern templates, and detailed project structure - Important Instructions in AGENTS.md: MCP tools, hot-reload, Reference Mission - .knowledge/code-patterns.md: full directory tree with key directory annotations, plus copy-paste templates for Express routes, Sequelize models, Tool plugins, and WebSocket handlers Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Update project structure trees to reflect current filesystem Add missing directories: tests/, .knowledge/, .specify/, .github/, views/, private/, spice/, build/, examples/, scripts/middleware.js. Both abbreviated (AGENTS.md) and detailed (.knowledge/code-patterns.md) trees now match the actual repo layout. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * chore: bump version to 4.3.30-20260501 [version bump] * Add Layers_.js to project structure (key singleton L_) Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Fix project structure: correct API layout, frontend modules, code templates API/Backend/ uses feature-domain modules (Draw/, Users/, Config/, etc.) with setup.js + routes/ + models/ per feature — not APIs/ or Databases/. Frontend essence/ has Components/, Helpers/, LandingPage/, mmgisAPI/, services/ — not Ancillary/. Basics/ includes all singletons (Globe_, Formulae_, ToolController_, Viewer_, ComponentController_, Test_). Code templates updated to match actual patterns (setup.js, module.exports). Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * refactor: remove test infrastructure (Test_ module, testModules, DrawTool.test) - Delete src/essence/Basics/Test_/ directory - Delete src/essence/Tools/Draw/DrawTool.test.js - Remove Test_ import and Shift+T keydown handler from essence.js - Remove tests key from Draw tool config.json - Remove testModules generation logic from API/updateTools.js Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * chore: bump version to 4.3.31-20260501 [version bump] * style: move Cesium link button to top-right and match Leaflet zoom button styling - Change control container from top-left to top-right positioning - Update button size from 26px to 30px to match Leaflet zoom controls - Use CSS variables (--color-a, --color-f, --color-mmgis) instead of hardcoded colors - Add border-radius and box-shadow matching Leaflet control appearance - Update hover/inactive states to use themed colors Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * fix: anchor map logo to viewport instead of Leaflet map panel - Change MapLogo parent from .leaflet-bottom.leaflet-right to #main-container - Switch CSS position from absolute to fixed for viewport anchoring - Add explicit bottom-offset positioning in BottomElementPositioner (desktop) - Add explicit bottom-offset positioning in BottomElementPositioner (mobile) - Logo stays at viewport right edge regardless of open side panels - Retains smooth bottom offset transitions when bottom bar appears Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * docs: remove references to deleted test infrastructure (Test_, DrawTool.test) - Remove Test_/ from project structure in .knowledge/code-patterns.md - Remove DrawTool.test.js references from specs/006 spec, plan, and tasks - Remove Draw Tool Testing section from tasks.md Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * chore: bump version to 4.3.32-20260501 [version bump] * fix: append logo to document.body to avoid filter containing block #main-container has a CSS filter property which creates a new containing block per the CSS spec, causing position:fixed to behave like absolute. Appending to document.body ensures true viewport-fixed positioning. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * fix: prevent mobile topBarTitleName text wrapping by replacing max-width with white-space: nowrap Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * chore: bump version to 4.3.33-20260501 [version bump] * chore: bump version to 5.0.0 and update changelog Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * refactor(ui): move Screenshot/Fullscreen to BottomBar, About to TopBar kebab TopBar kebab menu now contains only Keyboard Shortcuts, Settings, and About (About now shows on both desktop and mobile). BottomBarReact now renders Screenshot, Fullscreen, and Copy Link buttons (top to bottom) following the same IconButton + Tooltip pattern. The About button has been removed from BottomBarReact. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * chore: bump version to 5.0.1-20260505 [version bump] * feat(mobile): enforce exclusive panel toggling on mobile in TopBar Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * chore: bump version to 5.0.1-20260505 [version bump] * style: reposition LithoSphere globe controls to match Leaflet/Cesium theme Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * chore: bump version to 5.0.1-20260505 [version bump] * feat(topbar): hide Viewer/Globe toggles based on configured panels Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * style(bottombar): reorder buttons (Copy Link, Screenshot, Fullscreen) and unify size Reorder the BottomBarReact buttons…
* Improve Base UI adoption and reduce :global() overuse
- TopBar: use IconButton for sign-in/kebab buttons, use Dropdown for user card
popup (replaces hand-rolled outside-click + manual popup with Base UI Menu
for keyboard nav, focus trap, accessibility)
- BottomBarReact: use IconButton + Tooltip instead of raw <i> + tippy.js
- Toolbar: use Tooltip instead of tippy.js for tool button tooltips
- Reduce :global() from 33 to 22 instances — all remaining are justified by
external jQuery/imperative code references:
- SplitScreens: convert #viewerScreen/#globeScreen to scoped classes
- Toolbar: convert #toolcontroller_incdiv to scoped class
- Splitter: convert .splitterV to scoped class
- UserInterfaceLayout: consolidate TopBar styles into TopBar.module.css
- BottomBarReact: fully scoped (IconButton handles all button styling)
Design system usage: 5 of 6 wrappers now imported across 3 files
(Toggle, Dropdown, IconButton, Tooltip — only Button/Modal unused,
which is appropriate since there are no standalone button/modal cases
in the main site shell).
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* Fix: remove stale setShowUserCard call in handleLogout
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* Migrate Ancillary UI components from jQuery to React/native DOM
- Modal.js: React-based with imperative API bridge (Modal.set/remove)
so existing jQuery callers (BottomBar, DrawTool, AnimationTool, etc.)
work without changes. Uses React state + createRoot for rendering.
- ConfirmationModal.js: Removed jQuery dependency, uses native DOM
event listeners with the React Modal backend.
- Help.js: Removed jQuery dependency, uses native fetch() + DOM
event listeners instead of $.get() and $().on().
- ContextMenu.js: Removed jQuery dependency entirely. Uses native
DOM APIs (createElement, addEventListener, querySelectorAll) for
building and managing the right-click context menu.
- Compass.js: Removed jQuery dependency. Uses native DOM APIs
(getElementById, querySelector) for compass element creation
and bearing updates.
- MapLogo.js: Removed jQuery dependency. Uses native DOM APIs
for logo element creation and Leaflet container insertion.
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* Migrate Ancillary components to true React + Base UI
- Modal.js: React component using Base UI Dialog with shared state store,
imperative Modal.set()/remove() API preserved for backwards compatibility.
Accepts both HTML strings (legacy callers) and React elements (new callers).
- ConfirmationModal.js: React JSX component using design-system Button for
Yes/No actions, rendered through Modal service. Same prompt() API.
- Help.js: React JSX component using native fetch + showdown for markdown
rendering, rendered through Modal service. Same getComponent/finalize API.
- ContextMenu.js: Full React component with createRoot, JSX menu items with
proper event handlers. Same init()/remove() API.
- Compass.js: React component rendered via createRoot into Leaflet
bottom-left container. SVG compass with bearing rotation on map events.
- MapLogo.js: React component rendered via createRoot into Leaflet
bottom-right container. Configurable size and link support.
All components use CSS Modules. Old plain CSS files removed.
Also fixes:
- About modal now respects look.help/look.info config flags
- Cleaned up dead #toggleTimeUI references in Coordinates.js
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* Fix modal, tooltip, measure, and context menu bugs
- Modal: Fix About modal not opening due to async createRoot in React 18.
ModalHost now initializes from shared state and syncs on mount.
Blur management moved to ModalHost (React state-driven), fixing
persistent blur after modal close.
- Tooltip/Dropdown: Use render prop on Trigger to avoid nesting
<button> inside <button> (IconButton is already a button element).
- MeasureTool: Migrate from deprecated ReactDOM.render to createRoot.
Register Chart.js scales via Chart.register(...registerables) to fix
'linear is not a registered scale' error with react-chartjs-2 v4.
- ContextMenu: Fix crash when right-clicking on LithoSphere scene
(native events lack originalEvent). Store element and handler refs
for proper cleanup, preventing listener leaks.
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* Fix Modal: remove close button, fix blur persistence, add fade animation
- Remove the Dialog.Close button (curved bottom-left border-radius)
- Drop Base UI Dialog wrapper entirely — it was fighting with the
imperative Modal.set/remove API. Now uses simple divs with CSS
transitions, matching the original jQuery modal behavior exactly.
- Blur management is now purely imperative via _applyBlur() called
synchronously in set() and remove(). Removed async useEffect approach.
- Add 500ms CSS opacity fade-in/fade-out transition matching original.
- Closing state: Modal.remove() marks modal as closing (triggers
opacity 0 transition), then removes from DOM after 500ms.
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* Rename Ancillary React components to .jsx, fix modal blur via Zustand store, fix IconButton forwardRef, add Help.finalize calls
- Rename Modal, ConfirmationModal, Help, ContextMenu, Compass, MapLogo from .js to .jsx
- Route modal blur through Zustand modalBlurCount instead of imperative DOM manipulation
- Remove conflicting jQuery blur animation in Layers_.js
- Wrap IconButton with React.forwardRef to fix Tooltip/Menu trigger warnings
- Add Help.finalize() calls to ChemistryTool, DrawTool, IsochroneTool
- Change aboutModalContent config type from 'markdown' to 'textarea'
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* Revert aboutModalContent type back to 'markdown' — Maker.js already handles it
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* Fix Help.jsx: check res.ok on fetch, sanitize HTML with DOMPurify
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* Fix ContextMenu WKT null guard, fix Modal blur timing during fade-out
- Add null check for feature in handleActionClick WKT placeholder handling
- Delay blur removal until after 500ms fade-out completes (blur stays in sync with backdrop opacity)
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* Reorganize folder structure: dissolve Ancillary, nest components
- Dissolve src/essence/Ancillary/ entirely
- UI components → UserInterface_/components/ (Modal, ConfirmationModal, Help,
ContextMenu, Compass, MapLogo, CursorInfo, Attributions, Login)
- Layout chrome → UserInterface_/components/ (Description, Coordinates, Search,
ScaleBar, ScaleBox)
- Pure services → essence/services/ (DataShaders, LocalFilterer, QueryURL, Sprites)
- Stylize.js → design-system/ (theme bridge alongside themeApplier)
- Delete unused Swap.js
- Nest all components into own folders (ComponentName/ComponentName.ext pattern):
- UserInterface_/components/: TopBar/, Toolbar/, ToolPanel/, SplitScreens/,
Splitter/, BottomBar/, BottomElementPositioner/, Layout/, Panels/
- design-system/components/: Button/, IconButton/, Dropdown/, Toggle/, Modal/,
Tooltip/
- Update ~70+ import paths across codebase
- Build verified locally
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* Fix Modal.set() race condition and themeApplier CSS variable override issue
- Modal.set() onAddCallback: Replace 50ms setTimeout with MutationObserver
that waits for the modal element to appear in DOM before firing callback.
Prevents silent jQuery binding failures on slower devices.
- themeApplier: Use Proxy to read computed CSS custom properties (set by
Stylize.js per-mission overrides) instead of hardcoded theme object values.
Stylize.js now calls refreshThemeDOM() after setting CSS variables so
inline styles reflect mission-specific color overrides.
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* Sync toggleTimeUI DOM state to Zustand store
toggleTimeUI() now calls setTimeUIActive() and setTimeUIExpanded()
so BottomFloatingBar visibility and BottomElementPositioner offsets
reflect actual TimeUI state.
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* Restore #toggleTimeUI element in Coordinates markup
The element was accidentally dropped during the folder restructure move.
TimeUI.js and DrawTool.js check $('#toggleTimeUI').hasClass('active')
to gate histogram rendering and time-filter toggle visibility.
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* Fix #toggleTimeUI: restore click handler, tippy, active class; remove redundant jQuery positioning
Restored pieces lost during folder restructure:
- Click handler in init() and off handler in remove()
- Tippy tooltip for the time toggle button
- display:none when time is not enabled
- $('#toggleTimeUI').toggleClass('active') so TimeUI.js can check it
- $('#CoordinatesDiv > #toggleTimeUI').remove() on mobile
Removed jQuery CSS positioning from toggleTimeUI() since
BottomElementPositioner now reactively handles all bottom-anchored
element offsets via the Zustand store.
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* Fix CurtainTool.destroy() using undefined ReactDOM.unmountComponentAtNode
Use the stored _reactRoot.unmount() instead, matching the React 18
createRoot pattern already used in make().
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* Fix tooltips, scale indicator position, modal blur, help close button
Tooltips:
- Reduce Base UI Tooltip delay from 600ms (default) to 200ms
- Restyle tooltip popup to match tippy blue theme (var(--color-c2))
- Add Tooltip wrappers to TopBar panel toggles (Viewer/Map/Globe)
- Wrap Toggle with forwardRef so Tooltip render prop can attach ref
- Remove title attrs that conflicted with custom tooltips
Scale indicator:
- Remove scalefactor-specific positioning from BottomElementPositioner
(it moves naturally with .leaflet-bottom.leaflet-left container)
- Position scalefactor to the left of compass at same bottom level
Modal blur:
- Call _applyBlur() immediately when marking modal as closing
so blur clears during fade-out instead of persisting 500ms
Help modal:
- Add close (X) button in title bar matching other modal patterns
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* Remove dead CSS: delete tools.css, clean ~600 lines from mmgisUI.css and mmgis.css
- Delete tools.css entirely (both selectors #CurtainToolList and
.searchToolSelect are unreferenced anywhere in the codebase)
- Remove from mmgisUI.css: .mmgisRadioBar3/4/Vertical (140 lines),
.mmgispureselect (104 lines), blink/condemned_blink_effect (38 lines),
.slidecontainer/.slider (41 lines), .ar_slider (91 lines),
.verticalSlider (91 lines), .mmgisMultirange_elev (19 lines),
.ui-corner-all/bottom/right/br (9 lines)
- Remove from mmgis.css: #nodeenv, empty #topBar{}, #topBarInfo,
#topBarHelp, #topBarFullscreen, #toggleUI, #logoGoBack
- Keep #topBarLink (used in BottomBarReact.jsx), #webgl-error-message
(used by vendored THREE.js)
- All selectors verified with repo-wide grep before removal
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* UI fixes: tooltips, splitter hover, mobile toolbar, color schemes
- Replace Base UI Tooltip with simple React portal tooltip (200ms delay,
tippy-matching style) — fixes missing tooltips for toolbar/topbar/bottom buttons
- Add cursor + hover highlight to vertical splitters (was missing because
module CSS didn't inherit global .splitterV styles)
- Add hover highlight to tool panel drag handle
- Remove mdi-drag-vertical icon from tool panel drag
- Add mobile toolbar horizontal layout via @media query overrides
- Add 4 new color schemes: High Contrast (a11y), Dark Mars, Dark Midnight,
Light Warm (total: 10 themes)
- Previous fixes also included in working tree:
- timeUI border moved to toolsWrapper border-bottom (conditional)
- #toggleTimeUI button removed entirely
- CoordinatesDiv: vertical centering, unified background, 12px right offset
- barBottom padding-bottom: 8px
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* Fix assignment operator used instead of comparison in TimeControl.fina()
Pre-existing bug: `TimeControl.enabled = true` was assigning instead of
comparing. Changed to `TimeControl.enabled === true`.
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* Restructure Configure page UI tab: add all themes, Custom mode, enableWhenField
- Added all 10 theme presets to dropdown (was missing Dark Mars, Dark Midnight,
Light Warm, High Contrast)
- Added 'Custom' option: skips preset theme, uses only color picker values
- Moved Theming section directly under Rebranding
- Nested 'Custom Color Options' under Theming with subdescription
- Added enableWhenField support to Maker.js: disables color pickers unless
theme is set to Custom
- Renamed color options with clearer names and improved descriptions:
Primary → Surface Color, Secondary → Deep Background Color,
Tertiary → Text Color, Body → Page Body Color, Highlight → Feature Highlight
- Stylize.js: skip setTheme() when theme is 'Custom'
- Rebuilt configure page
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* Revert tooltips to tippy.js, fix dropdown menus, redesign About modal
1. Tooltip: Replaced custom React portal tooltip with tippy.js wrapper.
Uses the existing tippy.js dependency and 'blue' theme for consistency.
2. Dropdown: Replaced Base UI Menu with native portal dropdown.
Base UI's nested Menu.Trigger + BaseButton composition was swallowing
click events, breaking userAvatar and menuBtn menus. New implementation
uses simple state + createPortal with proper outside-click dismissal.
3. About modal: Professional redesign with centered MMGIS ASCII art header,
proper GitHub SVG logo link, clean metadata section, centered link
buttons, attributions section, and NASA-AMMOS footer.
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* Restore .mmgisHelpButton base styles lost during Help.css module migration
The global .mmgisHelpButton styles (yellow color, compact 18x18px sizing,
0.7 opacity) were removed when Help.css was converted to Help.module.css.
Since Help.getComponent() emits raw HTML strings for jQuery-rendered tool
headers, it cannot use CSS Module scoped classes. Restored the base styles
in mmgis.css alongside the related .mmgisToolHelpBtn definition.
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* Fix session logout regression, About modal refinements, High Contrast theme, Stylize.js, Default Tool config
- Login: skip session.regenerate() for token-based re-auth (useToken:true)
so reloading the main page no longer invalidates the configure page session
- About modal: replace ASCII art with mmgis.png logo, rename Attributions to
Map Layer Attributions, remove footer logo, link NASA-AMMOS to ammos.nasa.gov
- High Contrast theme: change accent from #ffff00 to #ffd700 (gold) for better
contrast ratios against dark backgrounds
- Stylize.js: color overrides only apply when theme is Custom or unset,
preventing preset themes from being clobbered by stale config values
- Restore Default Tool config section in tab-ui-config.json (accidentally
removed during Theming section reorganization)
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* Restore defaulttooldropdown case handler in Maker.js
The case was accidentally removed during the Configure page UI tab
restructure (d7f96c50). Without it, the Default Tool dropdown in the
Configure page rendered as nothing despite the config referencing it.
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* High Contrast tooltip text, panel toggle styling, scale position swap
- High Contrast theme: tooltips now use black text on yellow background
via --color-c2-text variable (white for all other themes)
- About modal links use var(--color-f) for consistent theme text color
- Panel toggle buttons: 11px uppercase with 600 weight for better
visibility
- Mapping scale button moved to bottom-right of compass (was top-left)
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* Reposition Viewer and Globe panel buttons to top-right
- Viewer: dropdown selector at top-right edge, OSD buttons stacked
vertically below it; settings panel opens to the left
- Globe: home, exaggerate, observe, walk, link controls moved from
TopLeft to TopRight corner via addControl 4th arg
- Style consistency: OSD buttons and LithoSphere controls now match
Leaflet zoom controls (var(--color-a) bg, var(--color-f) text,
var(--color-mmgis) hover, 30px size, 3px border-radius)
- Viewer settings sliders use var(--color-a3) instead of hardcoded
#444444
- Az/el indicator stays at bottom center (exception per design)
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* Consistent modal theming + session security fix
Modal theming:
- All modals now share consistent styling: backdrop-filter blur, semi-transparent
background via --color-a-rgb, 10px border-radius, header divider line, box-shadow
- Updated: loginModal, Help, ConfirmationModal, Settings, Hotkeys, About modals
- Tool panel backgrounds changed from opaque var(--color-k) to transparent so the
ToolPanel's existing backdrop-filter effect shows through
- Legend tool header updated to match consistent 44px height with divider
- applyTheme.js now auto-derives --color-a-rgb from theme's --color-a hex value
- Modal service wrapper gets backdrop-filter: blur(12px)
Session security (Devin Review fix):
- Token re-auth now calls req.session.regenerate() with data preservation to
prevent session fixation while maintaining multi-tab compatibility
- Token is rotated via crypto.randomBytes on every re-auth (was being reused)
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* UI fixes: viewer settings, button sizing, menu contrast, coords, login header
1. Viewer OSD settings moved to top of button stack, panel opens downward
2. Overlay buttons consistent 30x30px (OSD line-height fix, home button)
3. Menu/icon contrast improved: Dropdown items and IconButtons use --color-a5
(was --color-a3) with --color-f on hover for better dark theme legibility
4. CoordinatesDiv fixed to 30px height, pickLngLat button centered
5. Login modal now has a header bar with 'Log In' title and close X button;
title toggles to 'Sign Up' when switching modes
Also reverts session regeneration for token re-auth (Devin Review feedback):
token-based re-auth now refreshes session data in-place without regeneration
or token rotation, preserving multi-tab compatibility.
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* UI polish: nav popover, sep tools, compass, zoom controls, status indicator, toast
1. Description nav popover: added z-index:9000 so menu appears above map panels
2. Separated tools: default color changed from accent to --color-f; fixed CSS
selector from .toolButtonSep to .toolSep to match actual class names
3. Compass + mapping scale shifted left by 30px for better positioning
4. Map zoom/home controls: use --color-f instead of accent --color-c to reduce
visual prominence; hover still highlights with accent color
5. Status indicators (reload/ws disconnect/layer update) moved from Leaflet
control to TopBar with soft pulsing fade animation and tooltip on hover
6. WebSocket retry toast: rounded corners, glass background with backdrop-filter,
border-left accent for failure state instead of solid red background
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* Fix 14 tool UI issues: headers, backgrounds, layout, functional bugs
Header alignment:
- LayersTool: align-items center on #filterLayers, gap between right icons
- InfoTool: align-items center on #infoToolHeader, 44px height
- ViewshedTool: align-items center, restructured header with left/right divs
- IsochroneTool: restructured flat header into nested mmgisToolHeader pattern
- ShadeTool: align-items center on #vstHeader children
Icon spacing:
- LayersTool: increased right margin to 28px + gap 2px
- ViewshedTool: #vstNew padding-right 30px (clear of close button)
- IsochroneTool: #iscNew padding-right 30px
Missing components:
- SitesTool: added Help import + help icon via mmgisToolHeader pattern
- AnimationTool: added full mmgisToolHeader with title and help icon
Background fixes:
- InfoTool: changed toolsContainer background from transparent to var(--color-a)
- DrawTool: changed toolsContainer background from transparent to var(--color-a)
Layout fixes:
- DrawTool: #drawToolContents top 81px, height calc(100%-81px), #drawToolNav margin-right 0
- MeasureTool: removed padding-left:0 override from mmgisToolHeader child selector
Functional fixes:
- InfoTool: updated jQuery selectors from #InfoTool to #toolButtonInfo (React toolbar IDs changed)
- CurtainTool: deferred OpenSeadragon init with requestAnimationFrame (React 18 async render)
- CurtainTool: curtainToolBar justify-content flex-end (icons at bottom)
Security:
- TopBar StatusIndicator: escape HTML in layer names to prevent XSS via addLayerQueue
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* Fix mapToolBar pointer events, login padding, default tool, About modal order
1. mapToolBar: set pointer-events:none on both #mapToolBar and direct children
so clicks pass through to the map; leaf elements still get auto via
.childpointerevents rule
2. #loginModalBody: padding changed to 40px 0px 0px
3. Default tool: deferred click to requestAnimationFrame so React toolbar
has rendered before getElementById runs
4. About modal: moved mainInfoModalCustom to right below mainInfoModalHero
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* Fix tool headers to 40px, ViewshedTool subheader, AnimationTool header, InfoTool close btn, statusIndicator position
1. All tool panel headers: changed from 44px to exactly 40px height
- Global .mmgisToolHeader and .mmgisToolTitle in mmgis.css
- InfoTool.css, ViewshedTool.css, IsochroneTool.css, ShadeTool.css
2. LayersTool #filterLayers: height 40px, .right > div height unset,
.right margin-right 30px, .right > div margin 0px 3px
3. ViewshedTool: restructured header — title+help in mmgisToolHeader row,
vstToggleAll (left) and vstNew (right) on a new #vstSubHeader row below
4. AnimationTool: removed old #animationToolHeader CSS (padding 15px 20px,
white background, 18px font), now uses standard mmgisToolHeader class.
Fixed color from var(--color-a) (background) to var(--color-f) (text)
5. InfoTool close X: re-inject close button after use() rebuilds content
via TC_.injectCloseButton() (toolsContainer.empty() was removing it)
6. StatusIndicator: moved to left of topBarTitle in JSX render order.
Added align-items:center to #topBarMain for vertical alignment.
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* Remove title attr from StatusIndicator (conflicts with tippy tooltip)
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* Fix TimeUI dropdown z-index above tool panel, reposition toasts to top-center
1. TimeUI dropdowns: added z-index 10000 to all dropy content ul elements
so they render above the vertical tool panel (z-index 1400). Also set
timeUIDock to position:relative with z-index 10000 and overflow:visible.
2. Toast notifications: repositioned #toast-container from bottom-right to
top-center just below the topbar (top: 44px, centered with transform).
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* Fix StatusIndicator spacing, CurtainTool close btn, header title font consistency
1. StatusIndicator: use display:none/flex instead of opacity:0/1 so it
takes no space when there's no active status indicator.
2. CurtainTool: added close X button at top of curtainToolBar (matching
MeasureTool pattern) with flex spacer pushing other buttons to bottom.
3. Header title font consistency: InfoTool, ShadeTool, CurtainTool titles
now match mmgisToolTitle standard (font-weight:600, padding-left:10px,
height:40px for CurtainTool which was 34px).
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* Legend empty state, scalefactor position, sep-tool-header unbold
1. Legend: show 'No active layers with legends' when no legend items
are present. Also fixed container height calc(100% - 40px).
2. Mapping Scale (.leaflet-control-scalefactor): shifted 10px right
(left 26→36px) and 1px down (bottom 30→29px).
3. sep-tool-header span: font-weight changed from 600 to 400.
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* Guard Legend empty state message to only show when panel is active
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* Fix TimeUI dropdown covered by toolPanel: remove splitscreens stacking context
#splitscreens had z-index:1 which created a stacking context, confining
its children (including bottomFloatingBar at z-index:1500) to that context.
Since ToolPanel (z-index:1400) was a sibling outside splitscreens, it
painted above all splitscreens children regardless of their internal
z-index values.
Fix: change #splitscreens z-index from 1 to auto so it no longer creates
a stacking context. Now bottomFloatingBar (1500) participates in the
same stacking context as ToolPanel (1400), and 1500 > 1400 means the
TimeUI dropdown correctly paints above the tool panel.
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* Migrate ~69 CursorInfo toast-like calls to proper Toast component
- Replace CursorInfo.update() toast-like calls with Toast.info/success/warning/error
- Preserve message colors: blue→info, green→success, yellow→warning, red→error
- Keep 12 legitimate cursor-following CursorInfo calls unchanged
- Files migrated: DrawTool.js, DrawTool_Files.js, DrawTool_FileModal.js,
DrawTool_Templater.js, DrawTool_SetOperations.js, DrawTool_Drawing.js,
DrawTool_Editing.js, DrawTool_Shapes.js, LayersTool.js, ShadeTool.js,
chemistrychart.js
- Fix Devin Review: Change misleading 'Bad token' to 'Login failed' in users.js
- Normalize line endings (CRLF→LF) in affected files
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* Fix Toast.js missing from git, CoordinatesDiv z-index, Legend duplicate ID, topBar padding
- Add Toast.js to version control (was untracked, causing webpack module error)
- Bump CoordinatesDiv z-index from 20 to 1001 (was hidden behind splitscreens
children after z-index:auto change)
- Fix Legend duplicate ID: separated tool icon was #LegendTool, same as content
container div, causing empty message to appear in button instead of panel
- Add hasStatus class to #topBarMain when statusIndicator is active, setting
#topBarTitleName padding-left to 0
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* Fix Legend empty message: scope selector to content container via targetId
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* Move Toast.js to design-system, fix --color-a3 text contrast, update AGENTS.md
- Move Toast.js from UserInterface_/components/Toast/ to design-system/components/Toast/
(generic component belongs in design-system, not MMGIS-specific UserInterface_)
- Update all 11 Toast import paths to new location
- Bump --color-a3 in 5 dark themes to pass WCAG AA 4.5:1 contrast for text:
Dark Default #747c81→#81888d, Dark Blue #64748b→#738399,
Dark Warm #8b7a5e→#908064, Dark Mars #8a6a60→#98796f,
Dark Midnight #606088→#7a7a9e
- Update AGENTS.md: document design-system/ vs UserInterface_/ distinction
in project structure and Key Directories
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* Fix topBarTitleName padding override: increase specificity and add !important
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* Fix IdentifierTool deactivation: update icon ID reference in separateFromMMWebGIS
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* Hide toolPanelDrag when no tool is open
- Add setToolPanelDragVisible(false) to closeActiveTool() (was only in makeTool toggle-off path)
- Also guard drag handle display on isOpen (toolPanelWidth > 0) as safety net
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* Add hover effect to MMGIS logo (opacity + brightness transition)
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* Fix MMGIS logo hover: keep full opacity, use subtle background highlight instead
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* Selective tile fade: fade on pan/zoom, instant on refresh/reload
- Remove blanket _fadeAnimated toggle from toggleTimeUI (was killing fade
for all tiles while TimeUI was open)
- Monkey-patch GridLayer.redraw, TileLayer.setUrl, and GridLayer._tileReady
to suppress fade via a transient _suppressTileFade map flag
- Set _suppressTileFade in reloadTimeLayers for time-driven reloads
- Flag auto-clears after 300ms so pan/zoom tiles still get the nice fade
- Install pbf dependency (required by CesiumMVTLayer from #942 merge)
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* Per-layer fade control: time-enabled + shade/viewshed layers never fade
- Replace transient map-level _suppressTileFade with per-layer _noFade flag
- Patch GridLayer._tileReady to check _noFade on the layer instance
- Set _noFade on time-enabled tile layers and data/GL layers at creation
- Set _noFade on Shade and Viewshed tool GL layers
- Non-time-enabled base imagery tiles still fade normally on pan/zoom
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix: mobile UI improvements — move hamburger to right menu, show panel toggles, isMobile-driven toolbar layout, desktop-matching scalebar/compass
- Remove left hamburger menu (#topBarMenu), move BottomBar items into
top-right kebab dropdown menu for both mobile and desktop
- Show panel toggles (Viewer/Map/Globe) and account/login UI in mobile
topbar's #topBarRight
- Move toolbar horizontal layout CSS from @media breakpoints to
UserInterfaceMobile_.css (loaded only when isMobile flag is true)
- Remove #mapTopBar @media rule from mmgis.css, add to mobile CSS
- Remove mobile-only simplified scalebar rendering; use full desktop
scalebar with both large and small axes on all viewports
- Remove display:none on .leaflet-control-scalefactor in mobile CSS
- Remove #loginDiv display:none from mobile CSS (React overlay handles it)
- Simplify BottomBarReact container styles (no more absolute positioning)
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* chore: bump version to 4.3.28-20260430 [version bump]
* fix: mobile toolbar 40px height, bottomFloatingBar flush, timeUI toggle, scalebar position, hide hotkeys
- Toolbar height 40px, toolButton width 40px, no border-bottom
- toolcontroller_incdiv: no padding-bottom, overflow-y hidden
- bottomFloatingBar: no border-radius, left/right/bottom = 0
- Add MobileTimeUIToggle button on far right of toolbar
- Hide Keyboard Shortcuts from kebab menu on mobile
- Fix scalebar positioning (remove top:48px override in UserInterfaceBridge)
- Set mobileTopSize/topSize to 40 (splitscreens top = 40px, not 50px)
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix: mobile topBar padding-left 34px
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix: update MobileCoordButton topBar paddingLeft from 80px to 34px
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix: MobileTimeUIToggle — inline toggle logic, float right, hide from settings on mobile
- Replace broken Coordinates.toggleTimeUI() call with direct jQuery/store toggle
- Float time button right in toolbar
- Hide Time UI toggle from settings modal on mobile (toolbar has it)
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix: push scalebar/compass/scale up 40px on mobile, keep #timeUI in DOM
- BottomElementPositioner: position mapToolBar, leaflet-bottom-left/right
40px above bottom on mobile (above toolbar)
- Stop removing #timeUI from DOM on mobile so MobileTimeUIToggle works
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix: mobile TimeUI — only show endtime, always expanded
- Hide #mmgisTimeUIStartWrapper and StartWrapperFake on mobile via CSS
- Force expanded state (addClass expanded + show) when toggling TimeUI on
- CSS ensures #timeUI.active always shows expanded content on mobile
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix: mobile TimeUI opens in tool panel with header, end time, expanded rows
- MobileTimeUIToggle now opens/closes the tool panel via ToolController_
- Closes any active tool before showing TimeUI
- Forces expanded state when opening
- CSS hides start time inputs, positions expanded content properly
- Overrides absolute positioning of expanded content for tool panel flow
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* feat: rewrite separated tools system from jQuery to React components
- Add separatedToolsList/activeSeparatedTools state to Zustand uiStore
- Rewrite SeparatedTools.jsx with glassmorphism panels, CSS Module styling
- Replace SepToolsContainer (setInterval hack) with SepToolButton/SepToolsSection
- Remove ~170 lines of jQuery DOM construction from ToolController_.js
- Fix hardcoded rgba(26,26,27,0.88) to theme-aware var(--color-a-rgb)
- Remove separated tool entries from themeApplier.js
- Remove separated tool overrides from FloatingElements.css
- Move Legend CSS overrides from Toolbar.module.css to SeparatedTools.module.css
- Remove jQuery active-state manipulation from IdentifierTool.js
- Add store sync in Map_.js displayOnStart logic
- Preserve all DOM IDs for backward compatibility (mmgisAPI, tool make())
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* chore: bump version to 4.3.28-20260501 [version bump]
* fix: TimeUI mobile checks — use Zustand store instead of L_.UserInterface_
L_.UserInterface_ is null when TimeUI.init() runs (TimeControl.init is called
before L_.link sets UserInterface_). All 16 isMobile checks now read from
useUIStore.getState().isMobile which is set at startup.
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* chore: bump version to 4.3.29-20260501 [version bump]
* fix: move displayOnStart logic from Map_.js to ToolController_.finalizeTools()
- Map_ no longer references specific tools (LegendTool)
- displayOnStart is now handled generically for all separated tools
- Added DOM element polling (tryMake) to handle React render timing
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* revert: remove all TimeUI-related mobile changes
Reverts TimeUI.js and BottomBar.js to development base.
Restores #timeUI DOM removal in UserInterfaceBridge.fina().
Removes MobileTimeUIToggle component from Toolbar.jsx.
Removes TimeUI mobile CSS overrides from UserInterfaceMobile_.css.
Non-TimeUI refinements (toolbar height, scalebar positioning, etc.) preserved.
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* simplify: remove DOM polling, use simple setTimeout(0) for auto-open
LegendTool handles its own content lifecycle via subscribeOnLayerToggle.
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* feat: mobile TimeUI — fix isMobile detection, staging container, toolbar toggle
- TimeUI.js: import useUIStore and replace all 16 L_.UserInterface_?.isMobile
checks with useUIStore.getState().isMobile (L_.UserInterface_ is null when
TimeUI.init() runs, so mobile conditionals were dead code)
- TimeUI.js: stage mobile #timeUI in hidden #timeUIMobileStaging instead of
placing directly in #tools (which gets cleared by other tools)
- UserInterfaceBridge.js: stop removing #timeUI from DOM on mobile
- Toolbar.jsx: add MobileTimeUIToggle that moves #timeUI between staging and
#tools, opens/closes tool panel via ToolController_
- BottomBar.js: hide TimeUI toggle from settings modal on mobile
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix: rescue #timeUI back to staging when another tool opens
Subscribe to activeToolName changes — when a tool becomes active while
TimeUI is showing, move #timeUI back to #timeUIMobileStaging before
the new tool's make() clears #tools.
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* chore: remove separatedTool/justification config toggles, fix review issues
- Remove separatedTool checkbox and justification dropdown from Legend
and Identifier config.json (these are always separated, not configurable)
- Remove justification property/code from LegendTool.js, IdentifierTool.js
- Simplify Globe_.js separated tool count (no justification filter)
- Remove justification from Reference-Mission config blueprint
- Update LegendTool help docs and Legend.md documentation
- Add --color-a-rgb fallback (29,31,32) in SeparatedTools.module.css
- Add display:none !important to .panelIdentifier to prevent 12px gap
- Update e2e test comment
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix: circular import in TimeUI.js, toolbar/bottomFloatingBar position sync
- TimeUI.js: replace top-level useUIStore import with lazy _getUIStore()
accessor to avoid 'Cannot access useUIStore before initialization'
circular import error at _remakeTimeSlider
- SplitScreens.jsx: skip #timeUI reparenting observer on mobile (mobile
uses MobileTimeUIToggle to manage #timeUI placement in #tools)
- BottomElementPositioner.jsx: unify mobile transition to 0.3s (matches
toolsWrapper and toolbar), guard pxIsTools against undefined
- Toolbar.jsx: align toolbar transition to 0.3s ease-out
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* LegendTool fix empty message
* chore: remove separated tools offset logic from Globe_.js
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix: skip _makeHistogram on mobile (no timeline slider, timestamps unset)
_makeHistogram renders inside the timeline slider which doesn't exist
on mobile. Without it, _timelineStartTimestamp is NaN, causing
'Invalid time value' RangeError at toISOString().
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix: mobile TimeUI — populate expanded rows, fix Invalid date, fix panel height
- TimeUI.js attachEvents: use _initialStart/_initialEnd on mobile (same
as desktop) instead of L_.TimeControl_ which isn't set yet at init time.
Fixes 'Invalid date' in start/end time inputs.
- TimeUI.js fina: set expanded=true on mobile and call _populateExpandedRows()
so year/month/day/hour rows actually render. Removed position:absolute and
pointer-events:none overrides.
- Toolbar.jsx: set tool panel height to 217px (TimeUI.height) instead of
45% viewport — matches actual TimeUI content height.
- UserInterfaceMobile_.css: expanded content flows naturally (position:relative),
hide start time inputs, allow overflow scroll, flex-wrap topbar.
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix: mobile TimeUI justify-content center, restore toolbar border-bottom
- Add justify-content: center to #mmgisTimeUIMain on mobile
- Remove border-bottom: none override so toolbar keeps its default border
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix: mobile TimeUI overflow hidden, scalebar/compass fixed at 40px offset
- #timeUI overflow-y: hidden (was auto, causing 2px scroll)
- Scalebar/compass/map controls stay at fixed 40px offset (above toolbar)
regardless of tool panel state — no longer shift up by pxIsTools
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* Implement multi-tier knowledge architecture
- Restructure AGENTS.md from 745 lines to 106 lines (Tier 1: essential context)
- Create knowledge/ directory with 30+ wiki-style documentation files (Tier 2: deep knowledge)
- Create knowledge/reference/ with 8 detailed reference files (Tier 3: lookup material)
- Move AI-GETTING-STARTED.md and AI-DEVELOPMENT.md to knowledge/
- Update all file references in .specify/templates and blueprints
- Create knowledge/README.md as the full knowledge base index
- Create knowledge/reference/README.md as reference material index
Three-tier knowledge discovery system:
Tier 1: AGENTS.md (~106 lines) - scannable in <2 minutes
Tier 2: knowledge/*.md - deep knowledge on architecture, tools, APIs, DB, infra
Tier 3: knowledge/reference/*.md - coding conventions, API reference, troubleshooting
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* chore: bump version to 4.3.29-20260501 [version bump]
* fix: mobile toolbar active button style matches desktop, fix icon alignment
- All mobile toolbar buttons (ToolButton, MobileCoordButton, MobileTimeUIToggle)
now use display:flex with align-items/justify-content center for proper
vertical icon centering
- MobileCoordButton: changed 'active' class to 'toolButtonActive' to match
the global CSS active style (color-mmgis + color-i background)
- Removed inline color overrides so CSS .toolButtonActive takes effect
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* Add Devin knowledge notes from past MMGIS sessions
Include curated lessons learned from past Devin sessions:
- CI/CD: ignore build-arm64/amd64 failures, focus on required checks
- Child sessions: no separate PRs when consolidating
- ENV triple-update rule (.env, sample.env, ENVs.md)
- Error handling: use logger with infrastructure_error for fatal startup errors
- Path traversal security: stay within /Missions, handle subpath serving
- Database initialization architecture and migration patterns
- API authentication behavior across AUTH modes
- Auto-generated MMGIS concept index
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix: mobile toolbar active button style, icon alignment, tool deactivation
- Active toolbar buttons get desktop-matching margin (1px 0) and
border-radius (8px) via .toolButton.toolButtonActive CSS rule
- Removed line-height: 40px from .toolButton (flex centering handles
vertical alignment, line-height was pushing icons up)
- MobileCoordButton now watches activeToolName store and deactivates
when another tool opens (fixes coords staying active)
- MobileTimeUIToggle sets activeToolName='MobileTimeUI' when opening
so coords/other buttons can detect it and deactivate
- MobileTimeUIToggle clears activeToolName when closing
- Both custom buttons skip self-deactivation via name check
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* Fix cross-references: convert backtick refs to markdown links, add Devin knowledge notes
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix: mobile toolbar icon height 40px, button margins for active padding
- #toolbar .toolButton i: height 40px fixes icon vertical alignment
- #toolbar .toolButton: margin 0 2px gives spacing between buttons
- #toolbar .toolButton.toolButtonActive: margin 1px 2px so active
background has visual padding around the icon
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* Rename knowledge/ to .knowledge/ for consistency with .specify/ convention
Dot-prefix signals agent infrastructure (not source code), consistent with
.specify/, .github/, .vscode/ conventions. All cross-references updated.
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix: mobile toolbar icon line-height 40px, active button padding via height
- Coord and TimeUI button <i> icons get line-height: 40px
- Active buttons: height 34px (vs 40px toolbar) creates visual padding
around the active background, centered by flex align-items
- Buttons get margin: 0 1px for horizontal spacing
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* Fix broken cross-reference: 06.2 -> 06.1-configure-rest-api.md
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix: close active tool + cancel deferred cleanup in MobileCoordButton/TimeUI
- MobileCoordButton: call closeActiveTool() before opening, destroy
_pendingCloseTool if set, increment _closeSeq to cancel deferred
tools.innerHTML clear
- MobileTimeUIToggle: same _pendingCloseTool + _closeSeq fix after
closeActiveTool() to prevent 420ms deferred cleanup from wiping
#timeUI after it's placed in #tools
- Removed redundant closeActiveTool() from MobileCoordButton close path
(was being called after destroy, not needed)
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix: active mobile toolbar buttons 34x34px (square)
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* Drastically compress .knowledge/ — keep only unique agent content
Remove 33 wiki files that duplicate docs/pages/ content.
Remove 9 reference/ files derivable from source code.
Keep only 5 files (down from 46):
- AI-GETTING-STARTED.md (agent setup walkthrough)
- AI-DEVELOPMENT.md (spec-kit workflow)
- conventions-and-gotchas.md (naming, code style, common issues)
- 12-devin-knowledge-notes.md (CI, auth, DB init, security gotchas)
- README.md (index pointing to docs/pages/ for everything else)
Principle: don't duplicate docs/ — only keep what's uniquely agent-optimized.
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* Rename to knowledge-notes.md, remove Devin branding and fork-specific CI section
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix: hide mmgis-map-logo on mobile
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* Restore Database Safety Rules for AI Agents section in AGENTS.md
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix: shift compass and map scale 6px to the right (both mobile and desktop)
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* Add back Important Instructions, code pattern templates, and detailed project structure
- Important Instructions in AGENTS.md: MCP tools, hot-reload, Reference Mission
- .knowledge/code-patterns.md: full directory tree with key directory annotations,
plus copy-paste templates for Express routes, Sequelize models, Tool plugins,
and WebSocket handlers
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* Update project structure trees to reflect current filesystem
Add missing directories: tests/, .knowledge/, .specify/, .github/, views/,
private/, spice/, build/, examples/, scripts/middleware.js.
Both abbreviated (AGENTS.md) and detailed (.knowledge/code-patterns.md) trees
now match the actual repo layout.
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* chore: bump version to 4.3.30-20260501 [version bump]
* Add Layers_.js to project structure (key singleton L_)
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* Fix project structure: correct API layout, frontend modules, code templates
API/Backend/ uses feature-domain modules (Draw/, Users/, Config/, etc.)
with setup.js + routes/ + models/ per feature — not APIs/ or Databases/.
Frontend essence/ has Components/, Helpers/, LandingPage/, mmgisAPI/,
services/ — not Ancillary/. Basics/ includes all singletons (Globe_,
Formulae_, ToolController_, Viewer_, ComponentController_, Test_).
Code templates updated to match actual patterns (setup.js, module.exports).
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* refactor: remove test infrastructure (Test_ module, testModules, DrawTool.test)
- Delete src/essence/Basics/Test_/ directory
- Delete src/essence/Tools/Draw/DrawTool.test.js
- Remove Test_ import and Shift+T keydown handler from essence.js
- Remove tests key from Draw tool config.json
- Remove testModules generation logic from API/updateTools.js
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* chore: bump version to 4.3.31-20260501 [version bump]
* style: move Cesium link button to top-right and match Leaflet zoom button styling
- Change control container from top-left to top-right positioning
- Update button size from 26px to 30px to match Leaflet zoom controls
- Use CSS variables (--color-a, --color-f, --color-mmgis) instead of hardcoded colors
- Add border-radius and box-shadow matching Leaflet control appearance
- Update hover/inactive states to use themed colors
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix: anchor map logo to viewport instead of Leaflet map panel
- Change MapLogo parent from .leaflet-bottom.leaflet-right to #main-container
- Switch CSS position from absolute to fixed for viewport anchoring
- Add explicit bottom-offset positioning in BottomElementPositioner (desktop)
- Add explicit bottom-offset positioning in BottomElementPositioner (mobile)
- Logo stays at viewport right edge regardless of open side panels
- Retains smooth bottom offset transitions when bottom bar appears
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* docs: remove references to deleted test infrastructure (Test_, DrawTool.test)
- Remove Test_/ from project structure in .knowledge/code-patterns.md
- Remove DrawTool.test.js references from specs/006 spec, plan, and tasks
- Remove Draw Tool Testing section from tasks.md
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* chore: bump version to 4.3.32-20260501 [version bump]
* fix: append logo to document.body to avoid filter containing block
#main-container has a CSS filter property which creates a new containing
block per the CSS spec, causing position:fixed to behave like absolute.
Appending to document.body ensures true viewport-fixed positioning.
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix: prevent mobile topBarTitleName text wrapping by replacing max-width with white-space: nowrap
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* chore: bump version to 4.3.33-20260501 [version bump]
* chore: bump version to 5.0.0 and update changelog
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* refactor(ui): move Screenshot/Fullscreen to BottomBar, About to TopBar kebab
TopBar kebab menu now contains only Keyboard Shortcuts, Settings, and About
(About now shows on both desktop and mobile).
BottomBarReact now renders Screenshot, Fullscreen, and Copy Link buttons
(top to bottom) following the same IconButton + Tooltip pattern. The
About button has been removed from BottomBarReact.
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* chore: bump version to 5.0.1-20260505 [version bump]
* feat(mobile): enforce exclusive panel toggling on mobile in TopBar
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* chore: bump version to 5.0.1-20260505 [version bump]
* style: reposition LithoSphere globe controls to match Leaflet/Cesium theme
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* chore: bump version to 5.0.1-20260505 [version bump]
* feat(topbar): hide Viewer/Globe toggles based on configured panels
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* style(bottombar): reorder buttons (Copy Link, Screenshot, Fullscreen) and unify size
Reorder the BottomBarReact buttons top-to-bottom to: Copy Link, Screenshot,
Fullscreen.
Move the 24x24 button sizing from the #topBarLink id selector in mmgis.css
into the .barButton class in BottomBarReact.module.css so all three buttons
share the same compact size as the original Copy Link button. Drop the now
redundant #topBarLink rule.
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* style(bottombar): increase padding-bottom to 12px and button margin to 3px 0
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* style: rearrange globe controls — compass top-right circular, nav row, vertical column, panels open left
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* chore: bump version to 5.0.2-20260505 [version bump]
* chore: bump version to 5.0.2-20260505 [version bump]
* style: anchor observe settings panel right:34px and float nav hover panels
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* feat(theming): add 5 new themes, --color-shadow variable, and configure ThemePreview
- Add Dark Terra, Dark Nebula, Dark Lunar, Dark Supernova, Light Botanical themes
- Add --color-shadow CSS variable to every theme + :root fallback
- Replace hardcoded rgba shadow colors with var(--color-shadow) in TopBar,
Toolbar, SeparatedTools, ToolPanel, FloatingElements, Dropdown, Modal,
and SplitScreens
- Add Custom shadowcolor color picker in tab-ui-config and apply it via Stylize
- Add ThemePreview component (configure/src) wired through Maker.js as
a new 'themepreview' row type so the configure UI shows a live mini
mockup of the selected theme
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* chore: bump version to 5.0.2-20260505 [version bump]
* fix(configure/ThemePreview): tighten top spacing and live-preview Custom theme
- Pull the preview up by 12px so the gap below the theme dropdown is tighter.
- Read the Custom color pickers (look.primarycolor / secondarycolor /
tertiarycolor / accentcolor / shadowcolor / topbarcolor / toolbarcolor /
mapcolor) from the configuration and overlay them on Dark Default so
the preview reflects Custom theme edits live, matching Stylize.js's
runtime behavior.
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* chore: bump version to 5.0.3-20260505 [version bump]
* feat(themes): add Dark Heliosphere, Dark Monokai, and Light Solarized
- Dark Heliosphere: deep night purple surface with corona-orange accent.
- Dark Monokai: warm graphite surface with lime accent (Monokai-inspired).
- Light Solarized: classic solarized base3/base02 with blue accent.
Mirror added to configure/src/themes/themes.js for the ThemePreview, and
the three names appended to the Color Theme dropdown options.
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix(coordinates): respect time.initiallyOpen when live deep-link is set
* chore: bump version to 5.0.3-20260505 [version bump]
* refactor(theming): remove Custom theme + per-field color overrides
- Drop the 'Custom' option from the Color Theme dropdown.
- Remove all Custom Color Options (look.primarycolor, .secondarycolor,
.tertiarycolor, .accentcolor, .bodycolor, .topbarcolor, .toolbarcolor,
.mapcolor, .hightlightcolor, .shadowcolor) from tab-ui-config.json.
- Strip the matching DOM/CSS-variable override block from Stylize.js;
Stylize now just applies the selected preset theme (and the page logo).
- Drop the empty bodycolor/topbarcolor/toolbarcolor/mapcolor/shadowcolor
defaults from API/templates/config_template.js.
- Simplify ThemePreview to render the selected preset directly — no
Custom branch, no overlay logic.
Preset themes cover all the looks we want and keep the configure surface
much smaller.
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* style(time-ui): round corners on TimeUI shell, action wrappers, mode dropdown
- #timeUI: 10px border-radius on the outer time control bar.
- #mmgisTimeUIActionsLeft / #mmgisTimeUIActionsRight: 10px border-radius
so the action clusters sit as rounded chips.
- #mmgisTimeUIActionsRight > div (excluding #mmgisTimeUIPresent): 10px
border-radius on each action button so they match the wrapper.
- #mmgisTimeUIModeDropdown: 40px height + 10px border-radius to align
with the rest of the bar; clear the dropy default border-color so the
rounded edge isn't outlined.
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* chore: bump version to 5.0.4-20260505 [version bump]
* feat(configure): mark light themes as (experimental) in dropdown label
Light themes still have outstanding contrast issues, so flag them in the
Color Theme dropdown without changing the saved value.
- Maker dropdown now accepts options as either a plain string (current
behavior) or { value, label } so the rendered label can differ from
the persisted value.
- tab-ui-config switches the six light themes to { value, label } form
with '(experimental)' appended to the label only. Existing mission
configs that already saved 'Light Default' etc. continue to match.
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* Fix timeUI border radius
* fix(mobile): rescue #timeUI before tool make() destroys it
Clicking Layers -> Time -> Layers -> Time on mobile caused the bottom
panel to render LayersTool content with TimeUI height. The #timeUI DOM
element was destroyed when LayersTool.make() called $('#tools').empty(),
before the async React useEffect in MobileTimeUIToggle could rescue it
to its staging container.
- ToolController_.makeTool: synchronously move #timeUI from #tools back
to #timeUIMobileStaging (and reset TimeUI store flags) on mobile,
before invoking the new tool's make().
- MobileTimeUIToggle.handleClick: defensive fallback that re-initializes
TimeUI if #timeUI no longer exists when the toggle is activated.
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix(mobile): move re-initialized #timeUI from staging into #tools
TimeUI.init() on mobile appends the new #timeUI to the hidden
#timeUIMobileStaging container, so the fallback branch must also move
it into #tools — otherwise the user sees an empty tool panel after
the destroyed-element recovery path.
Caught by Devin Review.
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix(mobile): preserve #timeUI when Coordinates tool empties #tools
On mobile, opening or closing the Coordinates tool runs
$('#tools').empty() inside interfaceWithMMWebGIS / separateFromMMWebGIS.
After the previous PR commits, clicking Coordinates -> Time still left
the bottom panel empty because:
- Coordinates.make() empties #tools while #timeUI is in staging (fine
on its own), but the Coordinates teardown that fires after the user
switches to the Time toggle (via MobileCoordButton's useEffect on
activeToolName change) calls Coordinates.destroy() ->
separateFromMMWebGIS(), which empties #tools wholesale and destroys
the freshly-placed #timeUI.
Add a rescueMobileTimeUI() helper that moves #timeUI from #tools back
to #timeUIMobileStaging before each tools.empty() call in Coordinates,
mirroring the rescue already done in ToolController_.makeTool().
Coordinates -> Time now correctly shows the TimeUI.
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix(mobile): harden TimeUI fallback recovery (call fina(), de-dupe popovers)
Devin Review correctly flagged that the safety-net path in
MobileTimeUIToggle.handleClick was producing a partially-broken TimeUI
when it fired:
- TimeUI.init() unconditionally appends a new #timeUIPlayPopover_global
to <body>, so a second init() left two elements with the same id.
- TimeUI.init() alone does not wire up date pickers or per-button click
handlers — that's TimeUI.fina()'s job. Without fina(), the recovered
TimeUI rendered visually but Play / Previous / Next / Fit / Follow /
Present / Expand were all dead.
Before re-initializing, remove the stale #timeUIPlayPopover_global and
#timeUIQuickSelectPopover_global divs to avoid duplicate ids. After the
new #timeUI is moved into #tools, call TimeUI.fina() to populate the
date pickers, attach the button click handlers, build the histogram,
and populate the expanded mobile rows.
Some delegated body/document handlers in attachEvents() will still be
duplicated on this path; that is acceptable for a degraded recovery
that should never run in practice now that the primary rescues in
ToolController_.makeTool() and Coordinates.js cover all known paths.
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* chore: bump version to 5.0.5-20260505 [version bump]
* fix(mobile): Coordinates teardown only removes its own DOM
The previous Coordinates fix was racing with itself: after the Time
toggle synchronously moved #timeUI into #tools, MobileCoordButton's
useEffect (triggered by the activeToolName change) ran on the next
React tick and called L_.Coordinates.destroy(). That called
separateFromMMWebGIS(), whose rescue moved #timeUI right back into the
hidden staging div before tools.empty() — so the bottom panel ended up
empty even though the time toggle was 'active'.
Make separateFromMMWebGIS selective: only remove the
Coordinates-specific DOM (#coordUIHeader and #CoordinatesDiv) instead
of wiping all of #tools. Any other content already in #tools (e.g.
#timeUI placed there by the Time toggle) is left alone.
interfaceWithMMWebGIS still keeps the rescue + tools.empty() pattern
on the open path so Coordinates always starts from a clean panel.
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* Bump DrawTool Temporal Drawings upward
* chore: bump version to 5.0.6-20260505 [version bump]
* chore: reset version to 5.0.0
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* test(e2e): fix 9 pre-existing failures (test-only changes)
- mmgis-api.spec.js: add form-fill login under AUTH=local; serialize
describe to avoid concurrent-login race in the session store
- coordinates.spec.js: TimeUI toggle was moved from the coordinates bar
to the Settings modal; navigate via topbar kebab menu and assert the
checkbox is rendered
- widgets.spec.js: target .leaflet-control-zoom-in/-out specifically;
the bare .leaflet-control-zoom class is also used by the home/reset
control, so the original assertion was always false
- sites.spec.js: scope panel selector to #toolPanel; both the toolbar
icon and the panel container share id="SitesTool"
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* chore: bump version to 5.0.1-20260505 [version bump]
* Revert "chore: bump version to 5.0.1-20260505 [version bump]"
This reverts commit 4880204c1163be5d1d7fa96d14a0ed018c6f586c.
* fix: prevent filter operator dropdown clipping in Layers panel
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* chore: bump version to 5.0.1-20260507 [version bump]
* revert: keep dropy openUp:true for operator dropdowns
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* Revert "chore: bump version to 5.0.1-20260507 [version bump]"
This reverts commit d67c369ed437e47d658ae051348d377978dc48ed.
* chore: bump version to 5.0.1-20260507 [version bump]
* Revert "chore: bump version to 5.0.1-20260507 [version bump]"
This reverts commit 29565ed829a55e9c241a789c9a3901d11cb5ca67.
* chore: bump version to 5.0.1-20260507 [version bump]
* Revert "chore: bump version to 5.0.1-20260507 [version bump]"
This reverts commit 50e357604ebe9378564619b34c508b63cfb62c1d.
* chore: bump version to 5.0.1-20260507 [version bump]
* chore: bump version to 5.0.2-20260511 [version bump]
* fix: render Globe panel immediately on first open without window resize
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* chore: bump version to 5.0.3-20260511 [version bump]
* feat: add theme borders to panels and gradient backgrounds to splitters
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* chore: bump version to 5.0.4-20260511 [version bump]
* style: bump split shadow gradient opacity to 0.4
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* style: hotkeys modal 3-col grid + smaller leaflet zoom button gap
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* style: prevent hotkey label/value wrapping (ellipsis instead)
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* style: hotkeys modal single column, no wrap, no truncation
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* chore: bump version to 5.0.4-20260511 [version bump]
* style: hotkeys modal dividers, invert title/subtitle colors, rename title, margin above subtitles
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* style: move splitter gradient to themed CSS class, restore hover feedback
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* chore: bump version to 5.0.5-20260511 [version bump]
* style: hotkeys section titles use --color-h (matches rest of app)
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* chore: bump version to 5.0.5-20260511 [version bump]
* fix: guard Globe_.init() inside rAF to prevent double instantiation
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* chore: bump version to 5.0.6-20260512 [version bump]
* feat(plugins): per-plugin deps, lazy tool loading, validation, shared discovery
Phase 3 — Plugin config validation + override warnings:
- New API/pluginValidation.js with validatePluginConfig() for tool, component,
and backend manifests. Validates required fields (name, paths), object/string
shape of paths, dependencies block (npm/python.pip/python.conda), and warns
on unknown top-level fields.
- updateTools()/updateComponents() now skip invalid plugins and emit override
warnings (matching what components already logged for tools).
Phase 2 — Shared discoverPlugins() utility:
- New API/pluginDiscovery.js consolidates the duplicated scanning logic from
updateTools(), updateComponents(), and getBackendSetups(). Supports exact-
name and substring container patterns, JSON/require/no-op loaders, and skips
dot/underscore-prefixed dirs.
- updateTools.js and setups.js refactored on top of the shared helper.
Phase 1 — Per-plugin dependency declaration + build-time aggregation:
- Plugin config.json may now declare a 'dependencies' block (npm + python.pip +
python.conda). validatePluginConfig() also validates this shape.
- New scripts/resolve-plugin-deps.js scans every tool/component/backend plugin
and writes plugin-package.json, plugin-python-requirements.txt, and
plugin-conda-deps…
) * Fix Modal: remove close button, fix blur persistence, add fade animation - Remove the Dialog.Close button (curved bottom-left border-radius) - Drop Base UI Dialog wrapper entirely — it was fighting with the imperative Modal.set/remove API. Now uses simple divs with CSS transitions, matching the original jQuery modal behavior exactly. - Blur management is now purely imperative via _applyBlur() called synchronously in set() and remove(). Removed async useEffect approach. - Add 500ms CSS opacity fade-in/fade-out transition matching original. - Closing state: Modal.remove() marks modal as closing (triggers opacity 0 transition), then removes from DOM after 500ms. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Rename Ancillary React components to .jsx, fix modal blur via Zustand store, fix IconButton forwardRef, add Help.finalize calls - Rename Modal, ConfirmationModal, Help, ContextMenu, Compass, MapLogo from .js to .jsx - Route modal blur through Zustand modalBlurCount instead of imperative DOM manipulation - Remove conflicting jQuery blur animation in Layers_.js - Wrap IconButton with React.forwardRef to fix Tooltip/Menu trigger warnings - Add Help.finalize() calls to ChemistryTool, DrawTool, IsochroneTool - Change aboutModalContent config type from 'markdown' to 'textarea' Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Revert aboutModalContent type back to 'markdown' — Maker.js already handles it Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Fix Help.jsx: check res.ok on fetch, sanitize HTML with DOMPurify Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Fix ContextMenu WKT null guard, fix Modal blur timing during fade-out - Add null check for feature in handleActionClick WKT placeholder handling - Delay blur removal until after 500ms fade-out completes (blur stays in sync with backdrop opacity) Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Reorganize folder structure: dissolve Ancillary, nest components - Dissolve src/essence/Ancillary/ entirely - UI components → UserInterface_/components/ (Modal, ConfirmationModal, Help, ContextMenu, Compass, MapLogo, CursorInfo, Attributions, Login) - Layout chrome → UserInterface_/components/ (Description, Coordinates, Search, ScaleBar, ScaleBox) - Pure services → essence/services/ (DataShaders, LocalFilterer, QueryURL, Sprites) - Stylize.js → design-system/ (theme bridge alongside themeApplier) - Delete unused Swap.js - Nest all components into own folders (ComponentName/ComponentName.ext pattern): - UserInterface_/components/: TopBar/, Toolbar/, ToolPanel/, SplitScreens/, Splitter/, BottomBar/, BottomElementPositioner/, Layout/, Panels/ - design-system/components/: Button/, IconButton/, Dropdown/, Toggle/, Modal/, Tooltip/ - Update ~70+ import paths across codebase - Build verified locally Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Fix Modal.set() race condition and themeApplier CSS variable override issue - Modal.set() onAddCallback: Replace 50ms setTimeout with MutationObserver that waits for the modal element to appear in DOM before firing callback. Prevents silent jQuery binding failures on slower devices. - themeApplier: Use Proxy to read computed CSS custom properties (set by Stylize.js per-mission overrides) instead of hardcoded theme object values. Stylize.js now calls refreshThemeDOM() after setting CSS variables so inline styles reflect mission-specific color overrides. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Sync toggleTimeUI DOM state to Zustand store toggleTimeUI() now calls setTimeUIActive() and setTimeUIExpanded() so BottomFloatingBar visibility and BottomElementPositioner offsets reflect actual TimeUI state. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Restore #toggleTimeUI element in Coordinates markup The element was accidentally dropped during the folder restructure move. TimeUI.js and DrawTool.js check $('#toggleTimeUI').hasClass('active') to gate histogram rendering and time-filter toggle visibility. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Fix #toggleTimeUI: restore click handler, tippy, active class; remove redundant jQuery positioning Restored pieces lost during folder restructure: - Click handler in init() and off handler in remove() - Tippy tooltip for the time toggle button - display:none when time is not enabled - $('#toggleTimeUI').toggleClass('active') so TimeUI.js can check it - $('#CoordinatesDiv > #toggleTimeUI').remove() on mobile Removed jQuery CSS positioning from toggleTimeUI() since BottomElementPositioner now reactively handles all bottom-anchored element offsets via the Zustand store. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Fix CurtainTool.destroy() using undefined ReactDOM.unmountComponentAtNode Use the stored _reactRoot.unmount() instead, matching the React 18 createRoot pattern already used in make(). Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Fix tooltips, scale indicator position, modal blur, help close button Tooltips: - Reduce Base UI Tooltip delay from 600ms (default) to 200ms - Restyle tooltip popup to match tippy blue theme (var(--color-c2)) - Add Tooltip wrappers to TopBar panel toggles (Viewer/Map/Globe) - Wrap Toggle with forwardRef so Tooltip render prop can attach ref - Remove title attrs that conflicted with custom tooltips Scale indicator: - Remove scalefactor-specific positioning from BottomElementPositioner (it moves naturally with .leaflet-bottom.leaflet-left container) - Position scalefactor to the left of compass at same bottom level Modal blur: - Call _applyBlur() immediately when marking modal as closing so blur clears during fade-out instead of persisting 500ms Help modal: - Add close (X) button in title bar matching other modal patterns Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Remove dead CSS: delete tools.css, clean ~600 lines from mmgisUI.css and mmgis.css - Delete tools.css entirely (both selectors #CurtainToolList and .searchToolSelect are unreferenced anywhere in the codebase) - Remove from mmgisUI.css: .mmgisRadioBar3/4/Vertical (140 lines), .mmgispureselect (104 lines), blink/condemned_blink_effect (38 lines), .slidecontainer/.slider (41 lines), .ar_slider (91 lines), .verticalSlider (91 lines), .mmgisMultirange_elev (19 lines), .ui-corner-all/bottom/right/br (9 lines) - Remove from mmgis.css: #nodeenv, empty #topBar{}, #topBarInfo, #topBarHelp, #topBarFullscreen, #toggleUI, #logoGoBack - Keep #topBarLink (used in BottomBarReact.jsx), #webgl-error-message (used by vendored THREE.js) - All selectors verified with repo-wide grep before removal Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * UI fixes: tooltips, splitter hover, mobile toolbar, color schemes - Replace Base UI Tooltip with simple React portal tooltip (200ms delay, tippy-matching style) — fixes missing tooltips for toolbar/topbar/bottom buttons - Add cursor + hover highlight to vertical splitters (was missing because module CSS didn't inherit global .splitterV styles) - Add hover highlight to tool panel drag handle - Remove mdi-drag-vertical icon from tool panel drag - Add mobile toolbar horizontal layout via @media query overrides - Add 4 new color schemes: High Contrast (a11y), Dark Mars, Dark Midnight, Light Warm (total: 10 themes) - Previous fixes also included in working tree: - timeUI border moved to toolsWrapper border-bottom (conditional) - #toggleTimeUI button removed entirely - CoordinatesDiv: vertical centering, unified background, 12px right offset - barBottom padding-bottom: 8px Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Fix assignment operator used instead of comparison in TimeControl.fina() Pre-existing bug: `TimeControl.enabled = true` was assigning instead of comparing. Changed to `TimeControl.enabled === true`. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Restructure Configure page UI tab: add all themes, Custom mode, enableWhenField - Added all 10 theme presets to dropdown (was missing Dark Mars, Dark Midnight, Light Warm, High Contrast) - Added 'Custom' option: skips preset theme, uses only color picker values - Moved Theming section directly under Rebranding - Nested 'Custom Color Options' under Theming with subdescription - Added enableWhenField support to Maker.js: disables color pickers unless theme is set to Custom - Renamed color options with clearer names and improved descriptions: Primary → Surface Color, Secondary → Deep Background Color, Tertiary → Text Color, Body → Page Body Color, Highlight → Feature Highlight - Stylize.js: skip setTheme() when theme is 'Custom' - Rebuilt configure page Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Revert tooltips to tippy.js, fix dropdown menus, redesign About modal 1. Tooltip: Replaced custom React portal tooltip with tippy.js wrapper. Uses the existing tippy.js dependency and 'blue' theme for consistency. 2. Dropdown: Replaced Base UI Menu with native portal dropdown. Base UI's nested Menu.Trigger + BaseButton composition was swallowing click events, breaking userAvatar and menuBtn menus. New implementation uses simple state + createPortal with proper outside-click dismissal. 3. About modal: Professional redesign with centered MMGIS ASCII art header, proper GitHub SVG logo link, clean metadata section, centered link buttons, attributions section, and NASA-AMMOS footer. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Restore .mmgisHelpButton base styles lost during Help.css module migration The global .mmgisHelpButton styles (yellow color, compact 18x18px sizing, 0.7 opacity) were removed when Help.css was converted to Help.module.css. Since Help.getComponent() emits raw HTML strings for jQuery-rendered tool headers, it cannot use CSS Module scoped classes. Restored the base styles in mmgis.css alongside the related .mmgisToolHelpBtn definition. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Fix session logout regression, About modal refinements, High Contrast theme, Stylize.js, Default Tool config - Login: skip session.regenerate() for token-based re-auth (useToken:true) so reloading the main page no longer invalidates the configure page session - About modal: replace ASCII art with mmgis.png logo, rename Attributions to Map Layer Attributions, remove footer logo, link NASA-AMMOS to ammos.nasa.gov - High Contrast theme: change accent from #ffff00 to #ffd700 (gold) for better contrast ratios against dark backgrounds - Stylize.js: color overrides only apply when theme is Custom or unset, preventing preset themes from being clobbered by stale config values - Restore Default Tool config section in tab-ui-config.json (accidentally removed during Theming section reorganization) Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Restore defaulttooldropdown case handler in Maker.js The case was accidentally removed during the Configure page UI tab restructure (d7f96c50). Without it, the Default Tool dropdown in the Configure page rendered as nothing despite the config referencing it. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * High Contrast tooltip text, panel toggle styling, scale position swap - High Contrast theme: tooltips now use black text on yellow background via --color-c2-text variable (white for all other themes) - About modal links use var(--color-f) for consistent theme text color - Panel toggle buttons: 11px uppercase with 600 weight for better visibility - Mapping scale button moved to bottom-right of compass (was top-left) Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Reposition Viewer and Globe panel buttons to top-right - Viewer: dropdown selector at top-right edge, OSD buttons stacked vertically below it; settings panel opens to the left - Globe: home, exaggerate, observe, walk, link controls moved from TopLeft to TopRight corner via addControl 4th arg - Style consistency: OSD buttons and LithoSphere controls now match Leaflet zoom controls (var(--color-a) bg, var(--color-f) text, var(--color-mmgis) hover, 30px size, 3px border-radius) - Viewer settings sliders use var(--color-a3) instead of hardcoded #444444 - Az/el indicator stays at bottom center (exception per design) Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Consistent modal theming + session security fix Modal theming: - All modals now share consistent styling: backdrop-filter blur, semi-transparent background via --color-a-rgb, 10px border-radius, header divider line, box-shadow - Updated: loginModal, Help, ConfirmationModal, Settings, Hotkeys, About modals - Tool panel backgrounds changed from opaque var(--color-k) to transparent so the ToolPanel's existing backdrop-filter effect shows through - Legend tool header updated to match consistent 44px height with divider - applyTheme.js now auto-derives --color-a-rgb from theme's --color-a hex value - Modal service wrapper gets backdrop-filter: blur(12px) Session security (Devin Review fix): - Token re-auth now calls req.session.regenerate() with data preservation to prevent session fixation while maintaining multi-tab compatibility - Token is rotated via crypto.randomBytes on every re-auth (was being reused) Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * UI fixes: viewer settings, button sizing, menu contrast, coords, login header 1. Viewer OSD settings moved to top of button stack, panel opens downward 2. Overlay buttons consistent 30x30px (OSD line-height fix, home button) 3. Menu/icon contrast improved: Dropdown items and IconButtons use --color-a5 (was --color-a3) with --color-f on hover for better dark theme legibility 4. CoordinatesDiv fixed to 30px height, pickLngLat button centered 5. Login modal now has a header bar with 'Log In' title and close X button; title toggles to 'Sign Up' when switching modes Also reverts session regeneration for token re-auth (Devin Review feedback): token-based re-auth now refreshes session data in-place without regeneration or token rotation, preserving multi-tab compatibility. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * UI polish: nav popover, sep tools, compass, zoom controls, status indicator, toast 1. Description nav popover: added z-index:9000 so menu appears above map panels 2. Separated tools: default color changed from accent to --color-f; fixed CSS selector from .toolButtonSep to .toolSep to match actual class names 3. Compass + mapping scale shifted left by 30px for better positioning 4. Map zoom/home controls: use --color-f instead of accent --color-c to reduce visual prominence; hover still highlights with accent color 5. Status indicators (reload/ws disconnect/layer update) moved from Leaflet control to TopBar with soft pulsing fade animation and tooltip on hover 6. WebSocket retry toast: rounded corners, glass background with backdrop-filter, border-left accent for failure state instead of solid red background Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Fix 14 tool UI issues: headers, backgrounds, layout, functional bugs Header alignment: - LayersTool: align-items center on #filterLayers, gap between right icons - InfoTool: align-items center on #infoToolHeader, 44px height - ViewshedTool: align-items center, restructured header with left/right divs - IsochroneTool: restructured flat header into nested mmgisToolHeader pattern - ShadeTool: align-items center on #vstHeader children Icon spacing: - LayersTool: increased right margin to 28px + gap 2px - ViewshedTool: #vstNew padding-right 30px (clear of close button) - IsochroneTool: #iscNew padding-right 30px Missing components: - SitesTool: added Help import + help icon via mmgisToolHeader pattern - AnimationTool: added full mmgisToolHeader with title and help icon Background fixes: - InfoTool: changed toolsContainer background from transparent to var(--color-a) - DrawTool: changed toolsContainer background from transparent to var(--color-a) Layout fixes: - DrawTool: #drawToolContents top 81px, height calc(100%-81px), #drawToolNav margin-right 0 - MeasureTool: removed padding-left:0 override from mmgisToolHeader child selector Functional fixes: - InfoTool: updated jQuery selectors from #InfoTool to #toolButtonInfo (React toolbar IDs changed) - CurtainTool: deferred OpenSeadragon init with requestAnimationFrame (React 18 async render) - CurtainTool: curtainToolBar justify-content flex-end (icons at bottom) Security: - TopBar StatusIndicator: escape HTML in layer names to prevent XSS via addLayerQueue Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Fix mapToolBar pointer events, login padding, default tool, About modal order 1. mapToolBar: set pointer-events:none on both #mapToolBar and direct children so clicks pass through to the map; leaf elements still get auto via .childpointerevents rule 2. #loginModalBody: padding changed to 40px 0px 0px 3. Default tool: deferred click to requestAnimationFrame so React toolbar has rendered before getElementById runs 4. About modal: moved mainInfoModalCustom to right below mainInfoModalHero Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Fix tool headers to 40px, ViewshedTool subheader, AnimationTool header, InfoTool close btn, statusIndicator position 1. All tool panel headers: changed from 44px to exactly 40px height - Global .mmgisToolHeader and .mmgisToolTitle in mmgis.css - InfoTool.css, ViewshedTool.css, IsochroneTool.css, ShadeTool.css 2. LayersTool #filterLayers: height 40px, .right > div height unset, .right margin-right 30px, .right > div margin 0px 3px 3. ViewshedTool: restructured header — title+help in mmgisToolHeader row, vstToggleAll (left) and vstNew (right) on a new #vstSubHeader row below 4. AnimationTool: removed old #animationToolHeader CSS (padding 15px 20px, white background, 18px font), now uses standard mmgisToolHeader class. Fixed color from var(--color-a) (background) to var(--color-f) (text) 5. InfoTool close X: re-inject close button after use() rebuilds content via TC_.injectCloseButton() (toolsContainer.empty() was removing it) 6. StatusIndicator: moved to left of topBarTitle in JSX render order. Added align-items:center to #topBarMain for vertical alignment. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Remove title attr from StatusIndicator (conflicts with tippy tooltip) Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Fix TimeUI dropdown z-index above tool panel, reposition toasts to top-center 1. TimeUI dropdowns: added z-index 10000 to all dropy content ul elements so they render above the vertical tool panel (z-index 1400). Also set timeUIDock to position:relative with z-index 10000 and overflow:visible. 2. Toast notifications: repositioned #toast-container from bottom-right to top-center just below the topbar (top: 44px, centered with transform). Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Fix StatusIndicator spacing, CurtainTool close btn, header title font consistency 1. StatusIndicator: use display:none/flex instead of opacity:0/1 so it takes no space when there's no active status indicator. 2. CurtainTool: added close X button at top of curtainToolBar (matching MeasureTool pattern) with flex spacer pushing other buttons to bottom. 3. Header title font consistency: InfoTool, ShadeTool, CurtainTool titles now match mmgisToolTitle standard (font-weight:600, padding-left:10px, height:40px for CurtainTool which was 34px). Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Legend empty state, scalefactor position, sep-tool-header unbold 1. Legend: show 'No active layers with legends' when no legend items are present. Also fixed container height calc(100% - 40px). 2. Mapping Scale (.leaflet-control-scalefactor): shifted 10px right (left 26→36px) and 1px down (bottom 30→29px). 3. sep-tool-header span: font-weight changed from 600 to 400. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Guard Legend empty state message to only show when panel is active Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Fix TimeUI dropdown covered by toolPanel: remove splitscreens stacking context #splitscreens had z-index:1 which created a stacking context, confining its children (including bottomFloatingBar at z-index:1500) to that context. Since ToolPanel (z-index:1400) was a sibling outside splitscreens, it painted above all splitscreens children regardless of their internal z-index values. Fix: change #splitscreens z-index from 1 to auto so it no longer creates a stacking context. Now bottomFloatingBar (1500) participates in the same stacking context as ToolPanel (1400), and 1500 > 1400 means the TimeUI dropdown correctly paints above the tool panel. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Migrate ~69 CursorInfo toast-like calls to proper Toast component - Replace CursorInfo.update() toast-like calls with Toast.info/success/warning/error - Preserve message colors: blue→info, green→success, yellow→warning, red→error - Keep 12 legitimate cursor-following CursorInfo calls unchanged - Files migrated: DrawTool.js, DrawTool_Files.js, DrawTool_FileModal.js, DrawTool_Templater.js, DrawTool_SetOperations.js, DrawTool_Drawing.js, DrawTool_Editing.js, DrawTool_Shapes.js, LayersTool.js, ShadeTool.js, chemistrychart.js - Fix Devin Review: Change misleading 'Bad token' to 'Login failed' in users.js - Normalize line endings (CRLF→LF) in affected files Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Fix Toast.js missing from git, CoordinatesDiv z-index, Legend duplicate ID, topBar padding - Add Toast.js to version control (was untracked, causing webpack module error) - Bump CoordinatesDiv z-index from 20 to 1001 (was hidden behind splitscreens children after z-index:auto change) - Fix Legend duplicate ID: separated tool icon was #LegendTool, same as content container div, causing empty message to appear in button instead of panel - Add hasStatus class to #topBarMain when statusIndicator is active, setting #topBarTitleName padding-left to 0 Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Fix Legend empty message: scope selector to content container via targetId Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Move Toast.js to design-system, fix --color-a3 text contrast, update AGENTS.md - Move Toast.js from UserInterface_/components/Toast/ to design-system/components/Toast/ (generic component belongs in design-system, not MMGIS-specific UserInterface_) - Update all 11 Toast import paths to new location - Bump --color-a3 in 5 dark themes to pass WCAG AA 4.5:1 contrast for text: Dark Default #747c81→#81888d, Dark Blue #64748b→#738399, Dark Warm #8b7a5e→#908064, Dark Mars #8a6a60→#98796f, Dark Midnight #606088→#7a7a9e - Update AGENTS.md: document design-system/ vs UserInterface_/ distinction in project structure and Key Directories Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Fix topBarTitleName padding override: increase specificity and add !important Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Fix IdentifierTool deactivation: update icon ID reference in separateFromMMWebGIS Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Hide toolPanelDrag when no tool is open - Add setToolPanelDragVisible(false) to closeActiveTool() (was only in makeTool toggle-off path) - Also guard drag handle display on isOpen (toolPanelWidth > 0) as safety net Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Add hover effect to MMGIS logo (opacity + brightness transition) Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Fix MMGIS logo hover: keep full opacity, use subtle background highlight instead Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Selective tile fade: fade on pan/zoom, instant on refresh/reload - Remove blanket _fadeAnimated toggle from toggleTimeUI (was killing fade for all tiles while TimeUI was open) - Monkey-patch GridLayer.redraw, TileLayer.setUrl, and GridLayer._tileReady to suppress fade via a transient _suppressTileFade map flag - Set _suppressTileFade in reloadTimeLayers for time-driven reloads - Flag auto-clears after 300ms so pan/zoom tiles still get the nice fade - Install pbf dependency (required by CesiumMVTLayer from #942 merge) Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Per-layer fade control: time-enabled + shade/viewshed layers never fade - Replace transient map-level _suppressTileFade with per-layer _noFade flag - Patch GridLayer._tileReady to check _noFade on the layer instance - Set _noFade on time-enabled tile layers and data/GL layers at creation - Set _noFade on Shade and Viewshed tool GL layers - Non-time-enabled base imagery tiles still fade normally on pan/zoom Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * fix: mobile UI improvements — move hamburger to right menu, show panel toggles, isMobile-driven toolbar layout, desktop-matching scalebar/compass - Remove left hamburger menu (#topBarMenu), move BottomBar items into top-right kebab dropdown menu for both mobile and desktop - Show panel toggles (Viewer/Map/Globe) and account/login UI in mobile topbar's #topBarRight - Move toolbar horizontal layout CSS from @media breakpoints to UserInterfaceMobile_.css (loaded only when isMobile flag is true) - Remove #mapTopBar @media rule from mmgis.css, add to mobile CSS - Remove mobile-only simplified scalebar rendering; use full desktop scalebar with both large and small axes on all viewports - Remove display:none on .leaflet-control-scalefactor in mobile CSS - Remove #loginDiv display:none from mobile CSS (React overlay handles it) - Simplify BottomBarReact container styles (no more absolute positioning) Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * chore: bump version to 4.3.28-20260430 [version bump] * fix: mobile toolbar 40px height, bottomFloatingBar flush, timeUI toggle, scalebar position, hide hotkeys - Toolbar height 40px, toolButton width 40px, no border-bottom - toolcontroller_incdiv: no padding-bottom, overflow-y hidden - bottomFloatingBar: no border-radius, left/right/bottom = 0 - Add MobileTimeUIToggle button on far right of toolbar - Hide Keyboard Shortcuts from kebab menu on mobile - Fix scalebar positioning (remove top:48px override in UserInterfaceBridge) - Set mobileTopSize/topSize to 40 (splitscreens top = 40px, not 50px) Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * fix: mobile topBar padding-left 34px Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * fix: update MobileCoordButton topBar paddingLeft from 80px to 34px Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * fix: MobileTimeUIToggle — inline toggle logic, float right, hide from settings on mobile - Replace broken Coordinates.toggleTimeUI() call with direct jQuery/store toggle - Float time button right in toolbar - Hide Time UI toggle from settings modal on mobile (toolbar has it) Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * fix: push scalebar/compass/scale up 40px on mobile, keep #timeUI in DOM - BottomElementPositioner: position mapToolBar, leaflet-bottom-left/right 40px above bottom on mobile (above toolbar) - Stop removing #timeUI from DOM on mobile so MobileTimeUIToggle works Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * fix: mobile TimeUI — only show endtime, always expanded - Hide #mmgisTimeUIStartWrapper and StartWrapperFake on mobile via CSS - Force expanded state (addClass expanded + show) when toggling TimeUI on - CSS ensures #timeUI.active always shows expanded content on mobile Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * fix: mobile TimeUI opens in tool panel with header, end time, expanded rows - MobileTimeUIToggle now opens/closes the tool panel via ToolController_ - Closes any active tool before showing TimeUI - Forces expanded state when opening - CSS hides start time inputs, positions expanded content properly - Overrides absolute positioning of expanded content for tool panel flow Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * feat: rewrite separated tools system from jQuery to React components - Add separatedToolsList/activeSeparatedTools state to Zustand uiStore - Rewrite SeparatedTools.jsx with glassmorphism panels, CSS Module styling - Replace SepToolsContainer (setInterval hack) with SepToolButton/SepToolsSection - Remove ~170 lines of jQuery DOM construction from ToolController_.js - Fix hardcoded rgba(26,26,27,0.88) to theme-aware var(--color-a-rgb) - Remove separated tool entries from themeApplier.js - Remove separated tool overrides from FloatingElements.css - Move Legend CSS overrides from Toolbar.module.css to SeparatedTools.module.css - Remove jQuery active-state manipulation from IdentifierTool.js - Add store sync in Map_.js displayOnStart logic - Preserve all DOM IDs for backward compatibility (mmgisAPI, tool make()) Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * chore: bump version to 4.3.28-20260501 [version bump] * fix: TimeUI mobile checks — use Zustand store instead of L_.UserInterface_ L_.UserInterface_ is null when TimeUI.init() runs (TimeControl.init is called before L_.link sets UserInterface_). All 16 isMobile checks now read from useUIStore.getState().isMobile which is set at startup. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * chore: bump version to 4.3.29-20260501 [version bump] * fix: move displayOnStart logic from Map_.js to ToolController_.finalizeTools() - Map_ no longer references specific tools (LegendTool) - displayOnStart is now handled generically for all separated tools - Added DOM element polling (tryMake) to handle React render timing Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * revert: remove all TimeUI-related mobile changes Reverts TimeUI.js and BottomBar.js to development base. Restores #timeUI DOM removal in UserInterfaceBridge.fina(). Removes MobileTimeUIToggle component from Toolbar.jsx. Removes TimeUI mobile CSS overrides from UserInterfaceMobile_.css. Non-TimeUI refinements (toolbar height, scalebar positioning, etc.) preserved. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * simplify: remove DOM polling, use simple setTimeout(0) for auto-open LegendTool handles its own content lifecycle via subscribeOnLayerToggle. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * feat: mobile TimeUI — fix isMobile detection, staging container, toolbar toggle - TimeUI.js: import useUIStore and replace all 16 L_.UserInterface_?.isMobile checks with useUIStore.getState().isMobile (L_.UserInterface_ is null when TimeUI.init() runs, so mobile conditionals were dead code) - TimeUI.js: stage mobile #timeUI in hidden #timeUIMobileStaging instead of placing directly in #tools (which gets cleared by other tools) - UserInterfaceBridge.js: stop removing #timeUI from DOM on mobile - Toolbar.jsx: add MobileTimeUIToggle that moves #timeUI between staging and #tools, opens/closes tool panel via ToolController_ - BottomBar.js: hide TimeUI toggle from settings modal on mobile Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * fix: rescue #timeUI back to staging when another tool opens Subscribe to activeToolName changes — when a tool becomes active while TimeUI is showing, move #timeUI back to #timeUIMobileStaging before the new tool's make() clears #tools. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * chore: remove separatedTool/justification config toggles, fix review issues - Remove separatedTool checkbox and justification dropdown from Legend and Identifier config.json (these are always separated, not configurable) - Remove justification property/code from LegendTool.js, IdentifierTool.js - Simplify Globe_.js separated tool count (no justification filter) - Remove justification from Reference-Mission config blueprint - Update LegendTool help docs and Legend.md documentation - Add --color-a-rgb fallback (29,31,32) in SeparatedTools.module.css - Add display:none !important to .panelIdentifier to prevent 12px gap - Update e2e test comment Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * fix: circular import in TimeUI.js, toolbar/bottomFloatingBar position sync - TimeUI.js: replace top-level useUIStore import with lazy _getUIStore() accessor to avoid 'Cannot access useUIStore before initialization' circular import error at _remakeTimeSlider - SplitScreens.jsx: skip #timeUI reparenting observer on mobile (mobile uses MobileTimeUIToggle to manage #timeUI placement in #tools) - BottomElementPositioner.jsx: unify mobile transition to 0.3s (matches toolsWrapper and toolbar), guard pxIsTools against undefined - Toolbar.jsx: align toolbar transition to 0.3s ease-out Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * LegendTool fix empty message * chore: remove separated tools offset logic from Globe_.js Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * fix: skip _makeHistogram on mobile (no timeline slider, timestamps unset) _makeHistogram renders inside the timeline slider which doesn't exist on mobile. Without it, _timelineStartTimestamp is NaN, causing 'Invalid time value' RangeError at toISOString(). Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * fix: mobile TimeUI — populate expanded rows, fix Invalid date, fix panel height - TimeUI.js attachEvents: use _initialStart/_initialEnd on mobile (same as desktop) instead of L_.TimeControl_ which isn't set yet at init time. Fixes 'Invalid date' in start/end time inputs. - TimeUI.js fina: set expanded=true on mobile and call _populateExpandedRows() so year/month/day/hour rows actually render. Removed position:absolute and pointer-events:none overrides. - Toolbar.jsx: set tool panel height to 217px (TimeUI.height) instead of 45% viewport — matches actual TimeUI content height. - UserInterfaceMobile_.css: expanded content flows naturally (position:relative), hide start time inputs, allow overflow scroll, flex-wrap topbar. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * fix: mobile TimeUI justify-content center, restore toolbar border-bottom - Add justify-content: center to #mmgisTimeUIMain on mobile - Remove border-bottom: none override so toolbar keeps its default border Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * fix: mobile TimeUI overflow hidden, scalebar/compass fixed at 40px offset - #timeUI overflow-y: hidden (was auto, causing 2px scroll) - Scalebar/compass/map controls stay at fixed 40px offset (above toolbar) regardless of tool panel state — no longer shift up by pxIsTools Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Implement multi-tier knowledge architecture - Restructure AGENTS.md from 745 lines to 106 lines (Tier 1: essential context) - Create knowledge/ directory with 30+ wiki-style documentation files (Tier 2: deep knowledge) - Create knowledge/reference/ with 8 detailed reference files (Tier 3: lookup material) - Move AI-GETTING-STARTED.md and AI-DEVELOPMENT.md to knowledge/ - Update all file references in .specify/templates and blueprints - Create knowledge/README.md as the full knowledge base index - Create knowledge/reference/README.md as reference material index Three-tier knowledge discovery system: Tier 1: AGENTS.md (~106 lines) - scannable in <2 minutes Tier 2: knowledge/*.md - deep knowledge on architecture, tools, APIs, DB, infra Tier 3: knowledge/reference/*.md - coding conventions, API reference, troubleshooting Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * chore: bump version to 4.3.29-20260501 [version bump] * fix: mobile toolbar active button style matches desktop, fix icon alignment - All mobile toolbar buttons (ToolButton, MobileCoordButton, MobileTimeUIToggle) now use display:flex with align-items/justify-content center for proper vertical icon centering - MobileCoordButton: changed 'active' class to 'toolButtonActive' to match the global CSS active style (color-mmgis + color-i background) - Removed inline color overrides so CSS .toolButtonActive takes effect Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Add Devin knowledge notes from past MMGIS sessions Include curated lessons learned from past Devin sessions: - CI/CD: ignore build-arm64/amd64 failures, focus on required checks - Child sessions: no separate PRs when consolidating - ENV triple-update rule (.env, sample.env, ENVs.md) - Error handling: use logger with infrastructure_error for fatal startup errors - Path traversal security: stay within /Missions, handle subpath serving - Database initialization architecture and migration patterns - API authentication behavior across AUTH modes - Auto-generated MMGIS concept index Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * fix: mobile toolbar active button style, icon alignment, tool deactivation - Active toolbar buttons get desktop-matching margin (1px 0) and border-radius (8px) via .toolButton.toolButtonActive CSS rule - Removed line-height: 40px from .toolButton (flex centering handles vertical alignment, line-height was pushing icons up) - MobileCoordButton now watches activeToolName store and deactivates when another tool opens (fixes coords staying active) - MobileTimeUIToggle sets activeToolName='MobileTimeUI' when opening so coords/other buttons can detect it and deactivate - MobileTimeUIToggle clears activeToolName when closing - Both custom buttons skip self-deactivation via name check Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Fix cross-references: convert backtick refs to markdown links, add Devin knowledge notes Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * fix: mobile toolbar icon height 40px, button margins for active padding - #toolbar .toolButton i: height 40px fixes icon vertical alignment - #toolbar .toolButton: margin 0 2px gives spacing between buttons - #toolbar .toolButton.toolButtonActive: margin 1px 2px so active background has visual padding around the icon Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Rename knowledge/ to .knowledge/ for consistency with .specify/ convention Dot-prefix signals agent infrastructure (not source code), consistent with .specify/, .github/, .vscode/ conventions. All cross-references updated. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * fix: mobile toolbar icon line-height 40px, active button padding via height - Coord and TimeUI button <i> icons get line-height: 40px - Active buttons: height 34px (vs 40px toolbar) creates visual padding around the active background, centered by flex align-items - Buttons get margin: 0 1px for horizontal spacing Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Fix broken cross-reference: 06.2 -> 06.1-configure-rest-api.md Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * fix: close active tool + cancel deferred cleanup in MobileCoordButton/TimeUI - MobileCoordButton: call closeActiveTool() before opening, destroy _pendingCloseTool if set, increment _closeSeq to cancel deferred tools.innerHTML clear - MobileTimeUIToggle: same _pendingCloseTool + _closeSeq fix after closeActiveTool() to prevent 420ms deferred cleanup from wiping #timeUI after it's placed in #tools - Removed redundant closeActiveTool() from MobileCoordButton close path (was being called after destroy, not needed) Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * fix: active mobile toolbar buttons 34x34px (square) Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Drastically compress .knowledge/ — keep only unique agent content Remove 33 wiki files that duplicate docs/pages/ content. Remove 9 reference/ files derivable from source code. Keep only 5 files (down from 46): - AI-GETTING-STARTED.md (agent setup walkthrough) - AI-DEVELOPMENT.md (spec-kit workflow) - conventions-and-gotchas.md (naming, code style, common issues) - 12-devin-knowledge-notes.md (CI, auth, DB init, security gotchas) - README.md (index pointing to docs/pages/ for everything else) Principle: don't duplicate docs/ — only keep what's uniquely agent-optimized. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Rename to knowledge-notes.md, remove Devin branding and fork-specific CI section Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * fix: hide mmgis-map-logo on mobile Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Restore Database Safety Rules for AI Agents section in AGENTS.md Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * fix: shift compass and map scale 6px to the right (both mobile and desktop) Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Add back Important Instructions, code pattern templates, and detailed project structure - Important Instructions in AGENTS.md: MCP tools, hot-reload, Reference Mission - .knowledge/code-patterns.md: full directory tree with key directory annotations, plus copy-paste templates for Express routes, Sequelize models, Tool plugins, and WebSocket handlers Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Update project structure trees to reflect current filesystem Add missing directories: tests/, .knowledge/, .specify/, .github/, views/, private/, spice/, build/, examples/, scripts/middleware.js. Both abbreviated (AGENTS.md) and detailed (.knowledge/code-patterns.md) trees now match the actual repo layout. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * chore: bump version to 4.3.30-20260501 [version bump] * Add Layers_.js to project structure (key singleton L_) Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Fix project structure: correct API layout, frontend modules, code templates API/Backend/ uses feature-domain modules (Draw/, Users/, Config/, etc.) with setup.js + routes/ + models/ per feature — not APIs/ or Databases/. Frontend essence/ has Components/, Helpers/, LandingPage/, mmgisAPI/, services/ — not Ancillary/. Basics/ includes all singletons (Globe_, Formulae_, ToolController_, Viewer_, ComponentController_, Test_). Code templates updated to match actual patterns (setup.js, module.exports). Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * refactor: remove test infrastructure (Test_ module, testModules, DrawTool.test) - Delete src/essence/Basics/Test_/ directory - Delete src/essence/Tools/Draw/DrawTool.test.js - Remove Test_ import and Shift+T keydown handler from essence.js - Remove tests key from Draw tool config.json - Remove testModules generation logic from API/updateTools.js Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * chore: bump version to 4.3.31-20260501 [version bump] * style: move Cesium link button to top-right and match Leaflet zoom button styling - Change control container from top-left to top-right positioning - Update button size from 26px to 30px to match Leaflet zoom controls - Use CSS variables (--color-a, --color-f, --color-mmgis) instead of hardcoded colors - Add border-radius and box-shadow matching Leaflet control appearance - Update hover/inactive states to use themed colors Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * fix: anchor map logo to viewport instead of Leaflet map panel - Change MapLogo parent from .leaflet-bottom.leaflet-right to #main-container - Switch CSS position from absolute to fixed for viewport anchoring - Add explicit bottom-offset positioning in BottomElementPositioner (desktop) - Add explicit bottom-offset positioning in BottomElementPositioner (mobile) - Logo stays at viewport right edge regardless of open side panels - Retains smooth bottom offset transitions when bottom bar appears Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * docs: remove references to deleted test infrastructure (Test_, DrawTool.test) - Remove Test_/ from project structure in .knowledge/code-patterns.md - Remove DrawTool.test.js references from specs/006 spec, plan, and tasks - Remove Draw Tool Testing section from tasks.md Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * chore: bump version to 4.3.32-20260501 [version bump] * fix: append logo to document.body to avoid filter containing block #main-container has a CSS filter property which creates a new containing block per the CSS spec, causing position:fixed to behave like absolute. Appending to document.body ensures true viewport-fixed positioning. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * fix: prevent mobile topBarTitleName text wrapping by replacing max-width with white-space: nowrap Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * chore: bump version to 4.3.33-20260501 [version bump] * chore: bump version to 5.0.0 and update changelog Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * refactor(ui): move Screenshot/Fullscreen to BottomBar, About to TopBar kebab TopBar kebab menu now contains only Keyboard Shortcuts, Settings, and About (About now shows on both desktop and mobile). BottomBarReact now renders Screenshot, Fullscreen, and Copy Link buttons (top to bottom) following the same IconButton + Tooltip pattern. The About button has been removed from BottomBarReact. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * chore: bump version to 5.0.1-20260505 [version bump] * feat(mobile): enforce exclusive panel toggling on mobile in TopBar Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * chore: bump version to 5.0.1-20260505 [version bump] * style: reposition LithoSphere globe controls to match Leaflet/Cesium theme Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * chore: bump version to 5.0.1-20260505 [version bump] * feat(topbar): hide Viewer/Globe toggles based on configured panels Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * style(bottombar): reorder buttons (Copy Link, Screenshot, Fullscreen) and unify size Reorder the BottomBarReact buttons top-to-bottom to: Copy Link, Screenshot, Fullscreen. Move the 24x24 button sizing from the #topBarLink id selector in mmgis.css into the .barButton class in BottomBarReact.module.css so all three buttons share the same compact size as the original Copy Link button. Drop the now redundant #topBarLink rule. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * style(bottombar): increase padding-bottom to 12px and button margin to 3px 0 Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * style: rearrange globe controls — compass top-right circular, nav row, vertical column, panels open left Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * chore: bump version to 5.0.2-20260505 [version bump] * chore: bump version to 5.0.2-20260505 [version bump] * style: anchor observe settings panel right:34px and float nav hover panels Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * feat(theming): add 5 new themes, --color-shadow variable, and configure ThemePreview - Add Dark Terra, Dark Nebula, Dark Lunar, Dark Supernova, Light Botanical themes - Add --color-shadow CSS variable to every theme + :root fallback - Replace hardcoded rgba shadow colors with var(--color-shadow) in TopBar, Toolbar, SeparatedTools, ToolPanel, FloatingElements, Dropdown, Modal, and SplitScreens - Add Custom shadowcolor color picker in tab-ui-config and apply it via Stylize - Add ThemePreview component (configure/src) wired through Maker.js as a new 'themepreview' row type so the configure UI shows a live mini mockup of the selected theme Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * chore: bump version to 5.0.2-20260505 [version bump] * fix(configure/ThemePreview): tighten top spacing and live-preview Custom theme - Pull the preview up by 12px so the gap below the theme dropdown is tighter. - Read the Custom color pickers (look.primarycolor / secondarycolor / tertiarycolor / accentcolor / shadowcolor / topbarcolor / toolbarcolor / mapcolor) from the configuration and overlay them on Dark Default so the preview reflects Custom theme edits live, matching Stylize.js's runtime behavior. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * chore: bump version to 5.0.3-20260505 [version bump] * feat(themes): add Dark Heliosphere, Dark Monokai, and Light Solarized - Dark Heliosphere: deep night purple surface with corona-orange accent. - Dark Monokai: warm graphite surface with lime accent (Monokai-inspired). - Light Solarized: classic solarized base3/base02 with blue accent. Mirror added to configure/src/themes/themes.js for the ThemePreview, and the three names appended to the Color Theme dropdown options. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * fix(coordinates): respect time.initiallyOpen when live deep-link is set * chore: bump version to 5.0.3-20260505 [version bump] * refactor(theming): remove Custom theme + per-field color overrides - Drop the 'Custom' option from the Color Theme dropdown. - Remove all Custom Color Options (look.primarycolor, .secondarycolor, .tertiarycolor, .accentcolor, .bodycolor, .topbarcolor, .toolbarcolor, .mapcolor, .hightlightcolor, .shadowcolor) from tab-ui-config.json. - Strip the matching DOM/CSS-variable override block from Stylize.js; Stylize now just applies the selected preset theme (and the page logo). - Drop the empty bodycolor/topbarcolor/toolbarcolor/mapcolor/shadowcolor defaults from API/templates/config_template.js. - Simplify ThemePreview to render the selected preset directly — no Custom branch, no overlay logic. Preset themes cover all the looks we want and keep the configure surface much smaller. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * style(time-ui): round corners on TimeUI shell, action wrappers, mode dropdown - #timeUI: 10px border-radius on the outer time control bar. - #mmgisTimeUIActionsLeft / #mmgisTimeUIActionsRight: 10px border-radius so the action clusters sit as rounded chips. - #mmgisTimeUIActionsRight > div (excluding #mmgisTimeUIPresent): 10px border-radius on each action button so they match the wrapper. - #mmgisTimeUIModeDropdown: 40px height + 10px border-radius to align with the rest of the bar; clear the dropy default border-color so the rounded edge isn't outlined. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * chore: bump version to 5.0.4-20260505 [version bump] * feat(configure): mark light themes as (experimental) in dropdown label Light themes still have outstanding contrast issues, so flag them in the Color Theme dropdown without changing the saved value. - Maker dropdown now accepts options as either a plain string (current behavior) or { value, label } so the rendered label can differ from the persisted value. - tab-ui-config switches the six light themes to { value, label } form with '(experimental)' appended to the label only. Existing mission configs that already saved 'Light Default' etc. continue to match. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Fix timeUI border radius * fix(mobile): rescue #timeUI before tool make() destroys it Clicking Layers -> Time -> Layers -> Time on mobile caused the bottom panel to render LayersTool content with TimeUI height. The #timeUI DOM element was destroyed when LayersTool.make() called $('#tools').empty(), before the async React useEffect in MobileTimeUIToggle could rescue it to its staging container. - ToolController_.makeTool: synchronously move #timeUI from #tools back to #timeUIMobileStaging (and reset TimeUI store flags) on mobile, before invoking the new tool's make(). - MobileTimeUIToggle.handleClick: defensive fallback that re-initializes TimeUI if #timeUI no longer exists when the toggle is activated. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * fix(mobile): move re-initialized #timeUI from staging into #tools TimeUI.init() on mobile appends the new #timeUI to the hidden #timeUIMobileStaging container, so the fallback branch must also move it into #tools — otherwise the user sees an empty tool panel after the destroyed-element recovery path. Caught by Devin Review. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * fix(mobile): preserve #timeUI when Coordinates tool empties #tools On mobile, opening or closing the Coordinates tool runs $('#tools').empty() inside interfaceWithMMWebGIS / separateFromMMWebGIS. After the previous PR commits, clicking Coordinates -> Time still left the bottom panel empty because: - Coordinates.make() empties #tools while #timeUI is in staging (fine on its own), but the Coordinates teardown that fires after the user switches to the Time toggle (via MobileCoordButton's useEffect on activeToolName change) calls Coordinates.destroy() -> separateFromMMWebGIS(), which empties #tools wholesale and destroys the freshly-placed #timeUI. Add a rescueMobileTimeUI() helper that moves #timeUI from #tools back to #timeUIMobileStaging before each tools.empty() call in Coordinates, mirroring the rescue already done in ToolController_.makeTool(). Coordinates -> Time now correctly shows the TimeUI. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * fix(mobile): harden TimeUI fallback recovery (call fina(), de-dupe popovers) Devin Review correctly flagged that the safety-net path in MobileTimeUIToggle.handleClick was producing a partially-broken TimeUI when it fired: - TimeUI.init() unconditionally appends a new #timeUIPlayPopover_global to <body>, so a second init() left two elements with the same id. - TimeUI.init() alone does not wire up date pickers or per-button click handlers — that's TimeUI.fina()'s job. Without fina(), the recovered TimeUI rendered visually but Play / Previous / Next / Fit / Follow / Present / Expand were all dead. Before re-initializing, remove the stale #timeUIPlayPopover_global and #timeUIQuickSelectPopover_global divs to avoid duplicate ids. After the new #timeUI is moved into #tools, call TimeUI.fina() to populate the date pickers, attach the button click handlers, build the histogram, and populate the expanded mobile rows. Some delegated body/document handlers in attachEvents() will still be duplicated on this path; that is acceptable for a degraded recovery that should never run in practice now that the primary rescues in ToolController_.makeTool() and Coordinates.js cover all known paths. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * chore: bump version to 5.0.5-20260505 [version bump] * fix(mobile): Coordinates teardown only removes its own DOM The previous Coordinates fix was racing with itself: after the Time toggle synchronously moved #timeUI into #tools, MobileCoordButton's useEffect (triggered by the activeToolName change) ran on the next React tick and called L_.Coordinates.destroy(). That called separateFromMMWebGIS(), whose rescue moved #timeUI right back into the hidden staging div before tools.empty() — so the bottom panel ended up empty even though the time toggle was 'active'. Make separateFromMMWebGIS selective: only remove the Coordinates-specific DOM (#coordUIHeader and #CoordinatesDiv) instead of wiping all of #tools. Any other content already in #tools (e.g. #timeUI placed there by the Time toggle) is left alone. interfaceWithMMWebGIS still keeps the rescue + tools.empty() pattern on the open path so Coordinates always starts from a clean panel. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Bump DrawTool Temporal Drawings upward * chore: bump version to 5.0.6-20260505 [version bump] * chore: reset version to 5.0.0 Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * test(e2e): fix 9 pre-existing failures (test-only changes) - mmgis-api.spec.js: add form-fill login under AUTH=local; serialize describe to avoid concurrent-login race in the session store - coordinates.spec.js: TimeUI toggle was moved from the coordinates bar to the Settings modal; navigate via topbar kebab menu and assert the checkbox is rendered - widgets.spec.js: target .leaflet-control-zoom-in/-out specifically; the bare .leaflet-control-zoom class is also used by the home/reset control, so the original assertion was always false - sites.spec.js: scope panel selector to #toolPanel; both the toolbar icon and the panel container share id="SitesTool" Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * chore: bump version to 5.0.1-20260505 [version bump] * Revert "chore: bump version to 5.0.1-20260505 [version bump]" This reverts commit 4880204c1163be5d1d7fa96d14a0ed018c6f586c. * fix: prevent filter operator dropdown clipping in Layers panel Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * chore: bump version to 5.0.1-20260507 [version bump] * revert: keep dropy openUp:true for operator dropdowns Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Revert "chore: bump version to 5.0.1-20260507 [version bump]" This reverts commit d67c369ed437e47d658ae051348d377978dc48ed. * chore: bump version to 5.0.1-20260507 [version bump] * Revert "chore: bump version to 5.0.1-20260507 [version bump]" This reverts commit 29565ed829a55e9c241a789c9a3901d11cb5ca67. * chore: bump version to 5.0.1-20260507 [version bump] * Revert "chore: bump version to 5.0.1-20260507 [version bump]" This reverts commit 50e357604ebe9378564619b34c508b63cfb62c1d. * chore: bump version to 5.0.1-20260507 [version bump] * chore: bump version to 5.0.2-20260511 [version bump] * fix: render Globe panel immediately on first open without window resize Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * chore: bump version to 5.0.3-20260511 [version bump] * feat: add theme borders to panels and gradient backgrounds to splitters Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * chore: bump version to 5.0.4-20260511 [version bump] * style: bump split shadow gradient opacity to 0.4 Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * style: hotkeys modal 3-col grid + smaller leaflet zoom button gap Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * style: prevent hotkey label/value wrapping (ellipsis instead) Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * style: hotkeys modal single column, no wrap, no truncation Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * chore: bump version to 5.0.4-20260511 [version bump] * style: hotkeys modal dividers, invert title/subtitle colors, rename title, margin above subtitles Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * style: move splitter gradient to themed CSS class, restore hover feedback Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * chore: bump version to 5.0.5-20260511 [version bump] * style: hotkeys section titles use --color-h (matches rest of app) Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * chore: bump version to 5.0.5-20260511 [version bump] * fix: guard Globe_.init() inside rAF to prevent double instantiation Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * chore: bump version to 5.0.6-20260512 [version bump] * feat(plugins): per-plugin deps, lazy tool loading, validation, shared discovery Phase 3 — Plugin config validation + override warnings: - New API/pluginValidation.js with validatePluginConfig() for tool, component, and backend manifests. Validates required fields (name, paths), object/string shape of paths, dependencies block (npm/python.pip/python.conda), and warns on unknown top-level fields. - updateTools()/updateComponents() now skip invalid plugins and emit override warnings (matching what components already logged for tools). Phase 2 — Shared discoverPlugins() utility: - New API/pluginDiscovery.js consolidates the duplicated scanning logic from updateTools(), updateComponents(), and getBackendSetups(). Supports exact- name and substring container patterns, JSON/require/no-op loaders, and skips dot/underscore-prefixed dirs. - updateTools.js and setups.js refactored on top of the shared helper. Phase 1 — Per-plugin dependency declaration + build-time aggregation: - Plugin config.json may now declare a 'dependencies' block (npm + python.pip + python.conda). validatePluginConfig() also validates this shape. - New scripts/resolve-plugin-deps.js scans every tool/component/backend plugin and writes plugin-package.json, plugin-python-requirements.txt, and plugin-conda-deps.txt. Detects version conflicts and fails loudly. - scripts/build.js calls resolvePluginDeps() before updateTools(). - Dockerfile installs the aggregated plugin npm and pip deps after the root npm ci, using --no-save / --no-package-lock / --ignore-scripts so the root lockfile is untouched. - Animation tool migrated: ffmpeg/gifshot/html2canvas now declared in its config.json (kept in root package.json for transitional compat). - Generated artifacts gitignored. Phase 4 — Lazy loading of tool bundles: - updateTools() now emits dynamic-import arrow functions in the generated src/pre/tools.js with webpackChunkName hints so each tool is split into its own chunk (Kinds stays static because it's required synchronously). - ToolController_ gains ensureToolLoaded(name) and getLoadedTool(name) helpers and makeTool is async; init/finalizeTools and the separated-tool auto-open flow are updated to handle lazy modules. - Toolbar.jsx, SeparatedTools.jsx, SitesTool.js, and Layers_.js migrated to resolve LayersTool/etc. via the new helpers instead of poking toolModules directly. Tests & docs: - tests/fixtures/test-plugin-tools/{TestPlugin,InvalidPlugin,OverridePlugin} + tests/helpers/plugin-helpers.js with install/uninstall helpers. - New unit specs: pluginValidation, pluginDiscovery, updateTools, resolvePluginDeps, toolLazyLoading (57 tests, all passing). - CONTRIBUTING.md and docs/pages/Contributing/Contributing.md updated with schema, override behaviour, dependency declaration, build-time aggregation, conflict detection, and Docker integration. * chore: bump version to 5.0.7-20260512 [version bump] * fix: make Globe_.init() idempotent against multi-init Globe_.init() previously constructed a fresh GlobeRenderer on every call, which after #71 could happen multiple times for a single toggle (uiStore setTimeout + TopBar rAF). Each extra construction appends another .cesium-widget / _lithosphere_scene to #globe and leaves event handlers wired to dereferenced renderer state, which has been observed to break LithoSphere globe control buttons on configurations where the globe panel starts closed at boot. Add a top-of-init() guard that bails out and calls invalidateSize() when a renderer already exists. Single small, surgical change; no behavior change for the !L_.hasGlobe mock-swap path or for first-time construction. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * chore: bump version to 5.0.7-20260512 [version bump] * test(plugins): generate src/pre/tools.js on demand in toolLazyLoading spec The Playwright unit-tests CI step runs before `npm run build` so the gitignored `src/pre/tools.js` artifact does not yet exist on disk. Add a beforeAll hook that invokes `updateTools()` to regenerate it when missing, keeping the spec self-contained on both CI and dev machines that already built locally. * fix(tools): defensive getTool() + preload flag for cross-referenced tools Devin Review flagged a behavioural regression introduced by Phase 4: `ToolController_.getTool(name)` previously always returned a method- callable object (real module or `{ use(){} }` stub) because every tool was statically imported. After Phase 4, unresolved lazy loaders are `() => import(...)` functions, so callers like `Map_.getTool('InfoTool').use(...)`, `mmgisAPI.getTool('DrawTool').filesOn`, and `LegendTool` calling `LayersTool.populateCogScale` would crash with TypeError until the target tool was opened. Two fixes: 1. **Defensive getTool()**: Returns the legacy fallback stub when the tool module is still a lazy-loader function, and fires off `ensureToolLoaded(name)` in the background so subsequent calls see the resolved module. Prevents all crashes immediately. 2. **`preload: true` config flag**: Tools reached synchronously from other code paths (Info, Draw, Layers, Chemistry) now declare `"preload": true` in their `config.json`. `ToolController_.init()` calls `preloadEagerTools()` which fires `ensureToolLoaded` for every such tool right after toolbar setup — the chunks download in parallel with the rest of the page becoming interactive, so by the time a user clicks a feature the InfoTool module is already resolved. `validatePluginConfig` now accepts `preload` as a known tool field; CONTRIBUTING.md and docs/pages/Contributing/Contributing.md updated to document when to set it. Added a unit test c…
* Sync toggleTimeUI DOM state to Zustand store
toggleTimeUI() now calls setTimeUIActive() and setTimeUIExpanded()
so BottomFloatingBar visibility and BottomElementPositioner offsets
reflect actual TimeUI state.
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* Restore #toggleTimeUI element in Coordinates markup
The element was accidentally dropped during the folder restructure move.
TimeUI.js and DrawTool.js check $('#toggleTimeUI').hasClass('active')
to gate histogram rendering and time-filter toggle visibility.
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* Fix #toggleTimeUI: restore click handler, tippy, active class; remove redundant jQuery positioning
Restored pieces lost during folder restructure:
- Click handler in init() and off handler in remove()
- Tippy tooltip for the time toggle button
- display:none when time is not enabled
- $('#toggleTimeUI').toggleClass('active') so TimeUI.js can check it
- $('#CoordinatesDiv > #toggleTimeUI').remove() on mobile
Removed jQuery CSS positioning from toggleTimeUI() since
BottomElementPositioner now reactively handles all bottom-anchored
element offsets via the Zustand store.
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* Fix CurtainTool.destroy() using undefined ReactDOM.unmountComponentAtNode
Use the stored _reactRoot.unmount() instead, matching the React 18
createRoot pattern already used in make().
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* Fix tooltips, scale indicator position, modal blur, help close button
Tooltips:
- Reduce Base UI Tooltip delay from 600ms (default) to 200ms
- Restyle tooltip popup to match tippy blue theme (var(--color-c2))
- Add Tooltip wrappers to TopBar panel toggles (Viewer/Map/Globe)
- Wrap Toggle with forwardRef so Tooltip render prop can attach ref
- Remove title attrs that conflicted with custom tooltips
Scale indicator:
- Remove scalefactor-specific positioning from BottomElementPositioner
(it moves naturally with .leaflet-bottom.leaflet-left container)
- Position scalefactor to the left of compass at same bottom level
Modal blur:
- Call _applyBlur() immediately when marking modal as closing
so blur clears during fade-out instead of persisting 500ms
Help modal:
- Add close (X) button in title bar matching other modal patterns
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* Remove dead CSS: delete tools.css, clean ~600 lines from mmgisUI.css and mmgis.css
- Delete tools.css entirely (both selectors #CurtainToolList and
.searchToolSelect are unreferenced anywhere in the codebase)
- Remove from mmgisUI.css: .mmgisRadioBar3/4/Vertical (140 lines),
.mmgispureselect (104 lines), blink/condemned_blink_effect (38 lines),
.slidecontainer/.slider (41 lines), .ar_slider (91 lines),
.verticalSlider (91 lines), .mmgisMultirange_elev (19 lines),
.ui-corner-all/bottom/right/br (9 lines)
- Remove from mmgis.css: #nodeenv, empty #topBar{}, #topBarInfo,
#topBarHelp, #topBarFullscreen, #toggleUI, #logoGoBack
- Keep #topBarLink (used in BottomBarReact.jsx), #webgl-error-message
(used by vendored THREE.js)
- All selectors verified with repo-wide grep before removal
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* UI fixes: tooltips, splitter hover, mobile toolbar, color schemes
- Replace Base UI Tooltip with simple React portal tooltip (200ms delay,
tippy-matching style) — fixes missing tooltips for toolbar/topbar/bottom buttons
- Add cursor + hover highlight to vertical splitters (was missing because
module CSS didn't inherit global .splitterV styles)
- Add hover highlight to tool panel drag handle
- Remove mdi-drag-vertical icon from tool panel drag
- Add mobile toolbar horizontal layout via @media query overrides
- Add 4 new color schemes: High Contrast (a11y), Dark Mars, Dark Midnight,
Light Warm (total: 10 themes)
- Previous fixes also included in working tree:
- timeUI border moved to toolsWrapper border-bottom (conditional)
- #toggleTimeUI button removed entirely
- CoordinatesDiv: vertical centering, unified background, 12px right offset
- barBottom padding-bottom: 8px
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* Fix assignment operator used instead of comparison in TimeControl.fina()
Pre-existing bug: `TimeControl.enabled = true` was assigning instead of
comparing. Changed to `TimeControl.enabled === true`.
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* Restructure Configure page UI tab: add all themes, Custom mode, enableWhenField
- Added all 10 theme presets to dropdown (was missing Dark Mars, Dark Midnight,
Light Warm, High Contrast)
- Added 'Custom' option: skips preset theme, uses only color picker values
- Moved Theming section directly under Rebranding
- Nested 'Custom Color Options' under Theming with subdescription
- Added enableWhenField support to Maker.js: disables color pickers unless
theme is set to Custom
- Renamed color options with clearer names and improved descriptions:
Primary → Surface Color, Secondary → Deep Background Color,
Tertiary → Text Color, Body → Page Body Color, Highlight → Feature Highlight
- Stylize.js: skip setTheme() when theme is 'Custom'
- Rebuilt configure page
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* Revert tooltips to tippy.js, fix dropdown menus, redesign About modal
1. Tooltip: Replaced custom React portal tooltip with tippy.js wrapper.
Uses the existing tippy.js dependency and 'blue' theme for consistency.
2. Dropdown: Replaced Base UI Menu with native portal dropdown.
Base UI's nested Menu.Trigger + BaseButton composition was swallowing
click events, breaking userAvatar and menuBtn menus. New implementation
uses simple state + createPortal with proper outside-click dismissal.
3. About modal: Professional redesign with centered MMGIS ASCII art header,
proper GitHub SVG logo link, clean metadata section, centered link
buttons, attributions section, and NASA-AMMOS footer.
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* Restore .mmgisHelpButton base styles lost during Help.css module migration
The global .mmgisHelpButton styles (yellow color, compact 18x18px sizing,
0.7 opacity) were removed when Help.css was converted to Help.module.css.
Since Help.getComponent() emits raw HTML strings for jQuery-rendered tool
headers, it cannot use CSS Module scoped classes. Restored the base styles
in mmgis.css alongside the related .mmgisToolHelpBtn definition.
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* Fix session logout regression, About modal refinements, High Contrast theme, Stylize.js, Default Tool config
- Login: skip session.regenerate() for token-based re-auth (useToken:true)
so reloading the main page no longer invalidates the configure page session
- About modal: replace ASCII art with mmgis.png logo, rename Attributions to
Map Layer Attributions, remove footer logo, link NASA-AMMOS to ammos.nasa.gov
- High Contrast theme: change accent from #ffff00 to #ffd700 (gold) for better
contrast ratios against dark backgrounds
- Stylize.js: color overrides only apply when theme is Custom or unset,
preventing preset themes from being clobbered by stale config values
- Restore Default Tool config section in tab-ui-config.json (accidentally
removed during Theming section reorganization)
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* Restore defaulttooldropdown case handler in Maker.js
The case was accidentally removed during the Configure page UI tab
restructure (d7f96c50). Without it, the Default Tool dropdown in the
Configure page rendered as nothing despite the config referencing it.
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* High Contrast tooltip text, panel toggle styling, scale position swap
- High Contrast theme: tooltips now use black text on yellow background
via --color-c2-text variable (white for all other themes)
- About modal links use var(--color-f) for consistent theme text color
- Panel toggle buttons: 11px uppercase with 600 weight for better
visibility
- Mapping scale button moved to bottom-right of compass (was top-left)
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* Reposition Viewer and Globe panel buttons to top-right
- Viewer: dropdown selector at top-right edge, OSD buttons stacked
vertically below it; settings panel opens to the left
- Globe: home, exaggerate, observe, walk, link controls moved from
TopLeft to TopRight corner via addControl 4th arg
- Style consistency: OSD buttons and LithoSphere controls now match
Leaflet zoom controls (var(--color-a) bg, var(--color-f) text,
var(--color-mmgis) hover, 30px size, 3px border-radius)
- Viewer settings sliders use var(--color-a3) instead of hardcoded
#444444
- Az/el indicator stays at bottom center (exception per design)
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* Consistent modal theming + session security fix
Modal theming:
- All modals now share consistent styling: backdrop-filter blur, semi-transparent
background via --color-a-rgb, 10px border-radius, header divider line, box-shadow
- Updated: loginModal, Help, ConfirmationModal, Settings, Hotkeys, About modals
- Tool panel backgrounds changed from opaque var(--color-k) to transparent so the
ToolPanel's existing backdrop-filter effect shows through
- Legend tool header updated to match consistent 44px height with divider
- applyTheme.js now auto-derives --color-a-rgb from theme's --color-a hex value
- Modal service wrapper gets backdrop-filter: blur(12px)
Session security (Devin Review fix):
- Token re-auth now calls req.session.regenerate() with data preservation to
prevent session fixation while maintaining multi-tab compatibility
- Token is rotated via crypto.randomBytes on every re-auth (was being reused)
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* UI fixes: viewer settings, button sizing, menu contrast, coords, login header
1. Viewer OSD settings moved to top of button stack, panel opens downward
2. Overlay buttons consistent 30x30px (OSD line-height fix, home button)
3. Menu/icon contrast improved: Dropdown items and IconButtons use --color-a5
(was --color-a3) with --color-f on hover for better dark theme legibility
4. CoordinatesDiv fixed to 30px height, pickLngLat button centered
5. Login modal now has a header bar with 'Log In' title and close X button;
title toggles to 'Sign Up' when switching modes
Also reverts session regeneration for token re-auth (Devin Review feedback):
token-based re-auth now refreshes session data in-place without regeneration
or token rotation, preserving multi-tab compatibility.
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* UI polish: nav popover, sep tools, compass, zoom controls, status indicator, toast
1. Description nav popover: added z-index:9000 so menu appears above map panels
2. Separated tools: default color changed from accent to --color-f; fixed CSS
selector from .toolButtonSep to .toolSep to match actual class names
3. Compass + mapping scale shifted left by 30px for better positioning
4. Map zoom/home controls: use --color-f instead of accent --color-c to reduce
visual prominence; hover still highlights with accent color
5. Status indicators (reload/ws disconnect/layer update) moved from Leaflet
control to TopBar with soft pulsing fade animation and tooltip on hover
6. WebSocket retry toast: rounded corners, glass background with backdrop-filter,
border-left accent for failure state instead of solid red background
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* Fix 14 tool UI issues: headers, backgrounds, layout, functional bugs
Header alignment:
- LayersTool: align-items center on #filterLayers, gap between right icons
- InfoTool: align-items center on #infoToolHeader, 44px height
- ViewshedTool: align-items center, restructured header with left/right divs
- IsochroneTool: restructured flat header into nested mmgisToolHeader pattern
- ShadeTool: align-items center on #vstHeader children
Icon spacing:
- LayersTool: increased right margin to 28px + gap 2px
- ViewshedTool: #vstNew padding-right 30px (clear of close button)
- IsochroneTool: #iscNew padding-right 30px
Missing components:
- SitesTool: added Help import + help icon via mmgisToolHeader pattern
- AnimationTool: added full mmgisToolHeader with title and help icon
Background fixes:
- InfoTool: changed toolsContainer background from transparent to var(--color-a)
- DrawTool: changed toolsContainer background from transparent to var(--color-a)
Layout fixes:
- DrawTool: #drawToolContents top 81px, height calc(100%-81px), #drawToolNav margin-right 0
- MeasureTool: removed padding-left:0 override from mmgisToolHeader child selector
Functional fixes:
- InfoTool: updated jQuery selectors from #InfoTool to #toolButtonInfo (React toolbar IDs changed)
- CurtainTool: deferred OpenSeadragon init with requestAnimationFrame (React 18 async render)
- CurtainTool: curtainToolBar justify-content flex-end (icons at bottom)
Security:
- TopBar StatusIndicator: escape HTML in layer names to prevent XSS via addLayerQueue
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* Fix mapToolBar pointer events, login padding, default tool, About modal order
1. mapToolBar: set pointer-events:none on both #mapToolBar and direct children
so clicks pass through to the map; leaf elements still get auto via
.childpointerevents rule
2. #loginModalBody: padding changed to 40px 0px 0px
3. Default tool: deferred click to requestAnimationFrame so React toolbar
has rendered before getElementById runs
4. About modal: moved mainInfoModalCustom to right below mainInfoModalHero
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* Fix tool headers to 40px, ViewshedTool subheader, AnimationTool header, InfoTool close btn, statusIndicator position
1. All tool panel headers: changed from 44px to exactly 40px height
- Global .mmgisToolHeader and .mmgisToolTitle in mmgis.css
- InfoTool.css, ViewshedTool.css, IsochroneTool.css, ShadeTool.css
2. LayersTool #filterLayers: height 40px, .right > div height unset,
.right margin-right 30px, .right > div margin 0px 3px
3. ViewshedTool: restructured header — title+help in mmgisToolHeader row,
vstToggleAll (left) and vstNew (right) on a new #vstSubHeader row below
4. AnimationTool: removed old #animationToolHeader CSS (padding 15px 20px,
white background, 18px font), now uses standard mmgisToolHeader class.
Fixed color from var(--color-a) (background) to var(--color-f) (text)
5. InfoTool close X: re-inject close button after use() rebuilds content
via TC_.injectCloseButton() (toolsContainer.empty() was removing it)
6. StatusIndicator: moved to left of topBarTitle in JSX render order.
Added align-items:center to #topBarMain for vertical alignment.
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* Remove title attr from StatusIndicator (conflicts with tippy tooltip)
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* Fix TimeUI dropdown z-index above tool panel, reposition toasts to top-center
1. TimeUI dropdowns: added z-index 10000 to all dropy content ul elements
so they render above the vertical tool panel (z-index 1400). Also set
timeUIDock to position:relative with z-index 10000 and overflow:visible.
2. Toast notifications: repositioned #toast-container from bottom-right to
top-center just below the topbar (top: 44px, centered with transform).
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* Fix StatusIndicator spacing, CurtainTool close btn, header title font consistency
1. StatusIndicator: use display:none/flex instead of opacity:0/1 so it
takes no space when there's no active status indicator.
2. CurtainTool: added close X button at top of curtainToolBar (matching
MeasureTool pattern) with flex spacer pushing other buttons to bottom.
3. Header title font consistency: InfoTool, ShadeTool, CurtainTool titles
now match mmgisToolTitle standard (font-weight:600, padding-left:10px,
height:40px for CurtainTool which was 34px).
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* Legend empty state, scalefactor position, sep-tool-header unbold
1. Legend: show 'No active layers with legends' when no legend items
are present. Also fixed container height calc(100% - 40px).
2. Mapping Scale (.leaflet-control-scalefactor): shifted 10px right
(left 26→36px) and 1px down (bottom 30→29px).
3. sep-tool-header span: font-weight changed from 600 to 400.
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* Guard Legend empty state message to only show when panel is active
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* Fix TimeUI dropdown covered by toolPanel: remove splitscreens stacking context
#splitscreens had z-index:1 which created a stacking context, confining
its children (including bottomFloatingBar at z-index:1500) to that context.
Since ToolPanel (z-index:1400) was a sibling outside splitscreens, it
painted above all splitscreens children regardless of their internal
z-index values.
Fix: change #splitscreens z-index from 1 to auto so it no longer creates
a stacking context. Now bottomFloatingBar (1500) participates in the
same stacking context as ToolPanel (1400), and 1500 > 1400 means the
TimeUI dropdown correctly paints above the tool panel.
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* Migrate ~69 CursorInfo toast-like calls to proper Toast component
- Replace CursorInfo.update() toast-like calls with Toast.info/success/warning/error
- Preserve message colors: blue→info, green→success, yellow→warning, red→error
- Keep 12 legitimate cursor-following CursorInfo calls unchanged
- Files migrated: DrawTool.js, DrawTool_Files.js, DrawTool_FileModal.js,
DrawTool_Templater.js, DrawTool_SetOperations.js, DrawTool_Drawing.js,
DrawTool_Editing.js, DrawTool_Shapes.js, LayersTool.js, ShadeTool.js,
chemistrychart.js
- Fix Devin Review: Change misleading 'Bad token' to 'Login failed' in users.js
- Normalize line endings (CRLF→LF) in affected files
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* Fix Toast.js missing from git, CoordinatesDiv z-index, Legend duplicate ID, topBar padding
- Add Toast.js to version control (was untracked, causing webpack module error)
- Bump CoordinatesDiv z-index from 20 to 1001 (was hidden behind splitscreens
children after z-index:auto change)
- Fix Legend duplicate ID: separated tool icon was #LegendTool, same as content
container div, causing empty message to appear in button instead of panel
- Add hasStatus class to #topBarMain when statusIndicator is active, setting
#topBarTitleName padding-left to 0
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* Fix Legend empty message: scope selector to content container via targetId
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* Move Toast.js to design-system, fix --color-a3 text contrast, update AGENTS.md
- Move Toast.js from UserInterface_/components/Toast/ to design-system/components/Toast/
(generic component belongs in design-system, not MMGIS-specific UserInterface_)
- Update all 11 Toast import paths to new location
- Bump --color-a3 in 5 dark themes to pass WCAG AA 4.5:1 contrast for text:
Dark Default #747c81→#81888d, Dark Blue #64748b→#738399,
Dark Warm #8b7a5e→#908064, Dark Mars #8a6a60→#98796f,
Dark Midnight #606088→#7a7a9e
- Update AGENTS.md: document design-system/ vs UserInterface_/ distinction
in project structure and Key Directories
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* Fix topBarTitleName padding override: increase specificity and add !important
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* Fix IdentifierTool deactivation: update icon ID reference in separateFromMMWebGIS
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* Hide toolPanelDrag when no tool is open
- Add setToolPanelDragVisible(false) to closeActiveTool() (was only in makeTool toggle-off path)
- Also guard drag handle display on isOpen (toolPanelWidth > 0) as safety net
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* Add hover effect to MMGIS logo (opacity + brightness transition)
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* Fix MMGIS logo hover: keep full opacity, use subtle background highlight instead
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* Selective tile fade: fade on pan/zoom, instant on refresh/reload
- Remove blanket _fadeAnimated toggle from toggleTimeUI (was killing fade
for all tiles while TimeUI was open)
- Monkey-patch GridLayer.redraw, TileLayer.setUrl, and GridLayer._tileReady
to suppress fade via a transient _suppressTileFade map flag
- Set _suppressTileFade in reloadTimeLayers for time-driven reloads
- Flag auto-clears after 300ms so pan/zoom tiles still get the nice fade
- Install pbf dependency (required by CesiumMVTLayer from #942 merge)
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* Per-layer fade control: time-enabled + shade/viewshed layers never fade
- Replace transient map-level _suppressTileFade with per-layer _noFade flag
- Patch GridLayer._tileReady to check _noFade on the layer instance
- Set _noFade on time-enabled tile layers and data/GL layers at creation
- Set _noFade on Shade and Viewshed tool GL layers
- Non-time-enabled base imagery tiles still fade normally on pan/zoom
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix: mobile UI improvements — move hamburger to right menu, show panel toggles, isMobile-driven toolbar layout, desktop-matching scalebar/compass
- Remove left hamburger menu (#topBarMenu), move BottomBar items into
top-right kebab dropdown menu for both mobile and desktop
- Show panel toggles (Viewer/Map/Globe) and account/login UI in mobile
topbar's #topBarRight
- Move toolbar horizontal layout CSS from @media breakpoints to
UserInterfaceMobile_.css (loaded only when isMobile flag is true)
- Remove #mapTopBar @media rule from mmgis.css, add to mobile CSS
- Remove mobile-only simplified scalebar rendering; use full desktop
scalebar with both large and small axes on all viewports
- Remove display:none on .leaflet-control-scalefactor in mobile CSS
- Remove #loginDiv display:none from mobile CSS (React overlay handles it)
- Simplify BottomBarReact container styles (no more absolute positioning)
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* chore: bump version to 4.3.28-20260430 [version bump]
* fix: mobile toolbar 40px height, bottomFloatingBar flush, timeUI toggle, scalebar position, hide hotkeys
- Toolbar height 40px, toolButton width 40px, no border-bottom
- toolcontroller_incdiv: no padding-bottom, overflow-y hidden
- bottomFloatingBar: no border-radius, left/right/bottom = 0
- Add MobileTimeUIToggle button on far right of toolbar
- Hide Keyboard Shortcuts from kebab menu on mobile
- Fix scalebar positioning (remove top:48px override in UserInterfaceBridge)
- Set mobileTopSize/topSize to 40 (splitscreens top = 40px, not 50px)
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix: mobile topBar padding-left 34px
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix: update MobileCoordButton topBar paddingLeft from 80px to 34px
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix: MobileTimeUIToggle — inline toggle logic, float right, hide from settings on mobile
- Replace broken Coordinates.toggleTimeUI() call with direct jQuery/store toggle
- Float time button right in toolbar
- Hide Time UI toggle from settings modal on mobile (toolbar has it)
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix: push scalebar/compass/scale up 40px on mobile, keep #timeUI in DOM
- BottomElementPositioner: position mapToolBar, leaflet-bottom-left/right
40px above bottom on mobile (above toolbar)
- Stop removing #timeUI from DOM on mobile so MobileTimeUIToggle works
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix: mobile TimeUI — only show endtime, always expanded
- Hide #mmgisTimeUIStartWrapper and StartWrapperFake on mobile via CSS
- Force expanded state (addClass expanded + show) when toggling TimeUI on
- CSS ensures #timeUI.active always shows expanded content on mobile
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix: mobile TimeUI opens in tool panel with header, end time, expanded rows
- MobileTimeUIToggle now opens/closes the tool panel via ToolController_
- Closes any active tool before showing TimeUI
- Forces expanded state when opening
- CSS hides start time inputs, positions expanded content properly
- Overrides absolute positioning of expanded content for tool panel flow
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* feat: rewrite separated tools system from jQuery to React components
- Add separatedToolsList/activeSeparatedTools state to Zustand uiStore
- Rewrite SeparatedTools.jsx with glassmorphism panels, CSS Module styling
- Replace SepToolsContainer (setInterval hack) with SepToolButton/SepToolsSection
- Remove ~170 lines of jQuery DOM construction from ToolController_.js
- Fix hardcoded rgba(26,26,27,0.88) to theme-aware var(--color-a-rgb)
- Remove separated tool entries from themeApplier.js
- Remove separated tool overrides from FloatingElements.css
- Move Legend CSS overrides from Toolbar.module.css to SeparatedTools.module.css
- Remove jQuery active-state manipulation from IdentifierTool.js
- Add store sync in Map_.js displayOnStart logic
- Preserve all DOM IDs for backward compatibility (mmgisAPI, tool make())
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* chore: bump version to 4.3.28-20260501 [version bump]
* fix: TimeUI mobile checks — use Zustand store instead of L_.UserInterface_
L_.UserInterface_ is null when TimeUI.init() runs (TimeControl.init is called
before L_.link sets UserInterface_). All 16 isMobile checks now read from
useUIStore.getState().isMobile which is set at startup.
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* chore: bump version to 4.3.29-20260501 [version bump]
* fix: move displayOnStart logic from Map_.js to ToolController_.finalizeTools()
- Map_ no longer references specific tools (LegendTool)
- displayOnStart is now handled generically for all separated tools
- Added DOM element polling (tryMake) to handle React render timing
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* revert: remove all TimeUI-related mobile changes
Reverts TimeUI.js and BottomBar.js to development base.
Restores #timeUI DOM removal in UserInterfaceBridge.fina().
Removes MobileTimeUIToggle component from Toolbar.jsx.
Removes TimeUI mobile CSS overrides from UserInterfaceMobile_.css.
Non-TimeUI refinements (toolbar height, scalebar positioning, etc.) preserved.
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* simplify: remove DOM polling, use simple setTimeout(0) for auto-open
LegendTool handles its own content lifecycle via subscribeOnLayerToggle.
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* feat: mobile TimeUI — fix isMobile detection, staging container, toolbar toggle
- TimeUI.js: import useUIStore and replace all 16 L_.UserInterface_?.isMobile
checks with useUIStore.getState().isMobile (L_.UserInterface_ is null when
TimeUI.init() runs, so mobile conditionals were dead code)
- TimeUI.js: stage mobile #timeUI in hidden #timeUIMobileStaging instead of
placing directly in #tools (which gets cleared by other tools)
- UserInterfaceBridge.js: stop removing #timeUI from DOM on mobile
- Toolbar.jsx: add MobileTimeUIToggle that moves #timeUI between staging and
#tools, opens/closes tool panel via ToolController_
- BottomBar.js: hide TimeUI toggle from settings modal on mobile
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix: rescue #timeUI back to staging when another tool opens
Subscribe to activeToolName changes — when a tool becomes active while
TimeUI is showing, move #timeUI back to #timeUIMobileStaging before
the new tool's make() clears #tools.
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* chore: remove separatedTool/justification config toggles, fix review issues
- Remove separatedTool checkbox and justification dropdown from Legend
and Identifier config.json (these are always separated, not configurable)
- Remove justification property/code from LegendTool.js, IdentifierTool.js
- Simplify Globe_.js separated tool count (no justification filter)
- Remove justification from Reference-Mission config blueprint
- Update LegendTool help docs and Legend.md documentation
- Add --color-a-rgb fallback (29,31,32) in SeparatedTools.module.css
- Add display:none !important to .panelIdentifier to prevent 12px gap
- Update e2e test comment
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix: circular import in TimeUI.js, toolbar/bottomFloatingBar position sync
- TimeUI.js: replace top-level useUIStore import with lazy _getUIStore()
accessor to avoid 'Cannot access useUIStore before initialization'
circular import error at _remakeTimeSlider
- SplitScreens.jsx: skip #timeUI reparenting observer on mobile (mobile
uses MobileTimeUIToggle to manage #timeUI placement in #tools)
- BottomElementPositioner.jsx: unify mobile transition to 0.3s (matches
toolsWrapper and toolbar), guard pxIsTools against undefined
- Toolbar.jsx: align toolbar transition to 0.3s ease-out
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* LegendTool fix empty message
* chore: remove separated tools offset logic from Globe_.js
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix: skip _makeHistogram on mobile (no timeline slider, timestamps unset)
_makeHistogram renders inside the timeline slider which doesn't exist
on mobile. Without it, _timelineStartTimestamp is NaN, causing
'Invalid time value' RangeError at toISOString().
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix: mobile TimeUI — populate expanded rows, fix Invalid date, fix panel height
- TimeUI.js attachEvents: use _initialStart/_initialEnd on mobile (same
as desktop) instead of L_.TimeControl_ which isn't set yet at init time.
Fixes 'Invalid date' in start/end time inputs.
- TimeUI.js fina: set expanded=true on mobile and call _populateExpandedRows()
so year/month/day/hour rows actually render. Removed position:absolute and
pointer-events:none overrides.
- Toolbar.jsx: set tool panel height to 217px (TimeUI.height) instead of
45% viewport — matches actual TimeUI content height.
- UserInterfaceMobile_.css: expanded content flows naturally (position:relative),
hide start time inputs, allow overflow scroll, flex-wrap topbar.
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix: mobile TimeUI justify-content center, restore toolbar border-bottom
- Add justify-content: center to #mmgisTimeUIMain on mobile
- Remove border-bottom: none override so toolbar keeps its default border
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix: mobile TimeUI overflow hidden, scalebar/compass fixed at 40px offset
- #timeUI overflow-y: hidden (was auto, causing 2px scroll)
- Scalebar/compass/map controls stay at fixed 40px offset (above toolbar)
regardless of tool panel state — no longer shift up by pxIsTools
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* Implement multi-tier knowledge architecture
- Restructure AGENTS.md from 745 lines to 106 lines (Tier 1: essential context)
- Create knowledge/ directory with 30+ wiki-style documentation files (Tier 2: deep knowledge)
- Create knowledge/reference/ with 8 detailed reference files (Tier 3: lookup material)
- Move AI-GETTING-STARTED.md and AI-DEVELOPMENT.md to knowledge/
- Update all file references in .specify/templates and blueprints
- Create knowledge/README.md as the full knowledge base index
- Create knowledge/reference/README.md as reference material index
Three-tier knowledge discovery system:
Tier 1: AGENTS.md (~106 lines) - scannable in <2 minutes
Tier 2: knowledge/*.md - deep knowledge on architecture, tools, APIs, DB, infra
Tier 3: knowledge/reference/*.md - coding conventions, API reference, troubleshooting
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* chore: bump version to 4.3.29-20260501 [version bump]
* fix: mobile toolbar active button style matches desktop, fix icon alignment
- All mobile toolbar buttons (ToolButton, MobileCoordButton, MobileTimeUIToggle)
now use display:flex with align-items/justify-content center for proper
vertical icon centering
- MobileCoordButton: changed 'active' class to 'toolButtonActive' to match
the global CSS active style (color-mmgis + color-i background)
- Removed inline color overrides so CSS .toolButtonActive takes effect
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* Add Devin knowledge notes from past MMGIS sessions
Include curated lessons learned from past Devin sessions:
- CI/CD: ignore build-arm64/amd64 failures, focus on required checks
- Child sessions: no separate PRs when consolidating
- ENV triple-update rule (.env, sample.env, ENVs.md)
- Error handling: use logger with infrastructure_error for fatal startup errors
- Path traversal security: stay within /Missions, handle subpath serving
- Database initialization architecture and migration patterns
- API authentication behavior across AUTH modes
- Auto-generated MMGIS concept index
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix: mobile toolbar active button style, icon alignment, tool deactivation
- Active toolbar buttons get desktop-matching margin (1px 0) and
border-radius (8px) via .toolButton.toolButtonActive CSS rule
- Removed line-height: 40px from .toolButton (flex centering handles
vertical alignment, line-height was pushing icons up)
- MobileCoordButton now watches activeToolName store and deactivates
when another tool opens (fixes coords staying active)
- MobileTimeUIToggle sets activeToolName='MobileTimeUI' when opening
so coords/other buttons can detect it and deactivate
- MobileTimeUIToggle clears activeToolName when closing
- Both custom buttons skip self-deactivation via name check
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* Fix cross-references: convert backtick refs to markdown links, add Devin knowledge notes
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix: mobile toolbar icon height 40px, button margins for active padding
- #toolbar .toolButton i: height 40px fixes icon vertical alignment
- #toolbar .toolButton: margin 0 2px gives spacing between buttons
- #toolbar .toolButton.toolButtonActive: margin 1px 2px so active
background has visual padding around the icon
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* Rename knowledge/ to .knowledge/ for consistency with .specify/ convention
Dot-prefix signals agent infrastructure (not source code), consistent with
.specify/, .github/, .vscode/ conventions. All cross-references updated.
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix: mobile toolbar icon line-height 40px, active button padding via height
- Coord and TimeUI button <i> icons get line-height: 40px
- Active buttons: height 34px (vs 40px toolbar) creates visual padding
around the active background, centered by flex align-items
- Buttons get margin: 0 1px for horizontal spacing
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* Fix broken cross-reference: 06.2 -> 06.1-configure-rest-api.md
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix: close active tool + cancel deferred cleanup in MobileCoordButton/TimeUI
- MobileCoordButton: call closeActiveTool() before opening, destroy
_pendingCloseTool if set, increment _closeSeq to cancel deferred
tools.innerHTML clear
- MobileTimeUIToggle: same _pendingCloseTool + _closeSeq fix after
closeActiveTool() to prevent 420ms deferred cleanup from wiping
#timeUI after it's placed in #tools
- Removed redundant closeActiveTool() from MobileCoordButton close path
(was being called after destroy, not needed)
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix: active mobile toolbar buttons 34x34px (square)
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* Drastically compress .knowledge/ — keep only unique agent content
Remove 33 wiki files that duplicate docs/pages/ content.
Remove 9 reference/ files derivable from source code.
Keep only 5 files (down from 46):
- AI-GETTING-STARTED.md (agent setup walkthrough)
- AI-DEVELOPMENT.md (spec-kit workflow)
- conventions-and-gotchas.md (naming, code style, common issues)
- 12-devin-knowledge-notes.md (CI, auth, DB init, security gotchas)
- README.md (index pointing to docs/pages/ for everything else)
Principle: don't duplicate docs/ — only keep what's uniquely agent-optimized.
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* Rename to knowledge-notes.md, remove Devin branding and fork-specific CI section
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix: hide mmgis-map-logo on mobile
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* Restore Database Safety Rules for AI Agents section in AGENTS.md
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix: shift compass and map scale 6px to the right (both mobile and desktop)
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* Add back Important Instructions, code pattern templates, and detailed project structure
- Important Instructions in AGENTS.md: MCP tools, hot-reload, Reference Mission
- .knowledge/code-patterns.md: full directory tree with key directory annotations,
plus copy-paste templates for Express routes, Sequelize models, Tool plugins,
and WebSocket handlers
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* Update project structure trees to reflect current filesystem
Add missing directories: tests/, .knowledge/, .specify/, .github/, views/,
private/, spice/, build/, examples/, scripts/middleware.js.
Both abbreviated (AGENTS.md) and detailed (.knowledge/code-patterns.md) trees
now match the actual repo layout.
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* chore: bump version to 4.3.30-20260501 [version bump]
* Add Layers_.js to project structure (key singleton L_)
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* Fix project structure: correct API layout, frontend modules, code templates
API/Backend/ uses feature-domain modules (Draw/, Users/, Config/, etc.)
with setup.js + routes/ + models/ per feature — not APIs/ or Databases/.
Frontend essence/ has Components/, Helpers/, LandingPage/, mmgisAPI/,
services/ — not Ancillary/. Basics/ includes all singletons (Globe_,
Formulae_, ToolController_, Viewer_, ComponentController_, Test_).
Code templates updated to match actual patterns (setup.js, module.exports).
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* refactor: remove test infrastructure (Test_ module, testModules, DrawTool.test)
- Delete src/essence/Basics/Test_/ directory
- Delete src/essence/Tools/Draw/DrawTool.test.js
- Remove Test_ import and Shift+T keydown handler from essence.js
- Remove tests key from Draw tool config.json
- Remove testModules generation logic from API/updateTools.js
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* chore: bump version to 4.3.31-20260501 [version bump]
* style: move Cesium link button to top-right and match Leaflet zoom button styling
- Change control container from top-left to top-right positioning
- Update button size from 26px to 30px to match Leaflet zoom controls
- Use CSS variables (--color-a, --color-f, --color-mmgis) instead of hardcoded colors
- Add border-radius and box-shadow matching Leaflet control appearance
- Update hover/inactive states to use themed colors
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix: anchor map logo to viewport instead of Leaflet map panel
- Change MapLogo parent from .leaflet-bottom.leaflet-right to #main-container
- Switch CSS position from absolute to fixed for viewport anchoring
- Add explicit bottom-offset positioning in BottomElementPositioner (desktop)
- Add explicit bottom-offset positioning in BottomElementPositioner (mobile)
- Logo stays at viewport right edge regardless of open side panels
- Retains smooth bottom offset transitions when bottom bar appears
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* docs: remove references to deleted test infrastructure (Test_, DrawTool.test)
- Remove Test_/ from project structure in .knowledge/code-patterns.md
- Remove DrawTool.test.js references from specs/006 spec, plan, and tasks
- Remove Draw Tool Testing section from tasks.md
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* chore: bump version to 4.3.32-20260501 [version bump]
* fix: append logo to document.body to avoid filter containing block
#main-container has a CSS filter property which creates a new containing
block per the CSS spec, causing position:fixed to behave like absolute.
Appending to document.body ensures true viewport-fixed positioning.
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix: prevent mobile topBarTitleName text wrapping by replacing max-width with white-space: nowrap
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* chore: bump version to 4.3.33-20260501 [version bump]
* chore: bump version to 5.0.0 and update changelog
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* refactor(ui): move Screenshot/Fullscreen to BottomBar, About to TopBar kebab
TopBar kebab menu now contains only Keyboard Shortcuts, Settings, and About
(About now shows on both desktop and mobile).
BottomBarReact now renders Screenshot, Fullscreen, and Copy Link buttons
(top to bottom) following the same IconButton + Tooltip pattern. The
About button has been removed from BottomBarReact.
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* chore: bump version to 5.0.1-20260505 [version bump]
* feat(mobile): enforce exclusive panel toggling on mobile in TopBar
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* chore: bump version to 5.0.1-20260505 [version bump]
* style: reposition LithoSphere globe controls to match Leaflet/Cesium theme
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* chore: bump version to 5.0.1-20260505 [version bump]
* feat(topbar): hide Viewer/Globe toggles based on configured panels
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* style(bottombar): reorder buttons (Copy Link, Screenshot, Fullscreen) and unify size
Reorder the BottomBarReact buttons top-to-bottom to: Copy Link, Screenshot,
Fullscreen.
Move the 24x24 button sizing from the #topBarLink id selector in mmgis.css
into the .barButton class in BottomBarReact.module.css so all three buttons
share the same compact size as the original Copy Link button. Drop the now
redundant #topBarLink rule.
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* style(bottombar): increase padding-bottom to 12px and button margin to 3px 0
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* style: rearrange globe controls — compass top-right circular, nav row, vertical column, panels open left
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* chore: bump version to 5.0.2-20260505 [version bump]
* chore: bump version to 5.0.2-20260505 [version bump]
* style: anchor observe settings panel right:34px and float nav hover panels
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* feat(theming): add 5 new themes, --color-shadow variable, and configure ThemePreview
- Add Dark Terra, Dark Nebula, Dark Lunar, Dark Supernova, Light Botanical themes
- Add --color-shadow CSS variable to every theme + :root fallback
- Replace hardcoded rgba shadow colors with var(--color-shadow) in TopBar,
Toolbar, SeparatedTools, ToolPanel, FloatingElements, Dropdown, Modal,
and SplitScreens
- Add Custom shadowcolor color picker in tab-ui-config and apply it via Stylize
- Add ThemePreview component (configure/src) wired through Maker.js as
a new 'themepreview' row type so the configure UI shows a live mini
mockup of the selected theme
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* chore: bump version to 5.0.2-20260505 [version bump]
* fix(configure/ThemePreview): tighten top spacing and live-preview Custom theme
- Pull the preview up by 12px so the gap below the theme dropdown is tighter.
- Read the Custom color pickers (look.primarycolor / secondarycolor /
tertiarycolor / accentcolor / shadowcolor / topbarcolor / toolbarcolor /
mapcolor) from the configuration and overlay them on Dark Default so
the preview reflects Custom theme edits live, matching Stylize.js's
runtime behavior.
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* chore: bump version to 5.0.3-20260505 [version bump]
* feat(themes): add Dark Heliosphere, Dark Monokai, and Light Solarized
- Dark Heliosphere: deep night purple surface with corona-orange accent.
- Dark Monokai: warm graphite surface with lime accent (Monokai-inspired).
- Light Solarized: classic solarized base3/base02 with blue accent.
Mirror added to configure/src/themes/themes.js for the ThemePreview, and
the three names appended to the Color Theme dropdown options.
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix(coordinates): respect time.initiallyOpen when live deep-link is set
* chore: bump version to 5.0.3-20260505 [version bump]
* refactor(theming): remove Custom theme + per-field color overrides
- Drop the 'Custom' option from the Color Theme dropdown.
- Remove all Custom Color Options (look.primarycolor, .secondarycolor,
.tertiarycolor, .accentcolor, .bodycolor, .topbarcolor, .toolbarcolor,
.mapcolor, .hightlightcolor, .shadowcolor) from tab-ui-config.json.
- Strip the matching DOM/CSS-variable override block from Stylize.js;
Stylize now just applies the selected preset theme (and the page logo).
- Drop the empty bodycolor/topbarcolor/toolbarcolor/mapcolor/shadowcolor
defaults from API/templates/config_template.js.
- Simplify ThemePreview to render the selected preset directly — no
Custom branch, no overlay logic.
Preset themes cover all the looks we want and keep the configure surface
much smaller.
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* style(time-ui): round corners on TimeUI shell, action wrappers, mode dropdown
- #timeUI: 10px border-radius on the outer time control bar.
- #mmgisTimeUIActionsLeft / #mmgisTimeUIActionsRight: 10px border-radius
so the action clusters sit as rounded chips.
- #mmgisTimeUIActionsRight > div (excluding #mmgisTimeUIPresent): 10px
border-radius on each action button so they match the wrapper.
- #mmgisTimeUIModeDropdown: 40px height + 10px border-radius to align
with the rest of the bar; clear the dropy default border-color so the
rounded edge isn't outlined.
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* chore: bump version to 5.0.4-20260505 [version bump]
* feat(configure): mark light themes as (experimental) in dropdown label
Light themes still have outstanding contrast issues, so flag them in the
Color Theme dropdown without changing the saved value.
- Maker dropdown now accepts options as either a plain string (current
behavior) or { value, label } so the rendered label can differ from
the persisted value.
- tab-ui-config switches the six light themes to { value, label } form
with '(experimental)' appended to the label only. Existing mission
configs that already saved 'Light Default' etc. continue to match.
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* Fix timeUI border radius
* fix(mobile): rescue #timeUI before tool make() destroys it
Clicking Layers -> Time -> Layers -> Time on mobile caused the bottom
panel to render LayersTool content with TimeUI height. The #timeUI DOM
element was destroyed when LayersTool.make() called $('#tools').empty(),
before the async React useEffect in MobileTimeUIToggle could rescue it
to its staging container.
- ToolController_.makeTool: synchronously move #timeUI from #tools back
to #timeUIMobileStaging (and reset TimeUI store flags) on mobile,
before invoking the new tool's make().
- MobileTimeUIToggle.handleClick: defensive fallback that re-initializes
TimeUI if #timeUI no longer exists when the toggle is activated.
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix(mobile): move re-initialized #timeUI from staging into #tools
TimeUI.init() on mobile appends the new #timeUI to the hidden
#timeUIMobileStaging container, so the fallback branch must also move
it into #tools — otherwise the user sees an empty tool panel after
the destroyed-element recovery path.
Caught by Devin Review.
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix(mobile): preserve #timeUI when Coordinates tool empties #tools
On mobile, opening or closing the Coordinates tool runs
$('#tools').empty() inside interfaceWithMMWebGIS / separateFromMMWebGIS.
After the previous PR commits, clicking Coordinates -> Time still left
the bottom panel empty because:
- Coordinates.make() empties #tools while #timeUI is in staging (fine
on its own), but the Coordinates teardown that fires after the user
switches to the Time toggle (via MobileCoordButton's useEffect on
activeToolName change) calls Coordinates.destroy() ->
separateFromMMWebGIS(), which empties #tools wholesale and destroys
the freshly-placed #timeUI.
Add a rescueMobileTimeUI() helper that moves #timeUI from #tools back
to #timeUIMobileStaging before each tools.empty() call in Coordinates,
mirroring the rescue already done in ToolController_.makeTool().
Coordinates -> Time now correctly shows the TimeUI.
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix(mobile): harden TimeUI fallback recovery (call fina(), de-dupe popovers)
Devin Review correctly flagged that the safety-net path in
MobileTimeUIToggle.handleClick was producing a partially-broken TimeUI
when it fired:
- TimeUI.init() unconditionally appends a new #timeUIPlayPopover_global
to <body>, so a second init() left two elements with the same id.
- TimeUI.init() alone does not wire up date pickers or per-button click
handlers — that's TimeUI.fina()'s job. Without fina(), the recovered
TimeUI rendered visually but Play / Previous / Next / Fit / Follow /
Present / Expand were all dead.
Before re-initializing, remove the stale #timeUIPlayPopover_global and
#timeUIQuickSelectPopover_global divs to avoid duplicate ids. After the
new #timeUI is moved into #tools, call TimeUI.fina() to populate the
date pickers, attach the button click handlers, build the histogram,
and populate the expanded mobile rows.
Some delegated body/document handlers in attachEvents() will still be
duplicated on this path; that is acceptable for a degraded recovery
that should never run in practice now that the primary rescues in
ToolController_.makeTool() and Coordinates.js cover all known paths.
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* chore: bump version to 5.0.5-20260505 [version bump]
* fix(mobile): Coordinates teardown only removes its own DOM
The previous Coordinates fix was racing with itself: after the Time
toggle synchronously moved #timeUI into #tools, MobileCoordButton's
useEffect (triggered by the activeToolName change) ran on the next
React tick and called L_.Coordinates.destroy(). That called
separateFromMMWebGIS(), whose rescue moved #timeUI right back into the
hidden staging div before tools.empty() — so the bottom panel ended up
empty even though the time toggle was 'active'.
Make separateFromMMWebGIS selective: only remove the
Coordinates-specific DOM (#coordUIHeader and #CoordinatesDiv) instead
of wiping all of #tools. Any other content already in #tools (e.g.
#timeUI placed there by the Time toggle) is left alone.
interfaceWithMMWebGIS still keeps the rescue + tools.empty() pattern
on the open path so Coordinates always starts from a clean panel.
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* Bump DrawTool Temporal Drawings upward
* chore: bump version to 5.0.6-20260505 [version bump]
* chore: reset version to 5.0.0
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* test(e2e): fix 9 pre-existing failures (test-only changes)
- mmgis-api.spec.js: add form-fill login under AUTH=local; serialize
describe to avoid concurrent-login race in the session store
- coordinates.spec.js: TimeUI toggle was moved from the coordinates bar
to the Settings modal; navigate via topbar kebab menu and assert the
checkbox is rendered
- widgets.spec.js: target .leaflet-control-zoom-in/-out specifically;
the bare .leaflet-control-zoom class is also used by the home/reset
control, so the original assertion was always false
- sites.spec.js: scope panel selector to #toolPanel; both the toolbar
icon and the panel container share id="SitesTool"
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* chore: bump version to 5.0.1-20260505 [version bump]
* Revert "chore: bump version to 5.0.1-20260505 [version bump]"
This reverts commit 4880204c1163be5d1d7fa96d14a0ed018c6f586c.
* fix: prevent filter operator dropdown clipping in Layers panel
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* chore: bump version to 5.0.1-20260507 [version bump]
* revert: keep dropy openUp:true for operator dropdowns
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* Revert "chore: bump version to 5.0.1-20260507 [version bump]"
This reverts commit d67c369ed437e47d658ae051348d377978dc48ed.
* chore: bump version to 5.0.1-20260507 [version bump]
* Revert "chore: bump version to 5.0.1-20260507 [version bump]"
This reverts commit 29565ed829a55e9c241a789c9a3901d11cb5ca67.
* chore: bump version to 5.0.1-20260507 [version bump]
* Revert "chore: bump version to 5.0.1-20260507 [version bump]"
This reverts commit 50e357604ebe9378564619b34c508b63cfb62c1d.
* chore: bump version to 5.0.1-20260507 [version bump]
* chore: bump version to 5.0.2-20260511 [version bump]
* fix: render Globe panel immediately on first open without window resize
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* chore: bump version to 5.0.3-20260511 [version bump]
* feat: add theme borders to panels and gradient backgrounds to splitters
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* chore: bump version to 5.0.4-20260511 [version bump]
* style: bump split shadow gradient opacity to 0.4
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* style: hotkeys modal 3-col grid + smaller leaflet zoom button gap
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* style: prevent hotkey label/value wrapping (ellipsis instead)
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* style: hotkeys modal single column, no wrap, no truncation
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* chore: bump version to 5.0.4-20260511 [version bump]
* style: hotkeys modal dividers, invert title/subtitle colors, rename title, margin above subtitles
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* style: move splitter gradient to themed CSS class, restore hover feedback
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* chore: bump version to 5.0.5-20260511 [version bump]
* style: hotkeys section titles use --color-h (matches rest of app)
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* chore: bump version to 5.0.5-20260511 [version bump]
* fix: guard Globe_.init() inside rAF to prevent double instantiation
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* chore: bump version to 5.0.6-20260512 [version bump]
* feat(plugins): per-plugin deps, lazy tool loading, validation, shared discovery
Phase 3 — Plugin config validation + override warnings:
- New API/pluginValidation.js with validatePluginConfig() for tool, component,
and backend manifests. Validates required fields (name, paths), object/string
shape of paths, dependencies block (npm/python.pip/python.conda), and warns
on unknown top-level fields.
- updateTools()/updateComponents() now skip invalid plugins and emit override
warnings (matching what components already logged for tools).
Phase 2 — Shared discoverPlugins() utility:
- New API/pluginDiscovery.js consolidates the duplicated scanning logic from
updateTools(), updateComponents(), and getBackendSetups(). Supports exact-
name and substring container patterns, JSON/require/no-op loaders, and skips
dot/underscore-prefixed dirs.
- updateTools.js and setups.js refactored on top of the shared helper.
Phase 1 — Per-plugin dependency declaration + build-time aggregation:
- Plugin config.json may now declare a 'dependencies' block (npm + python.pip +
python.conda). validatePluginConfig() also validates this shape.
- New scripts/resolve-plugin-deps.js scans every tool/component/backend plugin
and writes plugin-package.json, plugin-python-requirements.txt, and
plugin-conda-deps.txt. Detects version conflicts and fails loudly.
- scripts/build.js calls resolvePluginDeps() before updateTools().
- Dockerfile installs the aggregated plugin npm and pip deps after the root
npm ci, using --no-save / --no-package-lock / --ignore-scripts so the root
lockfile is untouched.
- Animation tool migrated: ffmpeg/gifshot/html2canvas now declared in its
config.json (kept in root package.json for transitional compat).
- Generated artifacts gitignored.
Phase 4 — Lazy loading of tool bundles:
- updateTools() now emits dynamic-import arrow functions in the generated
src/pre/tools.js with webpackChunkName hints so each tool is split into
its own chunk (Kinds stays static because it's required synchronously).
- ToolController_ gains ensureToolLoaded(name) and getLoadedTool(name) helpers
and makeTool is async; init/finalizeTools and the separated-tool auto-open
flow are updated to handle lazy modules.
- Toolbar.jsx, SeparatedTools.jsx, SitesTool.js, and Layers_.js migrated to
resolve LayersTool/etc. via the new helpers instead of poking toolModules
directly.
Tests & docs:
- tests/fixtures/test-plugin-tools/{TestPlugin,InvalidPlugin,OverridePlugin}
+ tests/helpers/plugin-helpers.js with install/uninstall helpers.
- New unit specs: pluginValidation, pluginDiscovery, updateTools,
resolvePluginDeps, toolLazyLoading (57 tests, all passing).
- CONTRIBUTING.md and docs/pages/Contributing/Contributing.md updated with
schema, override behaviour, dependency declaration, build-time aggregation,
conflict detection, and Docker integration.
* chore: bump version to 5.0.7-20260512 [version bump]
* fix: make Globe_.init() idempotent against multi-init
Globe_.init() previously constructed a fresh GlobeRenderer on every call,
which after #71 could happen multiple times for a single toggle (uiStore
setTimeout + TopBar rAF). Each extra construction appends another
.cesium-widget / _lithosphere_scene to #globe and leaves event handlers
wired to dereferenced renderer state, which has been observed to break
LithoSphere globe control buttons on configurations where the globe panel
starts closed at boot.
Add a top-of-init() guard that bails out and calls invalidateSize() when
a renderer already exists. Single small, surgical change; no behavior
change for the !L_.hasGlobe mock-swap path or for first-time construction.
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* chore: bump version to 5.0.7-20260512 [version bump]
* test(plugins): generate src/pre/tools.js on demand in toolLazyLoading spec
The Playwright unit-tests CI step runs before `npm run build` so the
gitignored `src/pre/tools.js` artifact does not yet exist on disk.
Add a beforeAll hook that invokes `updateTools()` to regenerate it
when missing, keeping the spec self-contained on both CI and dev
machines that already built locally.
* fix(tools): defensive getTool() + preload flag for cross-referenced tools
Devin Review flagged a behavioural regression introduced by Phase 4:
`ToolController_.getTool(name)` previously always returned a method-
callable object (real module or `{ use(){} }` stub) because every tool
was statically imported. After Phase 4, unresolved lazy loaders are
`() => import(...)` functions, so callers like `Map_.getTool('InfoTool').use(...)`,
`mmgisAPI.getTool('DrawTool').filesOn`, and `LegendTool` calling
`LayersTool.populateCogScale` would crash with TypeError until the
target tool was opened.
Two fixes:
1. **Defensive getTool()**: Returns the legacy fallback stub when the
tool module is still a lazy-loader function, and fires off
`ensureToolLoaded(name)` in the background so subsequent calls see
the resolved module. Prevents all crashes immediately.
2. **`preload: true` config flag**: Tools reached synchronously from
other code paths (Info, Draw, Layers, Chemistry) now declare
`"preload": true` in their `config.json`. `ToolController_.init()`
calls `preloadEagerTools()` which fires `ensureToolLoaded` for
every such tool right after toolbar setup — the chunks download
in parallel with the rest of the page becoming interactive, so by
the time a user clicks a feature the InfoTool module is already
resolved.
`validatePluginConfig` now accepts `preload` as a known tool field;
CONTRIBUTING.md and docs/pages/Contributing/Contributing.md updated to
document when to set it. Added a unit test covering the defensive
getTool behaviour and the `preload` propagation through
`toolConfigs`.
* chore: bump version to 5.0.8-20260512 [version bump]
* revert(plugins): remove Phase 4 lazy tool loading and preload mechanism
Phase 4 lazy emission caused cross-tool consumers (Map_ feature-click,
mmgisAPI, LegendTool) to receive raw '() => import(...)' arrows from
ToolController_.getTool(), breaking InfoTool open. Reverting to the
pre-Phase-4 behavior of static tool imports.
- API/updateTools.js: generated src/pre/tools.js now emits
'import FooTool from ...' for every tool (Kinds stays static too).
- ToolController_.js: getTool/makeTool back to sync; ensureToolLoaded,
getLoadedTool, preloadEagerTools deleted; separated-tool auto-open
flow simplified to direct sync calls.
- Toolbar.jsx, SeparatedTools.jsx, Layers_.js: revert async/lazy
patterns to sync ToolController_.toolModules[name] access.
- API/pluginValidation.js: drop 'preload' from KNOWN_FIELDS.
- src/essence/Tools/{Info,Draw,Layers,Chemistry}/config.json: drop
'preload: true'.
- CONTRIBUTING.md + docs: remove preload documentation.
- tests/unit/toolLazyLoading.spec.js: rewrite to verify static
imports instead of lazy loaders.
Also: log standard backends at startup (parity with plugin backends
and with tools/components), so all backends now produce
'info Loaded backend: <name> from <container>' at boot.
Phases 1-3 (per-plugin dependency aggregation, shared discoverPlugins,
config validation + override warnings) are unaffected.
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* feat(logger): new 'loaded' level (purple bg) for tool/component/backend startup
Previously the 'Loaded tool/component/backend: X from Y' lines used
the generic blue 'info' tag. They now use a dedicated 'loaded' level
rendered with a purple (#a855f7) background, so plugin discovery
output is visually distinct from other info messages.
- API/logger.js: add 'loaded' case to the dev-mode switch (white text
on purple bg) and suppress the redundant 'Caller:' echo for it
(matches how 'info' and 'success' are handled).
- API/updateTools.js: registerPlugin now logs at level 'loaded'.
Drops the redundant 'Loaded ' prefix since the level tag now reads
'loaded'.
- API/setups.js: standard and plugin backend startup logs use the
new level, same drop of the 'Loaded ' prefix.
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* feat(logs): 'Plugging in Tools/Components/Backends...' headings
Rename the cyan banner messages in scripts/build.js and scripts/server.js
from 'Updating Tools...' / 'Updating Components...' to 'Plugging in
Tools...' / 'Plugging in Components...' so the headings match the
plugin terminology used everywhere else (plugin-package.json,
discoverPlugins, etc.).
Also add a matching 'Plugging in Backends...' banner before
setups.getBackendSetups() in scripts/server.js so backends get an
equivalent title block to tools and components.
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* style(logs): cyan banner lines lead with a blank line instead of trailing one
Move the \n from the end to the beginning of every cyan banner in
scripts/build.js and scripts/server.js (Resolving Plugin Dependencies,
Plugging in Tools/Components/Backends, Validating Environment
Variables, Starting websocket, Starting the development server) so
that the blank line visually separates each section above its title
rather than below it.
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* feat(plugins): postinstall hook auto-installs plugin npm deps
Plain `npm install` (or `np…
…ectors (#981) * Remove dead CSS: delete tools.css, clean ~600 lines from mmgisUI.css and mmgis.css - Delete tools.css entirely (both selectors #CurtainToolList and .searchToolSelect are unreferenced anywhere in the codebase) - Remove from mmgisUI.css: .mmgisRadioBar3/4/Vertical (140 lines), .mmgispureselect (104 lines), blink/condemned_blink_effect (38 lines), .slidecontainer/.slider (41 lines), .ar_slider (91 lines), .verticalSlider (91 lines), .mmgisMultirange_elev (19 lines), .ui-corner-all/bottom/right/br (9 lines) - Remove from mmgis.css: #nodeenv, empty #topBar{}, #topBarInfo, #topBarHelp, #topBarFullscreen, #toggleUI, #logoGoBack - Keep #topBarLink (used in BottomBarReact.jsx), #webgl-error-message (used by vendored THREE.js) - All selectors verified with repo-wide grep before removal Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * UI fixes: tooltips, splitter hover, mobile toolbar, color schemes - Replace Base UI Tooltip with simple React portal tooltip (200ms delay, tippy-matching style) — fixes missing tooltips for toolbar/topbar/bottom buttons - Add cursor + hover highlight to vertical splitters (was missing because module CSS didn't inherit global .splitterV styles) - Add hover highlight to tool panel drag handle - Remove mdi-drag-vertical icon from tool panel drag - Add mobile toolbar horizontal layout via @media query overrides - Add 4 new color schemes: High Contrast (a11y), Dark Mars, Dark Midnight, Light Warm (total: 10 themes) - Previous fixes also included in working tree: - timeUI border moved to toolsWrapper border-bottom (conditional) - #toggleTimeUI button removed entirely - CoordinatesDiv: vertical centering, unified background, 12px right offset - barBottom padding-bottom: 8px Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Fix assignment operator used instead of comparison in TimeControl.fina() Pre-existing bug: `TimeControl.enabled = true` was assigning instead of comparing. Changed to `TimeControl.enabled === true`. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Restructure Configure page UI tab: add all themes, Custom mode, enableWhenField - Added all 10 theme presets to dropdown (was missing Dark Mars, Dark Midnight, Light Warm, High Contrast) - Added 'Custom' option: skips preset theme, uses only color picker values - Moved Theming section directly under Rebranding - Nested 'Custom Color Options' under Theming with subdescription - Added enableWhenField support to Maker.js: disables color pickers unless theme is set to Custom - Renamed color options with clearer names and improved descriptions: Primary → Surface Color, Secondary → Deep Background Color, Tertiary → Text Color, Body → Page Body Color, Highlight → Feature Highlight - Stylize.js: skip setTheme() when theme is 'Custom' - Rebuilt configure page Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Revert tooltips to tippy.js, fix dropdown menus, redesign About modal 1. Tooltip: Replaced custom React portal tooltip with tippy.js wrapper. Uses the existing tippy.js dependency and 'blue' theme for consistency. 2. Dropdown: Replaced Base UI Menu with native portal dropdown. Base UI's nested Menu.Trigger + BaseButton composition was swallowing click events, breaking userAvatar and menuBtn menus. New implementation uses simple state + createPortal with proper outside-click dismissal. 3. About modal: Professional redesign with centered MMGIS ASCII art header, proper GitHub SVG logo link, clean metadata section, centered link buttons, attributions section, and NASA-AMMOS footer. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Restore .mmgisHelpButton base styles lost during Help.css module migration The global .mmgisHelpButton styles (yellow color, compact 18x18px sizing, 0.7 opacity) were removed when Help.css was converted to Help.module.css. Since Help.getComponent() emits raw HTML strings for jQuery-rendered tool headers, it cannot use CSS Module scoped classes. Restored the base styles in mmgis.css alongside the related .mmgisToolHelpBtn definition. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Fix session logout regression, About modal refinements, High Contrast theme, Stylize.js, Default Tool config - Login: skip session.regenerate() for token-based re-auth (useToken:true) so reloading the main page no longer invalidates the configure page session - About modal: replace ASCII art with mmgis.png logo, rename Attributions to Map Layer Attributions, remove footer logo, link NASA-AMMOS to ammos.nasa.gov - High Contrast theme: change accent from #ffff00 to #ffd700 (gold) for better contrast ratios against dark backgrounds - Stylize.js: color overrides only apply when theme is Custom or unset, preventing preset themes from being clobbered by stale config values - Restore Default Tool config section in tab-ui-config.json (accidentally removed during Theming section reorganization) Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Restore defaulttooldropdown case handler in Maker.js The case was accidentally removed during the Configure page UI tab restructure (d7f96c50). Without it, the Default Tool dropdown in the Configure page rendered as nothing despite the config referencing it. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * High Contrast tooltip text, panel toggle styling, scale position swap - High Contrast theme: tooltips now use black text on yellow background via --color-c2-text variable (white for all other themes) - About modal links use var(--color-f) for consistent theme text color - Panel toggle buttons: 11px uppercase with 600 weight for better visibility - Mapping scale button moved to bottom-right of compass (was top-left) Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Reposition Viewer and Globe panel buttons to top-right - Viewer: dropdown selector at top-right edge, OSD buttons stacked vertically below it; settings panel opens to the left - Globe: home, exaggerate, observe, walk, link controls moved from TopLeft to TopRight corner via addControl 4th arg - Style consistency: OSD buttons and LithoSphere controls now match Leaflet zoom controls (var(--color-a) bg, var(--color-f) text, var(--color-mmgis) hover, 30px size, 3px border-radius) - Viewer settings sliders use var(--color-a3) instead of hardcoded #444444 - Az/el indicator stays at bottom center (exception per design) Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Consistent modal theming + session security fix Modal theming: - All modals now share consistent styling: backdrop-filter blur, semi-transparent background via --color-a-rgb, 10px border-radius, header divider line, box-shadow - Updated: loginModal, Help, ConfirmationModal, Settings, Hotkeys, About modals - Tool panel backgrounds changed from opaque var(--color-k) to transparent so the ToolPanel's existing backdrop-filter effect shows through - Legend tool header updated to match consistent 44px height with divider - applyTheme.js now auto-derives --color-a-rgb from theme's --color-a hex value - Modal service wrapper gets backdrop-filter: blur(12px) Session security (Devin Review fix): - Token re-auth now calls req.session.regenerate() with data preservation to prevent session fixation while maintaining multi-tab compatibility - Token is rotated via crypto.randomBytes on every re-auth (was being reused) Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * UI fixes: viewer settings, button sizing, menu contrast, coords, login header 1. Viewer OSD settings moved to top of button stack, panel opens downward 2. Overlay buttons consistent 30x30px (OSD line-height fix, home button) 3. Menu/icon contrast improved: Dropdown items and IconButtons use --color-a5 (was --color-a3) with --color-f on hover for better dark theme legibility 4. CoordinatesDiv fixed to 30px height, pickLngLat button centered 5. Login modal now has a header bar with 'Log In' title and close X button; title toggles to 'Sign Up' when switching modes Also reverts session regeneration for token re-auth (Devin Review feedback): token-based re-auth now refreshes session data in-place without regeneration or token rotation, preserving multi-tab compatibility. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * UI polish: nav popover, sep tools, compass, zoom controls, status indicator, toast 1. Description nav popover: added z-index:9000 so menu appears above map panels 2. Separated tools: default color changed from accent to --color-f; fixed CSS selector from .toolButtonSep to .toolSep to match actual class names 3. Compass + mapping scale shifted left by 30px for better positioning 4. Map zoom/home controls: use --color-f instead of accent --color-c to reduce visual prominence; hover still highlights with accent color 5. Status indicators (reload/ws disconnect/layer update) moved from Leaflet control to TopBar with soft pulsing fade animation and tooltip on hover 6. WebSocket retry toast: rounded corners, glass background with backdrop-filter, border-left accent for failure state instead of solid red background Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Fix 14 tool UI issues: headers, backgrounds, layout, functional bugs Header alignment: - LayersTool: align-items center on #filterLayers, gap between right icons - InfoTool: align-items center on #infoToolHeader, 44px height - ViewshedTool: align-items center, restructured header with left/right divs - IsochroneTool: restructured flat header into nested mmgisToolHeader pattern - ShadeTool: align-items center on #vstHeader children Icon spacing: - LayersTool: increased right margin to 28px + gap 2px - ViewshedTool: #vstNew padding-right 30px (clear of close button) - IsochroneTool: #iscNew padding-right 30px Missing components: - SitesTool: added Help import + help icon via mmgisToolHeader pattern - AnimationTool: added full mmgisToolHeader with title and help icon Background fixes: - InfoTool: changed toolsContainer background from transparent to var(--color-a) - DrawTool: changed toolsContainer background from transparent to var(--color-a) Layout fixes: - DrawTool: #drawToolContents top 81px, height calc(100%-81px), #drawToolNav margin-right 0 - MeasureTool: removed padding-left:0 override from mmgisToolHeader child selector Functional fixes: - InfoTool: updated jQuery selectors from #InfoTool to #toolButtonInfo (React toolbar IDs changed) - CurtainTool: deferred OpenSeadragon init with requestAnimationFrame (React 18 async render) - CurtainTool: curtainToolBar justify-content flex-end (icons at bottom) Security: - TopBar StatusIndicator: escape HTML in layer names to prevent XSS via addLayerQueue Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Fix mapToolBar pointer events, login padding, default tool, About modal order 1. mapToolBar: set pointer-events:none on both #mapToolBar and direct children so clicks pass through to the map; leaf elements still get auto via .childpointerevents rule 2. #loginModalBody: padding changed to 40px 0px 0px 3. Default tool: deferred click to requestAnimationFrame so React toolbar has rendered before getElementById runs 4. About modal: moved mainInfoModalCustom to right below mainInfoModalHero Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Fix tool headers to 40px, ViewshedTool subheader, AnimationTool header, InfoTool close btn, statusIndicator position 1. All tool panel headers: changed from 44px to exactly 40px height - Global .mmgisToolHeader and .mmgisToolTitle in mmgis.css - InfoTool.css, ViewshedTool.css, IsochroneTool.css, ShadeTool.css 2. LayersTool #filterLayers: height 40px, .right > div height unset, .right margin-right 30px, .right > div margin 0px 3px 3. ViewshedTool: restructured header — title+help in mmgisToolHeader row, vstToggleAll (left) and vstNew (right) on a new #vstSubHeader row below 4. AnimationTool: removed old #animationToolHeader CSS (padding 15px 20px, white background, 18px font), now uses standard mmgisToolHeader class. Fixed color from var(--color-a) (background) to var(--color-f) (text) 5. InfoTool close X: re-inject close button after use() rebuilds content via TC_.injectCloseButton() (toolsContainer.empty() was removing it) 6. StatusIndicator: moved to left of topBarTitle in JSX render order. Added align-items:center to #topBarMain for vertical alignment. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Remove title attr from StatusIndicator (conflicts with tippy tooltip) Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Fix TimeUI dropdown z-index above tool panel, reposition toasts to top-center 1. TimeUI dropdowns: added z-index 10000 to all dropy content ul elements so they render above the vertical tool panel (z-index 1400). Also set timeUIDock to position:relative with z-index 10000 and overflow:visible. 2. Toast notifications: repositioned #toast-container from bottom-right to top-center just below the topbar (top: 44px, centered with transform). Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Fix StatusIndicator spacing, CurtainTool close btn, header title font consistency 1. StatusIndicator: use display:none/flex instead of opacity:0/1 so it takes no space when there's no active status indicator. 2. CurtainTool: added close X button at top of curtainToolBar (matching MeasureTool pattern) with flex spacer pushing other buttons to bottom. 3. Header title font consistency: InfoTool, ShadeTool, CurtainTool titles now match mmgisToolTitle standard (font-weight:600, padding-left:10px, height:40px for CurtainTool which was 34px). Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Legend empty state, scalefactor position, sep-tool-header unbold 1. Legend: show 'No active layers with legends' when no legend items are present. Also fixed container height calc(100% - 40px). 2. Mapping Scale (.leaflet-control-scalefactor): shifted 10px right (left 26→36px) and 1px down (bottom 30→29px). 3. sep-tool-header span: font-weight changed from 600 to 400. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Guard Legend empty state message to only show when panel is active Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Fix TimeUI dropdown covered by toolPanel: remove splitscreens stacking context #splitscreens had z-index:1 which created a stacking context, confining its children (including bottomFloatingBar at z-index:1500) to that context. Since ToolPanel (z-index:1400) was a sibling outside splitscreens, it painted above all splitscreens children regardless of their internal z-index values. Fix: change #splitscreens z-index from 1 to auto so it no longer creates a stacking context. Now bottomFloatingBar (1500) participates in the same stacking context as ToolPanel (1400), and 1500 > 1400 means the TimeUI dropdown correctly paints above the tool panel. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Migrate ~69 CursorInfo toast-like calls to proper Toast component - Replace CursorInfo.update() toast-like calls with Toast.info/success/warning/error - Preserve message colors: blue→info, green→success, yellow→warning, red→error - Keep 12 legitimate cursor-following CursorInfo calls unchanged - Files migrated: DrawTool.js, DrawTool_Files.js, DrawTool_FileModal.js, DrawTool_Templater.js, DrawTool_SetOperations.js, DrawTool_Drawing.js, DrawTool_Editing.js, DrawTool_Shapes.js, LayersTool.js, ShadeTool.js, chemistrychart.js - Fix Devin Review: Change misleading 'Bad token' to 'Login failed' in users.js - Normalize line endings (CRLF→LF) in affected files Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Fix Toast.js missing from git, CoordinatesDiv z-index, Legend duplicate ID, topBar padding - Add Toast.js to version control (was untracked, causing webpack module error) - Bump CoordinatesDiv z-index from 20 to 1001 (was hidden behind splitscreens children after z-index:auto change) - Fix Legend duplicate ID: separated tool icon was #LegendTool, same as content container div, causing empty message to appear in button instead of panel - Add hasStatus class to #topBarMain when statusIndicator is active, setting #topBarTitleName padding-left to 0 Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Fix Legend empty message: scope selector to content container via targetId Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Move Toast.js to design-system, fix --color-a3 text contrast, update AGENTS.md - Move Toast.js from UserInterface_/components/Toast/ to design-system/components/Toast/ (generic component belongs in design-system, not MMGIS-specific UserInterface_) - Update all 11 Toast import paths to new location - Bump --color-a3 in 5 dark themes to pass WCAG AA 4.5:1 contrast for text: Dark Default #747c81→#81888d, Dark Blue #64748b→#738399, Dark Warm #8b7a5e→#908064, Dark Mars #8a6a60→#98796f, Dark Midnight #606088→#7a7a9e - Update AGENTS.md: document design-system/ vs UserInterface_/ distinction in project structure and Key Directories Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Fix topBarTitleName padding override: increase specificity and add !important Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Fix IdentifierTool deactivation: update icon ID reference in separateFromMMWebGIS Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Hide toolPanelDrag when no tool is open - Add setToolPanelDragVisible(false) to closeActiveTool() (was only in makeTool toggle-off path) - Also guard drag handle display on isOpen (toolPanelWidth > 0) as safety net Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Add hover effect to MMGIS logo (opacity + brightness transition) Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Fix MMGIS logo hover: keep full opacity, use subtle background highlight instead Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Selective tile fade: fade on pan/zoom, instant on refresh/reload - Remove blanket _fadeAnimated toggle from toggleTimeUI (was killing fade for all tiles while TimeUI was open) - Monkey-patch GridLayer.redraw, TileLayer.setUrl, and GridLayer._tileReady to suppress fade via a transient _suppressTileFade map flag - Set _suppressTileFade in reloadTimeLayers for time-driven reloads - Flag auto-clears after 300ms so pan/zoom tiles still get the nice fade - Install pbf dependency (required by CesiumMVTLayer from #942 merge) Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Per-layer fade control: time-enabled + shade/viewshed layers never fade - Replace transient map-level _suppressTileFade with per-layer _noFade flag - Patch GridLayer._tileReady to check _noFade on the layer instance - Set _noFade on time-enabled tile layers and data/GL layers at creation - Set _noFade on Shade and Viewshed tool GL layers - Non-time-enabled base imagery tiles still fade normally on pan/zoom Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * fix: mobile UI improvements — move hamburger to right menu, show panel toggles, isMobile-driven toolbar layout, desktop-matching scalebar/compass - Remove left hamburger menu (#topBarMenu), move BottomBar items into top-right kebab dropdown menu for both mobile and desktop - Show panel toggles (Viewer/Map/Globe) and account/login UI in mobile topbar's #topBarRight - Move toolbar horizontal layout CSS from @media breakpoints to UserInterfaceMobile_.css (loaded only when isMobile flag is true) - Remove #mapTopBar @media rule from mmgis.css, add to mobile CSS - Remove mobile-only simplified scalebar rendering; use full desktop scalebar with both large and small axes on all viewports - Remove display:none on .leaflet-control-scalefactor in mobile CSS - Remove #loginDiv display:none from mobile CSS (React overlay handles it) - Simplify BottomBarReact container styles (no more absolute positioning) Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * chore: bump version to 4.3.28-20260430 [version bump] * fix: mobile toolbar 40px height, bottomFloatingBar flush, timeUI toggle, scalebar position, hide hotkeys - Toolbar height 40px, toolButton width 40px, no border-bottom - toolcontroller_incdiv: no padding-bottom, overflow-y hidden - bottomFloatingBar: no border-radius, left/right/bottom = 0 - Add MobileTimeUIToggle button on far right of toolbar - Hide Keyboard Shortcuts from kebab menu on mobile - Fix scalebar positioning (remove top:48px override in UserInterfaceBridge) - Set mobileTopSize/topSize to 40 (splitscreens top = 40px, not 50px) Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * fix: mobile topBar padding-left 34px Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * fix: update MobileCoordButton topBar paddingLeft from 80px to 34px Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * fix: MobileTimeUIToggle — inline toggle logic, float right, hide from settings on mobile - Replace broken Coordinates.toggleTimeUI() call with direct jQuery/store toggle - Float time button right in toolbar - Hide Time UI toggle from settings modal on mobile (toolbar has it) Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * fix: push scalebar/compass/scale up 40px on mobile, keep #timeUI in DOM - BottomElementPositioner: position mapToolBar, leaflet-bottom-left/right 40px above bottom on mobile (above toolbar) - Stop removing #timeUI from DOM on mobile so MobileTimeUIToggle works Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * fix: mobile TimeUI — only show endtime, always expanded - Hide #mmgisTimeUIStartWrapper and StartWrapperFake on mobile via CSS - Force expanded state (addClass expanded + show) when toggling TimeUI on - CSS ensures #timeUI.active always shows expanded content on mobile Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * fix: mobile TimeUI opens in tool panel with header, end time, expanded rows - MobileTimeUIToggle now opens/closes the tool panel via ToolController_ - Closes any active tool before showing TimeUI - Forces expanded state when opening - CSS hides start time inputs, positions expanded content properly - Overrides absolute positioning of expanded content for tool panel flow Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * feat: rewrite separated tools system from jQuery to React components - Add separatedToolsList/activeSeparatedTools state to Zustand uiStore - Rewrite SeparatedTools.jsx with glassmorphism panels, CSS Module styling - Replace SepToolsContainer (setInterval hack) with SepToolButton/SepToolsSection - Remove ~170 lines of jQuery DOM construction from ToolController_.js - Fix hardcoded rgba(26,26,27,0.88) to theme-aware var(--color-a-rgb) - Remove separated tool entries from themeApplier.js - Remove separated tool overrides from FloatingElements.css - Move Legend CSS overrides from Toolbar.module.css to SeparatedTools.module.css - Remove jQuery active-state manipulation from IdentifierTool.js - Add store sync in Map_.js displayOnStart logic - Preserve all DOM IDs for backward compatibility (mmgisAPI, tool make()) Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * chore: bump version to 4.3.28-20260501 [version bump] * fix: TimeUI mobile checks — use Zustand store instead of L_.UserInterface_ L_.UserInterface_ is null when TimeUI.init() runs (TimeControl.init is called before L_.link sets UserInterface_). All 16 isMobile checks now read from useUIStore.getState().isMobile which is set at startup. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * chore: bump version to 4.3.29-20260501 [version bump] * fix: move displayOnStart logic from Map_.js to ToolController_.finalizeTools() - Map_ no longer references specific tools (LegendTool) - displayOnStart is now handled generically for all separated tools - Added DOM element polling (tryMake) to handle React render timing Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * revert: remove all TimeUI-related mobile changes Reverts TimeUI.js and BottomBar.js to development base. Restores #timeUI DOM removal in UserInterfaceBridge.fina(). Removes MobileTimeUIToggle component from Toolbar.jsx. Removes TimeUI mobile CSS overrides from UserInterfaceMobile_.css. Non-TimeUI refinements (toolbar height, scalebar positioning, etc.) preserved. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * simplify: remove DOM polling, use simple setTimeout(0) for auto-open LegendTool handles its own content lifecycle via subscribeOnLayerToggle. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * feat: mobile TimeUI — fix isMobile detection, staging container, toolbar toggle - TimeUI.js: import useUIStore and replace all 16 L_.UserInterface_?.isMobile checks with useUIStore.getState().isMobile (L_.UserInterface_ is null when TimeUI.init() runs, so mobile conditionals were dead code) - TimeUI.js: stage mobile #timeUI in hidden #timeUIMobileStaging instead of placing directly in #tools (which gets cleared by other tools) - UserInterfaceBridge.js: stop removing #timeUI from DOM on mobile - Toolbar.jsx: add MobileTimeUIToggle that moves #timeUI between staging and #tools, opens/closes tool panel via ToolController_ - BottomBar.js: hide TimeUI toggle from settings modal on mobile Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * fix: rescue #timeUI back to staging when another tool opens Subscribe to activeToolName changes — when a tool becomes active while TimeUI is showing, move #timeUI back to #timeUIMobileStaging before the new tool's make() clears #tools. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * chore: remove separatedTool/justification config toggles, fix review issues - Remove separatedTool checkbox and justification dropdown from Legend and Identifier config.json (these are always separated, not configurable) - Remove justification property/code from LegendTool.js, IdentifierTool.js - Simplify Globe_.js separated tool count (no justification filter) - Remove justification from Reference-Mission config blueprint - Update LegendTool help docs and Legend.md documentation - Add --color-a-rgb fallback (29,31,32) in SeparatedTools.module.css - Add display:none !important to .panelIdentifier to prevent 12px gap - Update e2e test comment Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * fix: circular import in TimeUI.js, toolbar/bottomFloatingBar position sync - TimeUI.js: replace top-level useUIStore import with lazy _getUIStore() accessor to avoid 'Cannot access useUIStore before initialization' circular import error at _remakeTimeSlider - SplitScreens.jsx: skip #timeUI reparenting observer on mobile (mobile uses MobileTimeUIToggle to manage #timeUI placement in #tools) - BottomElementPositioner.jsx: unify mobile transition to 0.3s (matches toolsWrapper and toolbar), guard pxIsTools against undefined - Toolbar.jsx: align toolbar transition to 0.3s ease-out Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * LegendTool fix empty message * chore: remove separated tools offset logic from Globe_.js Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * fix: skip _makeHistogram on mobile (no timeline slider, timestamps unset) _makeHistogram renders inside the timeline slider which doesn't exist on mobile. Without it, _timelineStartTimestamp is NaN, causing 'Invalid time value' RangeError at toISOString(). Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * fix: mobile TimeUI — populate expanded rows, fix Invalid date, fix panel height - TimeUI.js attachEvents: use _initialStart/_initialEnd on mobile (same as desktop) instead of L_.TimeControl_ which isn't set yet at init time. Fixes 'Invalid date' in start/end time inputs. - TimeUI.js fina: set expanded=true on mobile and call _populateExpandedRows() so year/month/day/hour rows actually render. Removed position:absolute and pointer-events:none overrides. - Toolbar.jsx: set tool panel height to 217px (TimeUI.height) instead of 45% viewport — matches actual TimeUI content height. - UserInterfaceMobile_.css: expanded content flows naturally (position:relative), hide start time inputs, allow overflow scroll, flex-wrap topbar. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * fix: mobile TimeUI justify-content center, restore toolbar border-bottom - Add justify-content: center to #mmgisTimeUIMain on mobile - Remove border-bottom: none override so toolbar keeps its default border Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * fix: mobile TimeUI overflow hidden, scalebar/compass fixed at 40px offset - #timeUI overflow-y: hidden (was auto, causing 2px scroll) - Scalebar/compass/map controls stay at fixed 40px offset (above toolbar) regardless of tool panel state — no longer shift up by pxIsTools Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Implement multi-tier knowledge architecture - Restructure AGENTS.md from 745 lines to 106 lines (Tier 1: essential context) - Create knowledge/ directory with 30+ wiki-style documentation files (Tier 2: deep knowledge) - Create knowledge/reference/ with 8 detailed reference files (Tier 3: lookup material) - Move AI-GETTING-STARTED.md and AI-DEVELOPMENT.md to knowledge/ - Update all file references in .specify/templates and blueprints - Create knowledge/README.md as the full knowledge base index - Create knowledge/reference/README.md as reference material index Three-tier knowledge discovery system: Tier 1: AGENTS.md (~106 lines) - scannable in <2 minutes Tier 2: knowledge/*.md - deep knowledge on architecture, tools, APIs, DB, infra Tier 3: knowledge/reference/*.md - coding conventions, API reference, troubleshooting Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * chore: bump version to 4.3.29-20260501 [version bump] * fix: mobile toolbar active button style matches desktop, fix icon alignment - All mobile toolbar buttons (ToolButton, MobileCoordButton, MobileTimeUIToggle) now use display:flex with align-items/justify-content center for proper vertical icon centering - MobileCoordButton: changed 'active' class to 'toolButtonActive' to match the global CSS active style (color-mmgis + color-i background) - Removed inline color overrides so CSS .toolButtonActive takes effect Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Add Devin knowledge notes from past MMGIS sessions Include curated lessons learned from past Devin sessions: - CI/CD: ignore build-arm64/amd64 failures, focus on required checks - Child sessions: no separate PRs when consolidating - ENV triple-update rule (.env, sample.env, ENVs.md) - Error handling: use logger with infrastructure_error for fatal startup errors - Path traversal security: stay within /Missions, handle subpath serving - Database initialization architecture and migration patterns - API authentication behavior across AUTH modes - Auto-generated MMGIS concept index Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * fix: mobile toolbar active button style, icon alignment, tool deactivation - Active toolbar buttons get desktop-matching margin (1px 0) and border-radius (8px) via .toolButton.toolButtonActive CSS rule - Removed line-height: 40px from .toolButton (flex centering handles vertical alignment, line-height was pushing icons up) - MobileCoordButton now watches activeToolName store and deactivates when another tool opens (fixes coords staying active) - MobileTimeUIToggle sets activeToolName='MobileTimeUI' when opening so coords/other buttons can detect it and deactivate - MobileTimeUIToggle clears activeToolName when closing - Both custom buttons skip self-deactivation via name check Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Fix cross-references: convert backtick refs to markdown links, add Devin knowledge notes Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * fix: mobile toolbar icon height 40px, button margins for active padding - #toolbar .toolButton i: height 40px fixes icon vertical alignment - #toolbar .toolButton: margin 0 2px gives spacing between buttons - #toolbar .toolButton.toolButtonActive: margin 1px 2px so active background has visual padding around the icon Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Rename knowledge/ to .knowledge/ for consistency with .specify/ convention Dot-prefix signals agent infrastructure (not source code), consistent with .specify/, .github/, .vscode/ conventions. All cross-references updated. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * fix: mobile toolbar icon line-height 40px, active button padding via height - Coord and TimeUI button <i> icons get line-height: 40px - Active buttons: height 34px (vs 40px toolbar) creates visual padding around the active background, centered by flex align-items - Buttons get margin: 0 1px for horizontal spacing Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Fix broken cross-reference: 06.2 -> 06.1-configure-rest-api.md Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * fix: close active tool + cancel deferred cleanup in MobileCoordButton/TimeUI - MobileCoordButton: call closeActiveTool() before opening, destroy _pendingCloseTool if set, increment _closeSeq to cancel deferred tools.innerHTML clear - MobileTimeUIToggle: same _pendingCloseTool + _closeSeq fix after closeActiveTool() to prevent 420ms deferred cleanup from wiping #timeUI after it's placed in #tools - Removed redundant closeActiveTool() from MobileCoordButton close path (was being called after destroy, not needed) Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * fix: active mobile toolbar buttons 34x34px (square) Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Drastically compress .knowledge/ — keep only unique agent content Remove 33 wiki files that duplicate docs/pages/ content. Remove 9 reference/ files derivable from source code. Keep only 5 files (down from 46): - AI-GETTING-STARTED.md (agent setup walkthrough) - AI-DEVELOPMENT.md (spec-kit workflow) - conventions-and-gotchas.md (naming, code style, common issues) - 12-devin-knowledge-notes.md (CI, auth, DB init, security gotchas) - README.md (index pointing to docs/pages/ for everything else) Principle: don't duplicate docs/ — only keep what's uniquely agent-optimized. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Rename to knowledge-notes.md, remove Devin branding and fork-specific CI section Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * fix: hide mmgis-map-logo on mobile Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Restore Database Safety Rules for AI Agents section in AGENTS.md Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * fix: shift compass and map scale 6px to the right (both mobile and desktop) Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Add back Important Instructions, code pattern templates, and detailed project structure - Important Instructions in AGENTS.md: MCP tools, hot-reload, Reference Mission - .knowledge/code-patterns.md: full directory tree with key directory annotations, plus copy-paste templates for Express routes, Sequelize models, Tool plugins, and WebSocket handlers Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Update project structure trees to reflect current filesystem Add missing directories: tests/, .knowledge/, .specify/, .github/, views/, private/, spice/, build/, examples/, scripts/middleware.js. Both abbreviated (AGENTS.md) and detailed (.knowledge/code-patterns.md) trees now match the actual repo layout. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * chore: bump version to 4.3.30-20260501 [version bump] * Add Layers_.js to project structure (key singleton L_) Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Fix project structure: correct API layout, frontend modules, code templates API/Backend/ uses feature-domain modules (Draw/, Users/, Config/, etc.) with setup.js + routes/ + models/ per feature — not APIs/ or Databases/. Frontend essence/ has Components/, Helpers/, LandingPage/, mmgisAPI/, services/ — not Ancillary/. Basics/ includes all singletons (Globe_, Formulae_, ToolController_, Viewer_, ComponentController_, Test_). Code templates updated to match actual patterns (setup.js, module.exports). Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * refactor: remove test infrastructure (Test_ module, testModules, DrawTool.test) - Delete src/essence/Basics/Test_/ directory - Delete src/essence/Tools/Draw/DrawTool.test.js - Remove Test_ import and Shift+T keydown handler from essence.js - Remove tests key from Draw tool config.json - Remove testModules generation logic from API/updateTools.js Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * chore: bump version to 4.3.31-20260501 [version bump] * style: move Cesium link button to top-right and match Leaflet zoom button styling - Change control container from top-left to top-right positioning - Update button size from 26px to 30px to match Leaflet zoom controls - Use CSS variables (--color-a, --color-f, --color-mmgis) instead of hardcoded colors - Add border-radius and box-shadow matching Leaflet control appearance - Update hover/inactive states to use themed colors Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * fix: anchor map logo to viewport instead of Leaflet map panel - Change MapLogo parent from .leaflet-bottom.leaflet-right to #main-container - Switch CSS position from absolute to fixed for viewport anchoring - Add explicit bottom-offset positioning in BottomElementPositioner (desktop) - Add explicit bottom-offset positioning in BottomElementPositioner (mobile) - Logo stays at viewport right edge regardless of open side panels - Retains smooth bottom offset transitions when bottom bar appears Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * docs: remove references to deleted test infrastructure (Test_, DrawTool.test) - Remove Test_/ from project structure in .knowledge/code-patterns.md - Remove DrawTool.test.js references from specs/006 spec, plan, and tasks - Remove Draw Tool Testing section from tasks.md Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * chore: bump version to 4.3.32-20260501 [version bump] * fix: append logo to document.body to avoid filter containing block #main-container has a CSS filter property which creates a new containing block per the CSS spec, causing position:fixed to behave like absolute. Appending to document.body ensures true viewport-fixed positioning. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * fix: prevent mobile topBarTitleName text wrapping by replacing max-width with white-space: nowrap Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * chore: bump version to 4.3.33-20260501 [version bump] * chore: bump version to 5.0.0 and update changelog Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * refactor(ui): move Screenshot/Fullscreen to BottomBar, About to TopBar kebab TopBar kebab menu now contains only Keyboard Shortcuts, Settings, and About (About now shows on both desktop and mobile). BottomBarReact now renders Screenshot, Fullscreen, and Copy Link buttons (top to bottom) following the same IconButton + Tooltip pattern. The About button has been removed from BottomBarReact. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * chore: bump version to 5.0.1-20260505 [version bump] * feat(mobile): enforce exclusive panel toggling on mobile in TopBar Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * chore: bump version to 5.0.1-20260505 [version bump] * style: reposition LithoSphere globe controls to match Leaflet/Cesium theme Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * chore: bump version to 5.0.1-20260505 [version bump] * feat(topbar): hide Viewer/Globe toggles based on configured panels Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * style(bottombar): reorder buttons (Copy Link, Screenshot, Fullscreen) and unify size Reorder the BottomBarReact buttons top-to-bottom to: Copy Link, Screenshot, Fullscreen. Move the 24x24 button sizing from the #topBarLink id selector in mmgis.css into the .barButton class in BottomBarReact.module.css so all three buttons share the same compact size as the original Copy Link button. Drop the now redundant #topBarLink rule. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * style(bottombar): increase padding-bottom to 12px and button margin to 3px 0 Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * style: rearrange globe controls — compass top-right circular, nav row, vertical column, panels open left Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * chore: bump version to 5.0.2-20260505 [version bump] * chore: bump version to 5.0.2-20260505 [version bump] * style: anchor observe settings panel right:34px and float nav hover panels Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * feat(theming): add 5 new themes, --color-shadow variable, and configure ThemePreview - Add Dark Terra, Dark Nebula, Dark Lunar, Dark Supernova, Light Botanical themes - Add --color-shadow CSS variable to every theme + :root fallback - Replace hardcoded rgba shadow colors with var(--color-shadow) in TopBar, Toolbar, SeparatedTools, ToolPanel, FloatingElements, Dropdown, Modal, and SplitScreens - Add Custom shadowcolor color picker in tab-ui-config and apply it via Stylize - Add ThemePreview component (configure/src) wired through Maker.js as a new 'themepreview' row type so the configure UI shows a live mini mockup of the selected theme Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * chore: bump version to 5.0.2-20260505 [version bump] * fix(configure/ThemePreview): tighten top spacing and live-preview Custom theme - Pull the preview up by 12px so the gap below the theme dropdown is tighter. - Read the Custom color pickers (look.primarycolor / secondarycolor / tertiarycolor / accentcolor / shadowcolor / topbarcolor / toolbarcolor / mapcolor) from the configuration and overlay them on Dark Default so the preview reflects Custom theme edits live, matching Stylize.js's runtime behavior. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * chore: bump version to 5.0.3-20260505 [version bump] * feat(themes): add Dark Heliosphere, Dark Monokai, and Light Solarized - Dark Heliosphere: deep night purple surface with corona-orange accent. - Dark Monokai: warm graphite surface with lime accent (Monokai-inspired). - Light Solarized: classic solarized base3/base02 with blue accent. Mirror added to configure/src/themes/themes.js for the ThemePreview, and the three names appended to the Color Theme dropdown options. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * fix(coordinates): respect time.initiallyOpen when live deep-link is set * chore: bump version to 5.0.3-20260505 [version bump] * refactor(theming): remove Custom theme + per-field color overrides - Drop the 'Custom' option from the Color Theme dropdown. - Remove all Custom Color Options (look.primarycolor, .secondarycolor, .tertiarycolor, .accentcolor, .bodycolor, .topbarcolor, .toolbarcolor, .mapcolor, .hightlightcolor, .shadowcolor) from tab-ui-config.json. - Strip the matching DOM/CSS-variable override block from Stylize.js; Stylize now just applies the selected preset theme (and the page logo). - Drop the empty bodycolor/topbarcolor/toolbarcolor/mapcolor/shadowcolor defaults from API/templates/config_template.js. - Simplify ThemePreview to render the selected preset directly — no Custom branch, no overlay logic. Preset themes cover all the looks we want and keep the configure surface much smaller. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * style(time-ui): round corners on TimeUI shell, action wrappers, mode dropdown - #timeUI: 10px border-radius on the outer time control bar. - #mmgisTimeUIActionsLeft / #mmgisTimeUIActionsRight: 10px border-radius so the action clusters sit as rounded chips. - #mmgisTimeUIActionsRight > div (excluding #mmgisTimeUIPresent): 10px border-radius on each action button so they match the wrapper. - #mmgisTimeUIModeDropdown: 40px height + 10px border-radius to align with the rest of the bar; clear the dropy default border-color so the rounded edge isn't outlined. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * chore: bump version to 5.0.4-20260505 [version bump] * feat(configure): mark light themes as (experimental) in dropdown label Light themes still have outstanding contrast issues, so flag them in the Color Theme dropdown without changing the saved value. - Maker dropdown now accepts options as either a plain string (current behavior) or { value, label } so the rendered label can differ from the persisted value. - tab-ui-config switches the six light themes to { value, label } form with '(experimental)' appended to the label only. Existing mission configs that already saved 'Light Default' etc. continue to match. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Fix timeUI border radius * fix(mobile): rescue #timeUI before tool make() destroys it Clicking Layers -> Time -> Layers -> Time on mobile caused the bottom panel to render LayersTool content with TimeUI height. The #timeUI DOM element was destroyed when LayersTool.make() called $('#tools').empty(), before the async React useEffect in MobileTimeUIToggle could rescue it to its staging container. - ToolController_.makeTool: synchronously move #timeUI from #tools back to #timeUIMobileStaging (and reset TimeUI store flags) on mobile, before invoking the new tool's make(). - MobileTimeUIToggle.handleClick: defensive fallback that re-initializes TimeUI if #timeUI no longer exists when the toggle is activated. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * fix(mobile): move re-initialized #timeUI from staging into #tools TimeUI.init() on mobile appends the new #timeUI to the hidden #timeUIMobileStaging container, so the fallback branch must also move it into #tools — otherwise the user sees an empty tool panel after the destroyed-element recovery path. Caught by Devin Review. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * fix(mobile): preserve #timeUI when Coordinates tool empties #tools On mobile, opening or closing the Coordinates tool runs $('#tools').empty() inside interfaceWithMMWebGIS / separateFromMMWebGIS. After the previous PR commits, clicking Coordinates -> Time still left the bottom panel empty because: - Coordinates.make() empties #tools while #timeUI is in staging (fine on its own), but the Coordinates teardown that fires after the user switches to the Time toggle (via MobileCoordButton's useEffect on activeToolName change) calls Coordinates.destroy() -> separateFromMMWebGIS(), which empties #tools wholesale and destroys the freshly-placed #timeUI. Add a rescueMobileTimeUI() helper that moves #timeUI from #tools back to #timeUIMobileStaging before each tools.empty() call in Coordinates, mirroring the rescue already done in ToolController_.makeTool(). Coordinates -> Time now correctly shows the TimeUI. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * fix(mobile): harden TimeUI fallback recovery (call fina(), de-dupe popovers) Devin Review correctly flagged that the safety-net path in MobileTimeUIToggle.handleClick was producing a partially-broken TimeUI when it fired: - TimeUI.init() unconditionally appends a new #timeUIPlayPopover_global to <body>, so a second init() left two elements with the same id. - TimeUI.init() alone does not wire up date pickers or per-button click handlers — that's TimeUI.fina()'s job. Without fina(), the recovered TimeUI rendered visually but Play / Previous / Next / Fit / Follow / Present / Expand were all dead. Before re-initializing, remove the stale #timeUIPlayPopover_global and #timeUIQuickSelectPopover_global divs to avoid duplicate ids. After the new #timeUI is moved into #tools, call TimeUI.fina() to populate the date pickers, attach the button click handlers, build the histogram, and populate the expanded mobile rows. Some delegated body/document handlers in attachEvents() will still be duplicated on this path; that is acceptable for a degraded recovery that should never run in practice now that the primary rescues in ToolController_.makeTool() and Coordinates.js cover all known paths. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * chore: bump version to 5.0.5-20260505 [version bump] * fix(mobile): Coordinates teardown only removes its own DOM The previous Coordinates fix was racing with itself: after the Time toggle synchronously moved #timeUI into #tools, MobileCoordButton's useEffect (triggered by the activeToolName change) ran on the next React tick and called L_.Coordinates.destroy(). That called separateFromMMWebGIS(), whose rescue moved #timeUI right back into the hidden staging div before tools.empty() — so the bottom panel ended up empty even though the time toggle was 'active'. Make separateFromMMWebGIS selective: only remove the Coordinates-specific DOM (#coordUIHeader and #CoordinatesDiv) instead of wiping all of #tools. Any other content already in #tools (e.g. #timeUI placed there by the Time toggle) is left alone. interfaceWithMMWebGIS still keeps the rescue + tools.empty() pattern on the open path so Coordinates always starts from a clean panel. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Bump DrawTool Temporal Drawings upward * chore: bump version to 5.0.6-20260505 [version bump] * chore: reset version to 5.0.0 Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * test(e2e): fix 9 pre-existing failures (test-only changes) - mmgis-api.spec.js: add form-fill login under AUTH=local; serialize describe to avoid concurrent-login race in the session store - coordinates.spec.js: TimeUI toggle was moved from the coordinates bar to the Settings modal; navigate via topbar kebab menu and assert the checkbox is rendered - widgets.spec.js: target .leaflet-control-zoom-in/-out specifically; the bare .leaflet-control-zoom class is also used by the home/reset control, so the original assertion was always false - sites.spec.js: scope panel selector to #toolPanel; both the toolbar icon and the panel container share id="SitesTool" Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * chore: bump version to 5.0.1-20260505 [version bump] * Revert "chore: bump version to 5.0.1-20260505 [version bump]" This reverts commit 4880204c1163be5d1d7fa96d14a0ed018c6f586c. * fix: prevent filter operator dropdown clipping in Layers panel Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * chore: bump version to 5.0.1-20260507 [version bump] * revert: keep dropy openUp:true for operator dropdowns Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Revert "chore: bump version to 5.0.1-20260507 [version bump]" This reverts commit d67c369ed437e47d658ae051348d377978dc48ed. * chore: bump version to 5.0.1-20260507 [version bump] * Revert "chore: bump version to 5.0.1-20260507 [version bump]" This reverts commit 29565ed829a55e9c241a789c9a3901d11cb5ca67. * chore: bump version to 5.0.1-20260507 [version bump] * Revert "chore: bump version to 5.0.1-20260507 [version bump]" This reverts commit 50e357604ebe9378564619b34c508b63cfb62c1d. * chore: bump version to 5.0.1-20260507 [version bump] * chore: bump version to 5.0.2-20260511 [version bump] * fix: render Globe panel immediately on first open without window resize Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * chore: bump version to 5.0.3-20260511 [version bump] * feat: add theme borders to panels and gradient backgrounds to splitters Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * chore: bump version to 5.0.4-20260511 [version bump] * style: bump split shadow gradient opacity to 0.4 Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * style: hotkeys modal 3-col grid + smaller leaflet zoom button gap Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * style: prevent hotkey label/value wrapping (ellipsis instead) Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * style: hotkeys modal single column, no wrap, no truncation Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * chore: bump version to 5.0.4-20260511 [version bump] * style: hotkeys modal dividers, invert title/subtitle colors, rename title, margin above subtitles Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * style: move splitter gradient to themed CSS class, restore hover feedback Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * chore: bump version to 5.0.5-20260511 [version bump] * style: hotkeys section titles use --color-h (matches rest of app) Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * chore: bump version to 5.0.5-20260511 [version bump] * fix: guard Globe_.init() inside rAF to prevent double instantiation Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * chore: bump version to 5.0.6-20260512 [version bump] * feat(plugins): per-plugin deps, lazy tool loading, validation, shared discovery Phase 3 — Plugin config validation + override warnings: - New API/pluginValidation.js with validatePluginConfig() for tool, component, and backend manifests. Validates required fields (name, paths), object/string shape of paths, dependencies block (npm/python.pip/python.conda), and warns on unknown top-level fields. - updateTools()/updateComponents() now skip invalid plugins and emit override warnings (matching what components already logged for tools). Phase 2 — Shared discoverPlugins() utility: - New API/pluginDiscovery.js consolidates the duplicated scanning logic from updateTools(), updateComponents(), and getBackendSetups(). Supports exact- name and substring container patterns, JSON/require/no-op loaders, and skips dot/underscore-prefixed dirs. - updateTools.js and setups.js refactored on top of the shared helper. Phase 1 — Per-plugin dependency declaration + build-time aggregation: - Plugin config.json may now declare a 'dependencies' block (npm + python.pip + python.conda). validatePluginConfig() also validates this shape. - New scripts/resolve-plugin-deps.js scans every tool/component/backend plugin and writes plugin-package.json, plugin-python-requirements.txt, and plugin-conda-deps.txt. Detects version conflicts and fails loudly. - scripts/build.js calls resolvePluginDeps() before updateTools(). - Dockerfile installs the aggregated plugin npm and pip deps after the root npm ci, using --no-save / --no-package-lock / --ignore-scripts so the root lockfile is untouched. - Animation tool migrated: ffmpeg/gifshot/html2canvas now declared in its config.json (kept in root package.json for transitional compat). - Generated artifacts gitignored. Phase 4 — Lazy loading of tool bundles: - updateTools() now emits dynamic-import arrow functions in the generated src/pre/tools.js with webpackChunkName hints so each tool is split into its own chunk (Kinds stays static because it's required synchronously). - ToolController_ gains ensureToolLoaded(name) and getLoadedTool(name) helpers and makeTool is async; init/finalizeTools and the separated-tool auto-open flow are updated to handle lazy modules. - Toolbar.jsx, SeparatedTools.jsx, SitesTool.js, and Layers_.js migrated to resolve LayersTool/etc. via the new helpers instead of poking toolModules directly. Tests & docs: - tests/fixtures/test-plugin-tools/{TestPlugin,InvalidPlugin,OverridePlugin} + tests/helpers/plugin-helpers.js with install/uninstall helpers. - New unit specs: pluginValidation, pluginDiscovery, updateTools, resolvePluginDeps, toolLazyLoading (57 tests, all passing). - CONTRIBUTING.md and docs/pages/Contributing/Contributing.md updated with schema, override behaviour, dependency declaration, build-time aggregation, conflict detection, and Docker integration. * chore: bump version to 5.0.7-20260512 [version bump] * fix: make Globe_.init() idempotent against multi-init Globe_.init() previously constructed a fresh GlobeRenderer on every call, which after #71 could happen multiple times for a single toggle (uiStore setTimeout + TopBar rAF). Each extra construction appends another .cesium-widget / _lithosphere_scene to #globe and leaves event handlers wired to dereferenced renderer state, which has been observed to break LithoSphere globe control buttons on configurations where the globe panel starts closed at boot. Add a top-of-init() guard that bails out and calls invalidateSize() when a renderer already exists. Single small, surgical change; no behavior change for the !L_.hasGlobe mock-swap path or for first-time construction. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * chore: bump version to 5.0.7-20260512 [version bump] * test(plugins): generate src/pre/tools.js on demand in toolLazyLoading spec The Playwright unit-tests CI step runs before `npm run build` so the gitignored `src/pre/tools.js` artifact does not yet exist on disk. Add a beforeAll hook that invokes `updateTools()` to regenerate it when missing, keeping the spec self-contained on both CI and dev machines that already built locally. * fix(tools): defensive getTool() + preload flag for cross-referenced tools Devin Review flagged a behavioural regression introduced by Phase 4: `ToolController_.getTool(name)` previously always returned a method- callable object (real module or `{ use(){} }` stub) because every tool was statically imported. After Phase 4, unresolved lazy loaders are `() => import(...)` functions, so callers like `Map_.getTool('InfoTool').use(...)`, `mmgisAPI.getTool('DrawTool').filesOn`, and `LegendTool` calling `LayersTool.populateCogScale` would crash with TypeError until the target tool was opened. Two fixes: 1. **Defensive getTool()**: Returns the legacy fallback stub when the tool module is still a lazy-loader function, and fires off `ensureToolLoaded(name)` in the background so subsequent calls see the resolved module. Prevents all crashes immediately. 2. **`preload: true` config flag**: Tools reached synchronously from other code paths (Info, Draw, Layers, Chemistry) now declare `"preload": true` in their `config.json`. `ToolController_.init()` calls `preloadEagerTools()` which fires `ensureToolLoaded` for every such tool right after toolbar setup — the chunks download in parallel with the rest of the page becoming interactive, so by the time a user clicks a feature the InfoTool module is already resolved. `validatePluginConfig` now accepts `preload` as a known tool field; CONTRIBUTING.md and docs/pages/Contributing/Contributing.md updated to document when to set it. Added a unit test covering the defensive getTool behaviour and the `preload` propagation through `toolConfigs`. * chore: bump version to 5.0.8-20260512 [version bump] * revert(plugins): remove Phase 4 lazy tool loading and preload mechanism Phase 4 lazy emission caused cross-tool consumers (Map_ feature-click, mmgisAPI, LegendTool) to receive raw '() => import(...)' arrows from ToolController_.getTool(), breaking InfoTool open. Reverting to the pre-Phase-4 behavior of static tool imports. - API/updateTools.js: generated src/pre/tools.js now emits 'import FooTool from ...' for every tool (Kinds stays static too). - ToolController_.js: getTool/makeTool back to sync; ensureToolLoaded, getLoadedTool, preloadEagerTools deleted; separated-tool auto-open flow simplified to direct sync calls. - Toolbar.jsx, SeparatedTools.jsx, Layers_.js: revert async/lazy patterns to sync ToolController_.toolModules[name] access. - API/pluginValidation.js: drop 'preload' from KNOWN_FIELDS. - src/essence/Tools/{Info,Draw,Layers,Chemistry}/config.json: drop 'preload: true'. - CONTRIBUTING.md + docs: remove preload documentation. - tests/unit/toolLazyLoading.spec.js: rewrite to verify static imports instead of lazy loaders. Also: log standard backends at startup (parity with plugin backends and with tools/components), so all backends now produce 'info Loaded backend: <name> from <container>' at boot. Phases 1-3 (per-plugin dependency aggregation, shared discoverPlugins, config validation + override warnings) are unaffected. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * feat(logger): new 'loaded' level (purple bg) for tool/component/backend startup Previously the 'Loaded tool/component/backend: X from Y' lines used the generic blue 'info' tag. They now use a dedicated 'loaded' level rendered with a purple (#a855f7) background, so plugin discovery output is visually distinct from other info messages. - API/logger.js: add 'loaded' case to the dev-mode switch (white text on purple bg) and suppress the redundant 'Caller:' echo for it (matches how 'info' and 'success' are handled). - API/updateTools.js: registerPlugin now logs at level 'loaded'. Drops the redundant 'Loaded ' prefix since the level tag now reads 'loaded'. - API/setups.js: standard and plugin backend startup logs use the new level, same drop of the 'Loaded ' prefix. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * feat(logs): 'Plugging in Tools/Components/Backends...' headings Rename the cyan banner messages in scripts/build.js and scripts/server.js from 'Updating Tools...' / 'Updating Components...' to 'Plugging in Tools...' / 'Plugging in Components...' so the headings match the plugin terminology used everywhere else (plugin-package.json, discoverPlugins, etc.). Also add a matching 'Plugging in Backends...' banner before setups.getBackendSetups() in scripts/server.js so backends get an equivalent title block to tools and components. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * style(logs): cyan banner lines lead with a blank line instead of trailing one Move the \n from the end to the beginning of every cyan banner in scripts/build.js and scripts/server.js (Resolving Plugin Dependencies, Plugging in Tools/Components/Backends, Validating Environment Variables, Starting websocket, Starting the development server) so that the blank line visually separates each section above its title rather than below it. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * feat(plugins): postinstall hook auto-installs plugin npm deps Plain `npm install` (or `npm ci`) on a fresh clone now resolves and installs every plugin's declared npm dependencies automatically, so new developers don't need to remember a second command. - scripts/install-plugin-deps.js (new): reads plugin-package.json, filters out deps already declared in root package.json with the same version specifier (no-op for the Animation transitional case), installs the remainder with `npm install --no-save --no-package-lock --ignore-scripts <pkg@ver> ...`. `--no-package-lock` keeps the root lockfile clean; `--ignore-scripts` prevents the inner install from re-entering postinstall and matches the Dockerfile. - package.json: postinstall guards against the Dockerfile's package.json-only layer (`scripts/` not copied yet) by checking for the two script files via `node -e` before invoking them. Adds a `plugins:install` npm script for on-demand runs. - CONTRIBUTING.md + docs/pages/Contributing/Contributing.md: replace the manual-install paragraph with a note about the postinstall hook and the filtering behavior. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * docs(plugins): manual Python install step for plugin pip/conda deps Document that the npm postinstall hook only handles plugin npm deps — plugin pip/conda deps must be installed manually after creating the Python environment, since there's no portable way to detect which interpreter or environment to target from a Node script. - CONTRIBUTING.md: added a 'For local development ... Python' block with the explicit `node scripts/resolve-plugin-deps.js` + `micromamba run -n mmgis pip install -r plugin-python-requirements.txt` + optional conda install commands. - docs/pages/Contributing/Contributing.md: matching short blurb in the user-facing docs. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * docs(install): plugin Python install step in Installation.md Add a numbered step in docs/pages/Setup/Installation/Installation.md's Setup sequence (right after `micromamba activate mmgis`) with the `pip install -r plugin-python-requirements.txt` and optional `micromamba install --file plugin-conda-deps.txt` commands, so non-Docker installs have the step in their main flow rather than buried in CONTRIBUTING.md. Also adds a pointer from the Python Environment section earlier in the same file (after the env-create + activ…
…982) * Guard Legend empty state message to only show when panel is active Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Fix TimeUI dropdown covered by toolPanel: remove splitscreens stacking context #splitscreens had z-index:1 which created a stacking context, confining its children (including bottomFloatingBar at z-index:1500) to that context. Since ToolPanel (z-index:1400) was a sibling outside splitscreens, it painted above all splitscreens children regardless of their internal z-index values. Fix: change #splitscreens z-index from 1 to auto so it no longer creates a stacking context. Now bottomFloatingBar (1500) participates in the same stacking context as ToolPanel (1400), and 1500 > 1400 means the TimeUI dropdown correctly paints above the tool panel. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Migrate ~69 CursorInfo toast-like calls to proper Toast component - Replace CursorInfo.update() toast-like calls with Toast.info/success/warning/error - Preserve message colors: blue→info, green→success, yellow→warning, red→error - Keep 12 legitimate cursor-following CursorInfo calls unchanged - Files migrated: DrawTool.js, DrawTool_Files.js, DrawTool_FileModal.js, DrawTool_Templater.js, DrawTool_SetOperations.js, DrawTool_Drawing.js, DrawTool_Editing.js, DrawTool_Shapes.js, LayersTool.js, ShadeTool.js, chemistrychart.js - Fix Devin Review: Change misleading 'Bad token' to 'Login failed' in users.js - Normalize line endings (CRLF→LF) in affected files Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Fix Toast.js missing from git, CoordinatesDiv z-index, Legend duplicate ID, topBar padding - Add Toast.js to version control (was untracked, causing webpack module error) - Bump CoordinatesDiv z-index from 20 to 1001 (was hidden behind splitscreens children after z-index:auto change) - Fix Legend duplicate ID: separated tool icon was #LegendTool, same as content container div, causing empty message to appear in button instead of panel - Add hasStatus class to #topBarMain when statusIndicator is active, setting #topBarTitleName padding-left to 0 Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Fix Legend empty message: scope selector to content container via targetId Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Move Toast.js to design-system, fix --color-a3 text contrast, update AGENTS.md - Move Toast.js from UserInterface_/components/Toast/ to design-system/components/Toast/ (generic component belongs in design-system, not MMGIS-specific UserInterface_) - Update all 11 Toast import paths to new location - Bump --color-a3 in 5 dark themes to pass WCAG AA 4.5:1 contrast for text: Dark Default #747c81→#81888d, Dark Blue #64748b→#738399, Dark Warm #8b7a5e→#908064, Dark Mars #8a6a60→#98796f, Dark Midnight #606088→#7a7a9e - Update AGENTS.md: document design-system/ vs UserInterface_/ distinction in project structure and Key Directories Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Fix topBarTitleName padding override: increase specificity and add !important Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Fix IdentifierTool deactivation: update icon ID reference in separateFromMMWebGIS Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Hide toolPanelDrag when no tool is open - Add setToolPanelDragVisible(false) to closeActiveTool() (was only in makeTool toggle-off path) - Also guard drag handle display on isOpen (toolPanelWidth > 0) as safety net Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Add hover effect to MMGIS logo (opacity + brightness transition) Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Fix MMGIS logo hover: keep full opacity, use subtle background highlight instead Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Selective tile fade: fade on pan/zoom, instant on refresh/reload - Remove blanket _fadeAnimated toggle from toggleTimeUI (was killing fade for all tiles while TimeUI was open) - Monkey-patch GridLayer.redraw, TileLayer.setUrl, and GridLayer._tileReady to suppress fade via a transient _suppressTileFade map flag - Set _suppressTileFade in reloadTimeLayers for time-driven reloads - Flag auto-clears after 300ms so pan/zoom tiles still get the nice fade - Install pbf dependency (required by CesiumMVTLayer from #942 merge) Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Per-layer fade control: time-enabled + shade/viewshed layers never fade - Replace transient map-level _suppressTileFade with per-layer _noFade flag - Patch GridLayer._tileReady to check _noFade on the layer instance - Set _noFade on time-enabled tile layers and data/GL layers at creation - Set _noFade on Shade and Viewshed tool GL layers - Non-time-enabled base imagery tiles still fade normally on pan/zoom Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * fix: mobile UI improvements — move hamburger to right menu, show panel toggles, isMobile-driven toolbar layout, desktop-matching scalebar/compass - Remove left hamburger menu (#topBarMenu), move BottomBar items into top-right kebab dropdown menu for both mobile and desktop - Show panel toggles (Viewer/Map/Globe) and account/login UI in mobile topbar's #topBarRight - Move toolbar horizontal layout CSS from @media breakpoints to UserInterfaceMobile_.css (loaded only when isMobile flag is true) - Remove #mapTopBar @media rule from mmgis.css, add to mobile CSS - Remove mobile-only simplified scalebar rendering; use full desktop scalebar with both large and small axes on all viewports - Remove display:none on .leaflet-control-scalefactor in mobile CSS - Remove #loginDiv display:none from mobile CSS (React overlay handles it) - Simplify BottomBarReact container styles (no more absolute positioning) Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * chore: bump version to 4.3.28-20260430 [version bump] * fix: mobile toolbar 40px height, bottomFloatingBar flush, timeUI toggle, scalebar position, hide hotkeys - Toolbar height 40px, toolButton width 40px, no border-bottom - toolcontroller_incdiv: no padding-bottom, overflow-y hidden - bottomFloatingBar: no border-radius, left/right/bottom = 0 - Add MobileTimeUIToggle button on far right of toolbar - Hide Keyboard Shortcuts from kebab menu on mobile - Fix scalebar positioning (remove top:48px override in UserInterfaceBridge) - Set mobileTopSize/topSize to 40 (splitscreens top = 40px, not 50px) Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * fix: mobile topBar padding-left 34px Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * fix: update MobileCoordButton topBar paddingLeft from 80px to 34px Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * fix: MobileTimeUIToggle — inline toggle logic, float right, hide from settings on mobile - Replace broken Coordinates.toggleTimeUI() call with direct jQuery/store toggle - Float time button right in toolbar - Hide Time UI toggle from settings modal on mobile (toolbar has it) Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * fix: push scalebar/compass/scale up 40px on mobile, keep #timeUI in DOM - BottomElementPositioner: position mapToolBar, leaflet-bottom-left/right 40px above bottom on mobile (above toolbar) - Stop removing #timeUI from DOM on mobile so MobileTimeUIToggle works Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * fix: mobile TimeUI — only show endtime, always expanded - Hide #mmgisTimeUIStartWrapper and StartWrapperFake on mobile via CSS - Force expanded state (addClass expanded + show) when toggling TimeUI on - CSS ensures #timeUI.active always shows expanded content on mobile Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * fix: mobile TimeUI opens in tool panel with header, end time, expanded rows - MobileTimeUIToggle now opens/closes the tool panel via ToolController_ - Closes any active tool before showing TimeUI - Forces expanded state when opening - CSS hides start time inputs, positions expanded content properly - Overrides absolute positioning of expanded content for tool panel flow Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * feat: rewrite separated tools system from jQuery to React components - Add separatedToolsList/activeSeparatedTools state to Zustand uiStore - Rewrite SeparatedTools.jsx with glassmorphism panels, CSS Module styling - Replace SepToolsContainer (setInterval hack) with SepToolButton/SepToolsSection - Remove ~170 lines of jQuery DOM construction from ToolController_.js - Fix hardcoded rgba(26,26,27,0.88) to theme-aware var(--color-a-rgb) - Remove separated tool entries from themeApplier.js - Remove separated tool overrides from FloatingElements.css - Move Legend CSS overrides from Toolbar.module.css to SeparatedTools.module.css - Remove jQuery active-state manipulation from IdentifierTool.js - Add store sync in Map_.js displayOnStart logic - Preserve all DOM IDs for backward compatibility (mmgisAPI, tool make()) Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * chore: bump version to 4.3.28-20260501 [version bump] * fix: TimeUI mobile checks — use Zustand store instead of L_.UserInterface_ L_.UserInterface_ is null when TimeUI.init() runs (TimeControl.init is called before L_.link sets UserInterface_). All 16 isMobile checks now read from useUIStore.getState().isMobile which is set at startup. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * chore: bump version to 4.3.29-20260501 [version bump] * fix: move displayOnStart logic from Map_.js to ToolController_.finalizeTools() - Map_ no longer references specific tools (LegendTool) - displayOnStart is now handled generically for all separated tools - Added DOM element polling (tryMake) to handle React render timing Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * revert: remove all TimeUI-related mobile changes Reverts TimeUI.js and BottomBar.js to development base. Restores #timeUI DOM removal in UserInterfaceBridge.fina(). Removes MobileTimeUIToggle component from Toolbar.jsx. Removes TimeUI mobile CSS overrides from UserInterfaceMobile_.css. Non-TimeUI refinements (toolbar height, scalebar positioning, etc.) preserved. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * simplify: remove DOM polling, use simple setTimeout(0) for auto-open LegendTool handles its own content lifecycle via subscribeOnLayerToggle. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * feat: mobile TimeUI — fix isMobile detection, staging container, toolbar toggle - TimeUI.js: import useUIStore and replace all 16 L_.UserInterface_?.isMobile checks with useUIStore.getState().isMobile (L_.UserInterface_ is null when TimeUI.init() runs, so mobile conditionals were dead code) - TimeUI.js: stage mobile #timeUI in hidden #timeUIMobileStaging instead of placing directly in #tools (which gets cleared by other tools) - UserInterfaceBridge.js: stop removing #timeUI from DOM on mobile - Toolbar.jsx: add MobileTimeUIToggle that moves #timeUI between staging and #tools, opens/closes tool panel via ToolController_ - BottomBar.js: hide TimeUI toggle from settings modal on mobile Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * fix: rescue #timeUI back to staging when another tool opens Subscribe to activeToolName changes — when a tool becomes active while TimeUI is showing, move #timeUI back to #timeUIMobileStaging before the new tool's make() clears #tools. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * chore: remove separatedTool/justification config toggles, fix review issues - Remove separatedTool checkbox and justification dropdown from Legend and Identifier config.json (these are always separated, not configurable) - Remove justification property/code from LegendTool.js, IdentifierTool.js - Simplify Globe_.js separated tool count (no justification filter) - Remove justification from Reference-Mission config blueprint - Update LegendTool help docs and Legend.md documentation - Add --color-a-rgb fallback (29,31,32) in SeparatedTools.module.css - Add display:none !important to .panelIdentifier to prevent 12px gap - Update e2e test comment Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * fix: circular import in TimeUI.js, toolbar/bottomFloatingBar position sync - TimeUI.js: replace top-level useUIStore import with lazy _getUIStore() accessor to avoid 'Cannot access useUIStore before initialization' circular import error at _remakeTimeSlider - SplitScreens.jsx: skip #timeUI reparenting observer on mobile (mobile uses MobileTimeUIToggle to manage #timeUI placement in #tools) - BottomElementPositioner.jsx: unify mobile transition to 0.3s (matches toolsWrapper and toolbar), guard pxIsTools against undefined - Toolbar.jsx: align toolbar transition to 0.3s ease-out Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * LegendTool fix empty message * chore: remove separated tools offset logic from Globe_.js Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * fix: skip _makeHistogram on mobile (no timeline slider, timestamps unset) _makeHistogram renders inside the timeline slider which doesn't exist on mobile. Without it, _timelineStartTimestamp is NaN, causing 'Invalid time value' RangeError at toISOString(). Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * fix: mobile TimeUI — populate expanded rows, fix Invalid date, fix panel height - TimeUI.js attachEvents: use _initialStart/_initialEnd on mobile (same as desktop) instead of L_.TimeControl_ which isn't set yet at init time. Fixes 'Invalid date' in start/end time inputs. - TimeUI.js fina: set expanded=true on mobile and call _populateExpandedRows() so year/month/day/hour rows actually render. Removed position:absolute and pointer-events:none overrides. - Toolbar.jsx: set tool panel height to 217px (TimeUI.height) instead of 45% viewport — matches actual TimeUI content height. - UserInterfaceMobile_.css: expanded content flows naturally (position:relative), hide start time inputs, allow overflow scroll, flex-wrap topbar. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * fix: mobile TimeUI justify-content center, restore toolbar border-bottom - Add justify-content: center to #mmgisTimeUIMain on mobile - Remove border-bottom: none override so toolbar keeps its default border Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * fix: mobile TimeUI overflow hidden, scalebar/compass fixed at 40px offset - #timeUI overflow-y: hidden (was auto, causing 2px scroll) - Scalebar/compass/map controls stay at fixed 40px offset (above toolbar) regardless of tool panel state — no longer shift up by pxIsTools Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Implement multi-tier knowledge architecture - Restructure AGENTS.md from 745 lines to 106 lines (Tier 1: essential context) - Create knowledge/ directory with 30+ wiki-style documentation files (Tier 2: deep knowledge) - Create knowledge/reference/ with 8 detailed reference files (Tier 3: lookup material) - Move AI-GETTING-STARTED.md and AI-DEVELOPMENT.md to knowledge/ - Update all file references in .specify/templates and blueprints - Create knowledge/README.md as the full knowledge base index - Create knowledge/reference/README.md as reference material index Three-tier knowledge discovery system: Tier 1: AGENTS.md (~106 lines) - scannable in <2 minutes Tier 2: knowledge/*.md - deep knowledge on architecture, tools, APIs, DB, infra Tier 3: knowledge/reference/*.md - coding conventions, API reference, troubleshooting Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * chore: bump version to 4.3.29-20260501 [version bump] * fix: mobile toolbar active button style matches desktop, fix icon alignment - All mobile toolbar buttons (ToolButton, MobileCoordButton, MobileTimeUIToggle) now use display:flex with align-items/justify-content center for proper vertical icon centering - MobileCoordButton: changed 'active' class to 'toolButtonActive' to match the global CSS active style (color-mmgis + color-i background) - Removed inline color overrides so CSS .toolButtonActive takes effect Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Add Devin knowledge notes from past MMGIS sessions Include curated lessons learned from past Devin sessions: - CI/CD: ignore build-arm64/amd64 failures, focus on required checks - Child sessions: no separate PRs when consolidating - ENV triple-update rule (.env, sample.env, ENVs.md) - Error handling: use logger with infrastructure_error for fatal startup errors - Path traversal security: stay within /Missions, handle subpath serving - Database initialization architecture and migration patterns - API authentication behavior across AUTH modes - Auto-generated MMGIS concept index Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * fix: mobile toolbar active button style, icon alignment, tool deactivation - Active toolbar buttons get desktop-matching margin (1px 0) and border-radius (8px) via .toolButton.toolButtonActive CSS rule - Removed line-height: 40px from .toolButton (flex centering handles vertical alignment, line-height was pushing icons up) - MobileCoordButton now watches activeToolName store and deactivates when another tool opens (fixes coords staying active) - MobileTimeUIToggle sets activeToolName='MobileTimeUI' when opening so coords/other buttons can detect it and deactivate - MobileTimeUIToggle clears activeToolName when closing - Both custom buttons skip self-deactivation via name check Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Fix cross-references: convert backtick refs to markdown links, add Devin knowledge notes Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * fix: mobile toolbar icon height 40px, button margins for active padding - #toolbar .toolButton i: height 40px fixes icon vertical alignment - #toolbar .toolButton: margin 0 2px gives spacing between buttons - #toolbar .toolButton.toolButtonActive: margin 1px 2px so active background has visual padding around the icon Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Rename knowledge/ to .knowledge/ for consistency with .specify/ convention Dot-prefix signals agent infrastructure (not source code), consistent with .specify/, .github/, .vscode/ conventions. All cross-references updated. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * fix: mobile toolbar icon line-height 40px, active button padding via height - Coord and TimeUI button <i> icons get line-height: 40px - Active buttons: height 34px (vs 40px toolbar) creates visual padding around the active background, centered by flex align-items - Buttons get margin: 0 1px for horizontal spacing Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Fix broken cross-reference: 06.2 -> 06.1-configure-rest-api.md Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * fix: close active tool + cancel deferred cleanup in MobileCoordButton/TimeUI - MobileCoordButton: call closeActiveTool() before opening, destroy _pendingCloseTool if set, increment _closeSeq to cancel deferred tools.innerHTML clear - MobileTimeUIToggle: same _pendingCloseTool + _closeSeq fix after closeActiveTool() to prevent 420ms deferred cleanup from wiping #timeUI after it's placed in #tools - Removed redundant closeActiveTool() from MobileCoordButton close path (was being called after destroy, not needed) Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * fix: active mobile toolbar buttons 34x34px (square) Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Drastically compress .knowledge/ — keep only unique agent content Remove 33 wiki files that duplicate docs/pages/ content. Remove 9 reference/ files derivable from source code. Keep only 5 files (down from 46): - AI-GETTING-STARTED.md (agent setup walkthrough) - AI-DEVELOPMENT.md (spec-kit workflow) - conventions-and-gotchas.md (naming, code style, common issues) - 12-devin-knowledge-notes.md (CI, auth, DB init, security gotchas) - README.md (index pointing to docs/pages/ for everything else) Principle: don't duplicate docs/ — only keep what's uniquely agent-optimized. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Rename to knowledge-notes.md, remove Devin branding and fork-specific CI section Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * fix: hide mmgis-map-logo on mobile Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Restore Database Safety Rules for AI Agents section in AGENTS.md Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * fix: shift compass and map scale 6px to the right (both mobile and desktop) Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Add back Important Instructions, code pattern templates, and detailed project structure - Important Instructions in AGENTS.md: MCP tools, hot-reload, Reference Mission - .knowledge/code-patterns.md: full directory tree with key directory annotations, plus copy-paste templates for Express routes, Sequelize models, Tool plugins, and WebSocket handlers Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Update project structure trees to reflect current filesystem Add missing directories: tests/, .knowledge/, .specify/, .github/, views/, private/, spice/, build/, examples/, scripts/middleware.js. Both abbreviated (AGENTS.md) and detailed (.knowledge/code-patterns.md) trees now match the actual repo layout. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * chore: bump version to 4.3.30-20260501 [version bump] * Add Layers_.js to project structure (key singleton L_) Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Fix project structure: correct API layout, frontend modules, code templates API/Backend/ uses feature-domain modules (Draw/, Users/, Config/, etc.) with setup.js + routes/ + models/ per feature — not APIs/ or Databases/. Frontend essence/ has Components/, Helpers/, LandingPage/, mmgisAPI/, services/ — not Ancillary/. Basics/ includes all singletons (Globe_, Formulae_, ToolController_, Viewer_, ComponentController_, Test_). Code templates updated to match actual patterns (setup.js, module.exports). Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * refactor: remove test infrastructure (Test_ module, testModules, DrawTool.test) - Delete src/essence/Basics/Test_/ directory - Delete src/essence/Tools/Draw/DrawTool.test.js - Remove Test_ import and Shift+T keydown handler from essence.js - Remove tests key from Draw tool config.json - Remove testModules generation logic from API/updateTools.js Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * chore: bump version to 4.3.31-20260501 [version bump] * style: move Cesium link button to top-right and match Leaflet zoom button styling - Change control container from top-left to top-right positioning - Update button size from 26px to 30px to match Leaflet zoom controls - Use CSS variables (--color-a, --color-f, --color-mmgis) instead of hardcoded colors - Add border-radius and box-shadow matching Leaflet control appearance - Update hover/inactive states to use themed colors Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * fix: anchor map logo to viewport instead of Leaflet map panel - Change MapLogo parent from .leaflet-bottom.leaflet-right to #main-container - Switch CSS position from absolute to fixed for viewport anchoring - Add explicit bottom-offset positioning in BottomElementPositioner (desktop) - Add explicit bottom-offset positioning in BottomElementPositioner (mobile) - Logo stays at viewport right edge regardless of open side panels - Retains smooth bottom offset transitions when bottom bar appears Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * docs: remove references to deleted test infrastructure (Test_, DrawTool.test) - Remove Test_/ from project structure in .knowledge/code-patterns.md - Remove DrawTool.test.js references from specs/006 spec, plan, and tasks - Remove Draw Tool Testing section from tasks.md Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * chore: bump version to 4.3.32-20260501 [version bump] * fix: append logo to document.body to avoid filter containing block #main-container has a CSS filter property which creates a new containing block per the CSS spec, causing position:fixed to behave like absolute. Appending to document.body ensures true viewport-fixed positioning. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * fix: prevent mobile topBarTitleName text wrapping by replacing max-width with white-space: nowrap Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * chore: bump version to 4.3.33-20260501 [version bump] * chore: bump version to 5.0.0 and update changelog Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * refactor(ui): move Screenshot/Fullscreen to BottomBar, About to TopBar kebab TopBar kebab menu now contains only Keyboard Shortcuts, Settings, and About (About now shows on both desktop and mobile). BottomBarReact now renders Screenshot, Fullscreen, and Copy Link buttons (top to bottom) following the same IconButton + Tooltip pattern. The About button has been removed from BottomBarReact. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * chore: bump version to 5.0.1-20260505 [version bump] * feat(mobile): enforce exclusive panel toggling on mobile in TopBar Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * chore: bump version to 5.0.1-20260505 [version bump] * style: reposition LithoSphere globe controls to match Leaflet/Cesium theme Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * chore: bump version to 5.0.1-20260505 [version bump] * feat(topbar): hide Viewer/Globe toggles based on configured panels Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * style(bottombar): reorder buttons (Copy Link, Screenshot, Fullscreen) and unify size Reorder the BottomBarReact buttons top-to-bottom to: Copy Link, Screenshot, Fullscreen. Move the 24x24 button sizing from the #topBarLink id selector in mmgis.css into the .barButton class in BottomBarReact.module.css so all three buttons share the same compact size as the original Copy Link button. Drop the now redundant #topBarLink rule. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * style(bottombar): increase padding-bottom to 12px and button margin to 3px 0 Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * style: rearrange globe controls — compass top-right circular, nav row, vertical column, panels open left Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * chore: bump version to 5.0.2-20260505 [version bump] * chore: bump version to 5.0.2-20260505 [version bump] * style: anchor observe settings panel right:34px and float nav hover panels Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * feat(theming): add 5 new themes, --color-shadow variable, and configure ThemePreview - Add Dark Terra, Dark Nebula, Dark Lunar, Dark Supernova, Light Botanical themes - Add --color-shadow CSS variable to every theme + :root fallback - Replace hardcoded rgba shadow colors with var(--color-shadow) in TopBar, Toolbar, SeparatedTools, ToolPanel, FloatingElements, Dropdown, Modal, and SplitScreens - Add Custom shadowcolor color picker in tab-ui-config and apply it via Stylize - Add ThemePreview component (configure/src) wired through Maker.js as a new 'themepreview' row type so the configure UI shows a live mini mockup of the selected theme Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * chore: bump version to 5.0.2-20260505 [version bump] * fix(configure/ThemePreview): tighten top spacing and live-preview Custom theme - Pull the preview up by 12px so the gap below the theme dropdown is tighter. - Read the Custom color pickers (look.primarycolor / secondarycolor / tertiarycolor / accentcolor / shadowcolor / topbarcolor / toolbarcolor / mapcolor) from the configuration and overlay them on Dark Default so the preview reflects Custom theme edits live, matching Stylize.js's runtime behavior. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * chore: bump version to 5.0.3-20260505 [version bump] * feat(themes): add Dark Heliosphere, Dark Monokai, and Light Solarized - Dark Heliosphere: deep night purple surface with corona-orange accent. - Dark Monokai: warm graphite surface with lime accent (Monokai-inspired). - Light Solarized: classic solarized base3/base02 with blue accent. Mirror added to configure/src/themes/themes.js for the ThemePreview, and the three names appended to the Color Theme dropdown options. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * fix(coordinates): respect time.initiallyOpen when live deep-link is set * chore: bump version to 5.0.3-20260505 [version bump] * refactor(theming): remove Custom theme + per-field color overrides - Drop the 'Custom' option from the Color Theme dropdown. - Remove all Custom Color Options (look.primarycolor, .secondarycolor, .tertiarycolor, .accentcolor, .bodycolor, .topbarcolor, .toolbarcolor, .mapcolor, .hightlightcolor, .shadowcolor) from tab-ui-config.json. - Strip the matching DOM/CSS-variable override block from Stylize.js; Stylize now just applies the selected preset theme (and the page logo). - Drop the empty bodycolor/topbarcolor/toolbarcolor/mapcolor/shadowcolor defaults from API/templates/config_template.js. - Simplify ThemePreview to render the selected preset directly — no Custom branch, no overlay logic. Preset themes cover all the looks we want and keep the configure surface much smaller. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * style(time-ui): round corners on TimeUI shell, action wrappers, mode dropdown - #timeUI: 10px border-radius on the outer time control bar. - #mmgisTimeUIActionsLeft / #mmgisTimeUIActionsRight: 10px border-radius so the action clusters sit as rounded chips. - #mmgisTimeUIActionsRight > div (excluding #mmgisTimeUIPresent): 10px border-radius on each action button so they match the wrapper. - #mmgisTimeUIModeDropdown: 40px height + 10px border-radius to align with the rest of the bar; clear the dropy default border-color so the rounded edge isn't outlined. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * chore: bump version to 5.0.4-20260505 [version bump] * feat(configure): mark light themes as (experimental) in dropdown label Light themes still have outstanding contrast issues, so flag them in the Color Theme dropdown without changing the saved value. - Maker dropdown now accepts options as either a plain string (current behavior) or { value, label } so the rendered label can differ from the persisted value. - tab-ui-config switches the six light themes to { value, label } form with '(experimental)' appended to the label only. Existing mission configs that already saved 'Light Default' etc. continue to match. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Fix timeUI border radius * fix(mobile): rescue #timeUI before tool make() destroys it Clicking Layers -> Time -> Layers -> Time on mobile caused the bottom panel to render LayersTool content with TimeUI height. The #timeUI DOM element was destroyed when LayersTool.make() called $('#tools').empty(), before the async React useEffect in MobileTimeUIToggle could rescue it to its staging container. - ToolController_.makeTool: synchronously move #timeUI from #tools back to #timeUIMobileStaging (and reset TimeUI store flags) on mobile, before invoking the new tool's make(). - MobileTimeUIToggle.handleClick: defensive fallback that re-initializes TimeUI if #timeUI no longer exists when the toggle is activated. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * fix(mobile): move re-initialized #timeUI from staging into #tools TimeUI.init() on mobile appends the new #timeUI to the hidden #timeUIMobileStaging container, so the fallback branch must also move it into #tools — otherwise the user sees an empty tool panel after the destroyed-element recovery path. Caught by Devin Review. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * fix(mobile): preserve #timeUI when Coordinates tool empties #tools On mobile, opening or closing the Coordinates tool runs $('#tools').empty() inside interfaceWithMMWebGIS / separateFromMMWebGIS. After the previous PR commits, clicking Coordinates -> Time still left the bottom panel empty because: - Coordinates.make() empties #tools while #timeUI is in staging (fine on its own), but the Coordinates teardown that fires after the user switches to the Time toggle (via MobileCoordButton's useEffect on activeToolName change) calls Coordinates.destroy() -> separateFromMMWebGIS(), which empties #tools wholesale and destroys the freshly-placed #timeUI. Add a rescueMobileTimeUI() helper that moves #timeUI from #tools back to #timeUIMobileStaging before each tools.empty() call in Coordinates, mirroring the rescue already done in ToolController_.makeTool(). Coordinates -> Time now correctly shows the TimeUI. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * fix(mobile): harden TimeUI fallback recovery (call fina(), de-dupe popovers) Devin Review correctly flagged that the safety-net path in MobileTimeUIToggle.handleClick was producing a partially-broken TimeUI when it fired: - TimeUI.init() unconditionally appends a new #timeUIPlayPopover_global to <body>, so a second init() left two elements with the same id. - TimeUI.init() alone does not wire up date pickers or per-button click handlers — that's TimeUI.fina()'s job. Without fina(), the recovered TimeUI rendered visually but Play / Previous / Next / Fit / Follow / Present / Expand were all dead. Before re-initializing, remove the stale #timeUIPlayPopover_global and #timeUIQuickSelectPopover_global divs to avoid duplicate ids. After the new #timeUI is moved into #tools, call TimeUI.fina() to populate the date pickers, attach the button click handlers, build the histogram, and populate the expanded mobile rows. Some delegated body/document handlers in attachEvents() will still be duplicated on this path; that is acceptable for a degraded recovery that should never run in practice now that the primary rescues in ToolController_.makeTool() and Coordinates.js cover all known paths. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * chore: bump version to 5.0.5-20260505 [version bump] * fix(mobile): Coordinates teardown only removes its own DOM The previous Coordinates fix was racing with itself: after the Time toggle synchronously moved #timeUI into #tools, MobileCoordButton's useEffect (triggered by the activeToolName change) ran on the next React tick and called L_.Coordinates.destroy(). That called separateFromMMWebGIS(), whose rescue moved #timeUI right back into the hidden staging div before tools.empty() — so the bottom panel ended up empty even though the time toggle was 'active'. Make separateFromMMWebGIS selective: only remove the Coordinates-specific DOM (#coordUIHeader and #CoordinatesDiv) instead of wiping all of #tools. Any other content already in #tools (e.g. #timeUI placed there by the Time toggle) is left alone. interfaceWithMMWebGIS still keeps the rescue + tools.empty() pattern on the open path so Coordinates always starts from a clean panel. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Bump DrawTool Temporal Drawings upward * chore: bump version to 5.0.6-20260505 [version bump] * chore: reset version to 5.0.0 Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * test(e2e): fix 9 pre-existing failures (test-only changes) - mmgis-api.spec.js: add form-fill login under AUTH=local; serialize describe to avoid concurrent-login race in the session store - coordinates.spec.js: TimeUI toggle was moved from the coordinates bar to the Settings modal; navigate via topbar kebab menu and assert the checkbox is rendered - widgets.spec.js: target .leaflet-control-zoom-in/-out specifically; the bare .leaflet-control-zoom class is also used by the home/reset control, so the original assertion was always false - sites.spec.js: scope panel selector to #toolPanel; both the toolbar icon and the panel container share id="SitesTool" Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * chore: bump version to 5.0.1-20260505 [version bump] * Revert "chore: bump version to 5.0.1-20260505 [version bump]" This reverts commit 4880204c1163be5d1d7fa96d14a0ed018c6f586c. * fix: prevent filter operator dropdown clipping in Layers panel Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * chore: bump version to 5.0.1-20260507 [version bump] * revert: keep dropy openUp:true for operator dropdowns Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Revert "chore: bump version to 5.0.1-20260507 [version bump]" This reverts commit d67c369ed437e47d658ae051348d377978dc48ed. * chore: bump version to 5.0.1-20260507 [version bump] * Revert "chore: bump version to 5.0.1-20260507 [version bump]" This reverts commit 29565ed829a55e9c241a789c9a3901d11cb5ca67. * chore: bump version to 5.0.1-20260507 [version bump] * Revert "chore: bump version to 5.0.1-20260507 [version bump]" This reverts commit 50e357604ebe9378564619b34c508b63cfb62c1d. * chore: bump version to 5.0.1-20260507 [version bump] * chore: bump version to 5.0.2-20260511 [version bump] * fix: render Globe panel immediately on first open without window resize Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * chore: bump version to 5.0.3-20260511 [version bump] * feat: add theme borders to panels and gradient backgrounds to splitters Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * chore: bump version to 5.0.4-20260511 [version bump] * style: bump split shadow gradient opacity to 0.4 Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * style: hotkeys modal 3-col grid + smaller leaflet zoom button gap Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * style: prevent hotkey label/value wrapping (ellipsis instead) Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * style: hotkeys modal single column, no wrap, no truncation Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * chore: bump version to 5.0.4-20260511 [version bump] * style: hotkeys modal dividers, invert title/subtitle colors, rename title, margin above subtitles Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * style: move splitter gradient to themed CSS class, restore hover feedback Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * chore: bump version to 5.0.5-20260511 [version bump] * style: hotkeys section titles use --color-h (matches rest of app) Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * chore: bump version to 5.0.5-20260511 [version bump] * fix: guard Globe_.init() inside rAF to prevent double instantiation Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * chore: bump version to 5.0.6-20260512 [version bump] * feat(plugins): per-plugin deps, lazy tool loading, validation, shared discovery Phase 3 — Plugin config validation + override warnings: - New API/pluginValidation.js with validatePluginConfig() for tool, component, and backend manifests. Validates required fields (name, paths), object/string shape of paths, dependencies block (npm/python.pip/python.conda), and warns on unknown top-level fields. - updateTools()/updateComponents() now skip invalid plugins and emit override warnings (matching what components already logged for tools). Phase 2 — Shared discoverPlugins() utility: - New API/pluginDiscovery.js consolidates the duplicated scanning logic from updateTools(), updateComponents(), and getBackendSetups(). Supports exact- name and substring container patterns, JSON/require/no-op loaders, and skips dot/underscore-prefixed dirs. - updateTools.js and setups.js refactored on top of the shared helper. Phase 1 — Per-plugin dependency declaration + build-time aggregation: - Plugin config.json may now declare a 'dependencies' block (npm + python.pip + python.conda). validatePluginConfig() also validates this shape. - New scripts/resolve-plugin-deps.js scans every tool/component/backend plugin and writes plugin-package.json, plugin-python-requirements.txt, and plugin-conda-deps.txt. Detects version conflicts and fails loudly. - scripts/build.js calls resolvePluginDeps() before updateTools(). - Dockerfile installs the aggregated plugin npm and pip deps after the root npm ci, using --no-save / --no-package-lock / --ignore-scripts so the root lockfile is untouched. - Animation tool migrated: ffmpeg/gifshot/html2canvas now declared in its config.json (kept in root package.json for transitional compat). - Generated artifacts gitignored. Phase 4 — Lazy loading of tool bundles: - updateTools() now emits dynamic-import arrow functions in the generated src/pre/tools.js with webpackChunkName hints so each tool is split into its own chunk (Kinds stays static because it's required synchronously). - ToolController_ gains ensureToolLoaded(name) and getLoadedTool(name) helpers and makeTool is async; init/finalizeTools and the separated-tool auto-open flow are updated to handle lazy modules. - Toolbar.jsx, SeparatedTools.jsx, SitesTool.js, and Layers_.js migrated to resolve LayersTool/etc. via the new helpers instead of poking toolModules directly. Tests & docs: - tests/fixtures/test-plugin-tools/{TestPlugin,InvalidPlugin,OverridePlugin} + tests/helpers/plugin-helpers.js with install/uninstall helpers. - New unit specs: pluginValidation, pluginDiscovery, updateTools, resolvePluginDeps, toolLazyLoading (57 tests, all passing). - CONTRIBUTING.md and docs/pages/Contributing/Contributing.md updated with schema, override behaviour, dependency declaration, build-time aggregation, conflict detection, and Docker integration. * chore: bump version to 5.0.7-20260512 [version bump] * fix: make Globe_.init() idempotent against multi-init Globe_.init() previously constructed a fresh GlobeRenderer on every call, which after #71 could happen multiple times for a single toggle (uiStore setTimeout + TopBar rAF). Each extra construction appends another .cesium-widget / _lithosphere_scene to #globe and leaves event handlers wired to dereferenced renderer state, which has been observed to break LithoSphere globe control buttons on configurations where the globe panel starts closed at boot. Add a top-of-init() guard that bails out and calls invalidateSize() when a renderer already exists. Single small, surgical change; no behavior change for the !L_.hasGlobe mock-swap path or for first-time construction. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * chore: bump version to 5.0.7-20260512 [version bump] * test(plugins): generate src/pre/tools.js on demand in toolLazyLoading spec The Playwright unit-tests CI step runs before `npm run build` so the gitignored `src/pre/tools.js` artifact does not yet exist on disk. Add a beforeAll hook that invokes `updateTools()` to regenerate it when missing, keeping the spec self-contained on both CI and dev machines that already built locally. * fix(tools): defensive getTool() + preload flag for cross-referenced tools Devin Review flagged a behavioural regression introduced by Phase 4: `ToolController_.getTool(name)` previously always returned a method- callable object (real module or `{ use(){} }` stub) because every tool was statically imported. After Phase 4, unresolved lazy loaders are `() => import(...)` functions, so callers like `Map_.getTool('InfoTool').use(...)`, `mmgisAPI.getTool('DrawTool').filesOn`, and `LegendTool` calling `LayersTool.populateCogScale` would crash with TypeError until the target tool was opened. Two fixes: 1. **Defensive getTool()**: Returns the legacy fallback stub when the tool module is still a lazy-loader function, and fires off `ensureToolLoaded(name)` in the background so subsequent calls see the resolved module. Prevents all crashes immediately. 2. **`preload: true` config flag**: Tools reached synchronously from other code paths (Info, Draw, Layers, Chemistry) now declare `"preload": true` in their `config.json`. `ToolController_.init()` calls `preloadEagerTools()` which fires `ensureToolLoaded` for every such tool right after toolbar setup — the chunks download in parallel with the rest of the page becoming interactive, so by the time a user clicks a feature the InfoTool module is already resolved. `validatePluginConfig` now accepts `preload` as a known tool field; CONTRIBUTING.md and docs/pages/Contributing/Contributing.md updated to document when to set it. Added a unit test covering the defensive getTool behaviour and the `preload` propagation through `toolConfigs`. * chore: bump version to 5.0.8-20260512 [version bump] * revert(plugins): remove Phase 4 lazy tool loading and preload mechanism Phase 4 lazy emission caused cross-tool consumers (Map_ feature-click, mmgisAPI, LegendTool) to receive raw '() => import(...)' arrows from ToolController_.getTool(), breaking InfoTool open. Reverting to the pre-Phase-4 behavior of static tool imports. - API/updateTools.js: generated src/pre/tools.js now emits 'import FooTool from ...' for every tool (Kinds stays static too). - ToolController_.js: getTool/makeTool back to sync; ensureToolLoaded, getLoadedTool, preloadEagerTools deleted; separated-tool auto-open flow simplified to direct sync calls. - Toolbar.jsx, SeparatedTools.jsx, Layers_.js: revert async/lazy patterns to sync ToolController_.toolModules[name] access. - API/pluginValidation.js: drop 'preload' from KNOWN_FIELDS. - src/essence/Tools/{Info,Draw,Layers,Chemistry}/config.json: drop 'preload: true'. - CONTRIBUTING.md + docs: remove preload documentation. - tests/unit/toolLazyLoading.spec.js: rewrite to verify static imports instead of lazy loaders. Also: log standard backends at startup (parity with plugin backends and with tools/components), so all backends now produce 'info Loaded backend: <name> from <container>' at boot. Phases 1-3 (per-plugin dependency aggregation, shared discoverPlugins, config validation + override warnings) are unaffected. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * feat(logger): new 'loaded' level (purple bg) for tool/component/backend startup Previously the 'Loaded tool/component/backend: X from Y' lines used the generic blue 'info' tag. They now use a dedicated 'loaded' level rendered with a purple (#a855f7) background, so plugin discovery output is visually distinct from other info messages. - API/logger.js: add 'loaded' case to the dev-mode switch (white text on purple bg) and suppress the redundant 'Caller:' echo for it (matches how 'info' and 'success' are handled). - API/updateTools.js: registerPlugin now logs at level 'loaded'. Drops the redundant 'Loaded ' prefix since the level tag now reads 'loaded'. - API/setups.js: standard and plugin backend startup logs use the new level, same drop of the 'Loaded ' prefix. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * feat(logs): 'Plugging in Tools/Components/Backends...' headings Rename the cyan banner messages in scripts/build.js and scripts/server.js from 'Updating Tools...' / 'Updating Components...' to 'Plugging in Tools...' / 'Plugging in Components...' so the headings match the plugin terminology used everywhere else (plugin-package.json, discoverPlugins, etc.). Also add a matching 'Plugging in Backends...' banner before setups.getBackendSetups() in scripts/server.js so backends get an equivalent title block to tools and components. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * style(logs): cyan banner lines lead with a blank line instead of trailing one Move the \n from the end to the beginning of every cyan banner in scripts/build.js and scripts/server.js (Resolving Plugin Dependencies, Plugging in Tools/Components/Backends, Validating Environment Variables, Starting websocket, Starting the development server) so that the blank line visually separates each section above its title rather than below it. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * feat(plugins): postinstall hook auto-installs plugin npm deps Plain `npm install` (or `npm ci`) on a fresh clone now resolves and installs every plugin's declared npm dependencies automatically, so new developers don't need to remember a second command. - scripts/install-plugin-deps.js (new): reads plugin-package.json, filters out deps already declared in root package.json with the same version specifier (no-op for the Animation transitional case), installs the remainder with `npm install --no-save --no-package-lock --ignore-scripts <pkg@ver> ...`. `--no-package-lock` keeps the root lockfile clean; `--ignore-scripts` prevents the inner install from re-entering postinstall and matches the Dockerfile. - package.json: postinstall guards against the Dockerfile's package.json-only layer (`scripts/` not copied yet) by checking for the two script files via `node -e` before invoking them. Adds a `plugins:install` npm script for on-demand runs. - CONTRIBUTING.md + docs/pages/Contributing/Contributing.md: replace the manual-install paragraph with a note about the postinstall hook and the filtering behavior. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * docs(plugins): manual Python install step for plugin pip/conda deps Document that the npm postinstall hook only handles plugin npm deps — plugin pip/conda deps must be installed manually after creating the Python environment, since there's no portable way to detect which interpreter or environment to target from a Node script. - CONTRIBUTING.md: added a 'For local development ... Python' block with the explicit `node scripts/resolve-plugin-deps.js` + `micromamba run -n mmgis pip install -r plugin-python-requirements.txt` + optional conda install commands. - docs/pages/Contributing/Contributing.md: matching short blurb in the user-facing docs. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * docs(install): plugin Python install step in Installation.md Add a numbered step in docs/pages/Setup/Installation/Installation.md's Setup sequence (right after `micromamba activate mmgis`) with the `pip install -r plugin-python-requirements.txt` and optional `micromamba install --file plugin-conda-deps.txt` commands, so non-Docker installs have the step in their main flow rather than buried in CONTRIBUTING.md. Also adds a pointer from the Python Environment section earlier in the same file (after the env-create + activate steps) back to the numbered Setup step. CONTRIBUTING.md and docs/pages/Contributing/Contributing.md are slimmed: instead of duplicating the install commands, both now link to the Installation page (this matches the user request — the install commands live in installation docs, not contribution docs). Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * fix: serialize concurrent layer reloads and stop mutating layer.url Concurrent mmgisAPI.reloadLayer() calls for the same layer were silently dropped by the _layersBeingMade guard, and reloadLayer mutated layer.url in-place during async work — causing a race where a second reload would capture the resolved URL as its 'original' and permanently corrupt the {starttime}/{endtime} template placeholders. Changes: - TimeControl.reloadLayer: compute resolvedUrl into a local variable instead of mutating layer.url. Pass resolvedUrl through to Map_.refreshLayer. - Map_.refreshLayer: accept resolvedUrl; temporarily swap layer.url inside a try/finally for the makeLayer call so the fetched URL is the resolved one and the placeholder template is always restored. - Map_.refreshLayer: when a reload is already in flight for the same layer, queue the request (coalesced by name) instead of dropping it with a 'Cannot make layer' warning. - Map_.makeLayer: after releasing the lock, drain any queued reload for this layer via setTimeout 0. - TimeControl.reloadTimeLayers: become async and await Promise.all of every per-layer reload; remove the setTimeout(500) workaround around active-feature restoration and follow-pan logic. - mmgisAPI.reloadLayers: new batch API that reloads multiple time-enabled layers concurrently and returns a Promise<boolean[]>. - Tests: new tests/e2e/map/concurrent-layer-reload.spec.js covers the seven scenarios from the plan (single reload, URL preservation, multi-layer concurrent reload, rapid same-layer reload, reloadLayers API surface). tests/e2e/api/mmgis-api.spec.js gains a surface check that reloadLayer/reloadLayers are exposed. - Docs: Main.md documents the new reloadLayers entry. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * chore: bump version to 5.0.8-20260513 [version bump] * fix(TimeControl): use Promise.allSettled in reloadTimeLayers Promise.all short-circuits on first rejection, which would skip the active-feature restoration and follow-pan logic if any single layer reload threw (network error, malformed config, etc.). The old setTimeout(500) approach ran those steps unconditionally; switching to Promise.allSettled preserves that robustness. Addresses Devin Review feedback on PR #78. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * fix(mmgisAPI): use Promise.allSettled in reloadLayers batch API Mirrors the reloadTimeLayers fix: a single failing TimeControl.reloadLayer call (e.g. unknown layer name throws inside asLayerUUID, network error, malformed config) no longer rejects the whole batch. The returned array preserves order and reports failed entries as false instead, matching the documented Promise<boolean[]> contract. Addresses second Devin Review finding on PR #78. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * docs: note that reloadTimeLayers is now async (Promise<string[]>) The implementation changed from sync to async in commit a096cfe9 (the function now uses await + Promise.allSettled internally to coordinate per-layer reloads), but the public API JSDoc and Main.md still documented the old synchronous return type. External consumers using the old synchronous return value would get a Promise instead of an array. Updates JSDoc on mmgisAPI.reloadTimeLayers to declare Promise<string[]>, and rewrites the Main.md example to use 'await'. Also fixes the previous example, which had a syntactically-malformed trailing tuple-style index. Addresses third Devin Review finding on PR #78. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * test: split time-related e2e specs into tests/e2e/time/ Moves the two time-feature specs out of tests/e2e/map/ so the map suite stays focused on map-UI behavior and the time/time-enabled-layer suite can be run (and reasoned about) on its own: - tests/e2e/map/time-control.spec.js -> tests/e2e/time/time-control.spec.js - tests/e2e/map/concurrent-layer-reload.spec.js -> tests/e2e/time/concurrent-layer-reload.spec.js Relative imports (../../helpers, ../../pages, ../../fixtures) are unchanged because the new directory is the same depth. Playwright picks the files up automatically via testDir './tests' + testMatch '**/*.spec.js'. Adds a matching 'npm run test:e2e:time' script and documents the new suite in tests/README.md alongside the existing per-suite scripts. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * test: cap local Playwright workers at 4 The previous `workers: process.env.CI ? 1 : undefined` resolved to Playwright's default (~half the CPU cores). On higher-core machines (e.g. 16 cores -> 8 workers) the dev server gets overloaded and the suite actually runs slower. CI behavior is unchanged (1 worker). Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * fix: thread resolvedUrl through makeLayer/captureVector instead of mutating layer.url The previous fix in Map_.refreshLayer temporarily swapped `layerObj.url = resolvedUrl` during the `await makeLayer` call and restored it in a finally block. Since `layerObj` is the shared `L_.layers.data[name]` object, any concurrent code reading `layer.url` during that async window could observe the resolved URL instead of the template. Most importantly, a second `TimeControl.reloadLayer()` call would then capture the resolved URL as its 'template' and corrupt the placeholders for every subsequent reload. Surfaced by tests/e2e/time/concurrent-layer-reload.spec.js Test 5, which after Promise.all of two reloads observed `layer.url ==='geodatasets:...?from=...&to=...'` instead of the expected `{starttime}/{endtime}` template. Fix: thread `resolvedUrl` as an explicit parameter through `Map_.refreshLayer` -> `makeLayer` -> `makeVectorLayer` -> `captureVector` (via options.resolvedUrl). `captureVector` uses `options.resolvedUrl` when provided and skips the `{starttime}`/`{endtime}`/`{customtime.*}` regex replacement (which TimeControl.reloadLayer already performed). `layer.url` is NEVER mutated for the duration of the async operation, so the template is preserved across overlapping reloads. This also fully resolves the Devin Review #4 finding which flagged the temporary URL swap as reintroducing the same race the PR was meant to fix. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * fix: also replace {time} placeholder when computing resolvedUrl In the previous fix, captureVector skips its time-replacement block when the caller supplies options.resolvedUrl (because TimeControl.reloadLayer already performed those replacements). However TimeControl.reloadLayer was only replacing {starttime}/{endtime}/{customtime.*} on resolvedUrl, not {time} — which captureVector previously handled at LayerCapturer.js:97 by mapping {time} -> endTime. This caused vector layers using the documented {time} placeholder (see docs/pages/Configure/Layers/Tile/Tile.md:90 and docs/pages/APIs/JavaScript/Main/Main.md:482) to fetch URLs containing the literal text '{time}' on time-triggered reloads. Mirror the existing captureVector behavior: replace {time} with the formatted end-time value alongside {starttime}/{endtime}, before the resolved URL is threaded through Map_.refreshLayer -> makeLayer -> captureVector. Addresses Devin Review finding on commit fcba8101. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * fix: always run time-placeholder replacement in captureVector (idempotent) The previous fix gated captureVector's time-placeholder replacement block on `!hasResolvedUrl`, on the assumption that any caller passing options.resolvedUrl had already done the replacement. That assumption only holds for time.type === 'global' / 'requery' / forceRequery. For other time types that still flow through Map_.refreshLayer into captureVector (most importantly time.type === 'local' with endProp == null per TimeControl.js:276-287), TimeControl.reloadLayer's resolved-URL replacement block at lines 249-273 is skipped, so the resolvedUrl arrives at captureVector still containing literal {starttime}/{endtime}/{time} placeholders. The fetch then goes out with unreplaced placeholders. Fix: drop the !hasResolvedUrl guard and always run the replacement, reading the source from `layerUrl` (which is already either options.resolvedUrl or layerObj.url per the choice above). The .replace(/{starttime}/g, ...) chain is idempotent on URLs that have already been resolved — the regexes simply don't match — so the correct path is restored without re-introducing the mutate-in-place bug fcba8101 fixed. Addresses Devin Review finding on commit ddb90dbb. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * test: remove GIBS MODIS time tile test (always skips on external dependency) The test at tests/e2e/time/time-control.spec.js depended on gibs.earthdata.nasa.gov being reachable from the test environment and served only as a placeholder — it skips every run since the test infrastructure does not have external network access by policy. It provides no signal in CI or locally, so removing it reduces noise in the suite output without losing coverage. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * test: add coverage for {time}, local-endProp-null, stress, and feature-presence cases Four new e2e tests in tests/e2e/time/concurrent-layer-reload.spec.js, each targeting a gap the original suite missed: Test 8 — {time} placeholder preservation. Mirrors Test 2/5 but uses `{time}` instead of `{starttime}/{endtime}`. Catches the Devin Review #5 regression where captureVector's gating on !hasResolvedUrl silently dropped the {time} -> endTime replacement (the literal '{time}' would have ended up in the fetch URL). Test 9 — local + endProp==null path. Sets layer.time.type='local' and layer.time.endProp=null to force the TimeControl.reloadLayer branch (TimeControl.js:276-287) that bypasses the resolved-URL placeholder block and falls through to the else clause. Inspects outgoing /geodatasets/* requests via page.on('request', ...) and asserts no literal {starttime}/{endtime}/{time} remain. Catches the Devin Review #6 regression: when this branch hit captureVector with hasResolvedUrl=true, the !hasResolvedUrl gate previously short- circuited the only remaining replacement site. Test 10 — 20-reload stress burst. Extends Test 4's two-reload check to 20 concurrent reloadLayer() calls, capturing 'Cannot make layer' warnings to verify the queue coalesces requests instead of silently dropping them. Also re-asserts layer.url template integrity post- burst. Test 11 — Feature-presence after concurrent reload. Captures L_.layers.layer[key].getLayers().length before and after a 5-reload burst. Asserts the count is still > 0 afterwards — the user-visible 'gaps where dynamically-appearing data doesn't show up' symptom from the original bug report. All four tests skip gracefully when their fixture layer is absent or the dataset returns no rows, to avoid spurious failures across mission configurations. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * test: add 5 edge-case tests probing the blast radius of the URL fix Five additional tests in tests/e2e/time/concurrent-layer-reload.spec.js covering paths adjacent to the URL-mutation fix that were not exercised by tests 1-11. Each was chosen because the production code on that path changed (or now has a different contract) and a regression would not be caught by the original race-condition tests. Test 12 — {customtime.N} placeholder preservation. TimeControl.reloadLayer's customtime replacement loop was migrated from `layer.url = ...` to `resolvedUrl = ...`. Seeds TimeControl.customTimes.times so the loop actually runs, then asserts the {customtime.0} placeholder remains literally on layer.url after reload. Test 13 — mmgisAPI.reloadTimeLayers() returns a Promise. This is the backward-incompatible behavior change documented in docs/pages/APIs/JavaScript/Main/Main.md (previously synchronous). Asserts the returned value is a thenable that resolves to an array, pinning the new contract so it does not silently regress. Test 14 — mmgisAPI.reloadLayers handles unknown layer names. The Promise.allSettled change requires that a failing per-layer reload surfaces as `false` at the same array position as its input name, without throwing. Mixes a valid name + an unknown name + another valid name to verify the order and the boolean mapping. Test 15 — Reloading a time-DISABLED vector layer leaves layer.url unchanged. Discovers a candidate layer from L_.layers.data at runtime (skips if none exist), reloads it, and asserts the URL is byte-equal afterwards. Catches any accidental URL mutation introduced for the non-time code path. Test 16 — mmgisAPI.reloadLayers handles empty array, null, undefined, and string inputs without throwing — verifying the Array.isArray guard at mmgisAPI.js:618. Returns `[]` in all cases. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * fix(makeLayer): wrap dispatch in try/finally so lock + queue drain always run Address Devin Review finding: a thrown exception from any layer-type builder (makeVectorLayer, makeVelocityLayer, etc.) inside makeLayer's switch statement previously left lockRegistry[layerName] set to true and skipped the queue drain entirely, since the release statement and queue-drain block both lived AFTER the awaited dispatch. Effect of the bug: any subsequent refreshLayer call for that layer would queue against a permanently-locked entry that never drains. The new queue mechanism inherits this pre-existing issue and makes the failure mode worse — silent accumulation in _layerReloadQueue instead of a visible 'Cannot make layer' warning. Additional concern: the outer 'new Promise(async (resolve, reject) => {...})' is the async-executor anti-pattern. A throw inside the async executor escapes to the unhandled-rejection handler instead of rejecting the outer Promise — so the caller's 'awa…
* fix: update MobileCoordButton topBar paddingLeft from 80px to 34px
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix: MobileTimeUIToggle — inline toggle logic, float right, hide from settings on mobile
- Replace broken Coordinates.toggleTimeUI() call with direct jQuery/store toggle
- Float time button right in toolbar
- Hide Time UI toggle from settings modal on mobile (toolbar has it)
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix: push scalebar/compass/scale up 40px on mobile, keep #timeUI in DOM
- BottomElementPositioner: position mapToolBar, leaflet-bottom-left/right
40px above bottom on mobile (above toolbar)
- Stop removing #timeUI from DOM on mobile so MobileTimeUIToggle works
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix: mobile TimeUI — only show endtime, always expanded
- Hide #mmgisTimeUIStartWrapper and StartWrapperFake on mobile via CSS
- Force expanded state (addClass expanded + show) when toggling TimeUI on
- CSS ensures #timeUI.active always shows expanded content on mobile
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix: mobile TimeUI opens in tool panel with header, end time, expanded rows
- MobileTimeUIToggle now opens/closes the tool panel via ToolController_
- Closes any active tool before showing TimeUI
- Forces expanded state when opening
- CSS hides start time inputs, positions expanded content properly
- Overrides absolute positioning of expanded content for tool panel flow
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* feat: rewrite separated tools system from jQuery to React components
- Add separatedToolsList/activeSeparatedTools state to Zustand uiStore
- Rewrite SeparatedTools.jsx with glassmorphism panels, CSS Module styling
- Replace SepToolsContainer (setInterval hack) with SepToolButton/SepToolsSection
- Remove ~170 lines of jQuery DOM construction from ToolController_.js
- Fix hardcoded rgba(26,26,27,0.88) to theme-aware var(--color-a-rgb)
- Remove separated tool entries from themeApplier.js
- Remove separated tool overrides from FloatingElements.css
- Move Legend CSS overrides from Toolbar.module.css to SeparatedTools.module.css
- Remove jQuery active-state manipulation from IdentifierTool.js
- Add store sync in Map_.js displayOnStart logic
- Preserve all DOM IDs for backward compatibility (mmgisAPI, tool make())
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* chore: bump version to 4.3.28-20260501 [version bump]
* fix: TimeUI mobile checks — use Zustand store instead of L_.UserInterface_
L_.UserInterface_ is null when TimeUI.init() runs (TimeControl.init is called
before L_.link sets UserInterface_). All 16 isMobile checks now read from
useUIStore.getState().isMobile which is set at startup.
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* chore: bump version to 4.3.29-20260501 [version bump]
* fix: move displayOnStart logic from Map_.js to ToolController_.finalizeTools()
- Map_ no longer references specific tools (LegendTool)
- displayOnStart is now handled generically for all separated tools
- Added DOM element polling (tryMake) to handle React render timing
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* revert: remove all TimeUI-related mobile changes
Reverts TimeUI.js and BottomBar.js to development base.
Restores #timeUI DOM removal in UserInterfaceBridge.fina().
Removes MobileTimeUIToggle component from Toolbar.jsx.
Removes TimeUI mobile CSS overrides from UserInterfaceMobile_.css.
Non-TimeUI refinements (toolbar height, scalebar positioning, etc.) preserved.
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* simplify: remove DOM polling, use simple setTimeout(0) for auto-open
LegendTool handles its own content lifecycle via subscribeOnLayerToggle.
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* feat: mobile TimeUI — fix isMobile detection, staging container, toolbar toggle
- TimeUI.js: import useUIStore and replace all 16 L_.UserInterface_?.isMobile
checks with useUIStore.getState().isMobile (L_.UserInterface_ is null when
TimeUI.init() runs, so mobile conditionals were dead code)
- TimeUI.js: stage mobile #timeUI in hidden #timeUIMobileStaging instead of
placing directly in #tools (which gets cleared by other tools)
- UserInterfaceBridge.js: stop removing #timeUI from DOM on mobile
- Toolbar.jsx: add MobileTimeUIToggle that moves #timeUI between staging and
#tools, opens/closes tool panel via ToolController_
- BottomBar.js: hide TimeUI toggle from settings modal on mobile
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix: rescue #timeUI back to staging when another tool opens
Subscribe to activeToolName changes — when a tool becomes active while
TimeUI is showing, move #timeUI back to #timeUIMobileStaging before
the new tool's make() clears #tools.
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* chore: remove separatedTool/justification config toggles, fix review issues
- Remove separatedTool checkbox and justification dropdown from Legend
and Identifier config.json (these are always separated, not configurable)
- Remove justification property/code from LegendTool.js, IdentifierTool.js
- Simplify Globe_.js separated tool count (no justification filter)
- Remove justification from Reference-Mission config blueprint
- Update LegendTool help docs and Legend.md documentation
- Add --color-a-rgb fallback (29,31,32) in SeparatedTools.module.css
- Add display:none !important to .panelIdentifier to prevent 12px gap
- Update e2e test comment
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix: circular import in TimeUI.js, toolbar/bottomFloatingBar position sync
- TimeUI.js: replace top-level useUIStore import with lazy _getUIStore()
accessor to avoid 'Cannot access useUIStore before initialization'
circular import error at _remakeTimeSlider
- SplitScreens.jsx: skip #timeUI reparenting observer on mobile (mobile
uses MobileTimeUIToggle to manage #timeUI placement in #tools)
- BottomElementPositioner.jsx: unify mobile transition to 0.3s (matches
toolsWrapper and toolbar), guard pxIsTools against undefined
- Toolbar.jsx: align toolbar transition to 0.3s ease-out
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* LegendTool fix empty message
* chore: remove separated tools offset logic from Globe_.js
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix: skip _makeHistogram on mobile (no timeline slider, timestamps unset)
_makeHistogram renders inside the timeline slider which doesn't exist
on mobile. Without it, _timelineStartTimestamp is NaN, causing
'Invalid time value' RangeError at toISOString().
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix: mobile TimeUI — populate expanded rows, fix Invalid date, fix panel height
- TimeUI.js attachEvents: use _initialStart/_initialEnd on mobile (same
as desktop) instead of L_.TimeControl_ which isn't set yet at init time.
Fixes 'Invalid date' in start/end time inputs.
- TimeUI.js fina: set expanded=true on mobile and call _populateExpandedRows()
so year/month/day/hour rows actually render. Removed position:absolute and
pointer-events:none overrides.
- Toolbar.jsx: set tool panel height to 217px (TimeUI.height) instead of
45% viewport — matches actual TimeUI content height.
- UserInterfaceMobile_.css: expanded content flows naturally (position:relative),
hide start time inputs, allow overflow scroll, flex-wrap topbar.
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix: mobile TimeUI justify-content center, restore toolbar border-bottom
- Add justify-content: center to #mmgisTimeUIMain on mobile
- Remove border-bottom: none override so toolbar keeps its default border
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix: mobile TimeUI overflow hidden, scalebar/compass fixed at 40px offset
- #timeUI overflow-y: hidden (was auto, causing 2px scroll)
- Scalebar/compass/map controls stay at fixed 40px offset (above toolbar)
regardless of tool panel state — no longer shift up by pxIsTools
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* Implement multi-tier knowledge architecture
- Restructure AGENTS.md from 745 lines to 106 lines (Tier 1: essential context)
- Create knowledge/ directory with 30+ wiki-style documentation files (Tier 2: deep knowledge)
- Create knowledge/reference/ with 8 detailed reference files (Tier 3: lookup material)
- Move AI-GETTING-STARTED.md and AI-DEVELOPMENT.md to knowledge/
- Update all file references in .specify/templates and blueprints
- Create knowledge/README.md as the full knowledge base index
- Create knowledge/reference/README.md as reference material index
Three-tier knowledge discovery system:
Tier 1: AGENTS.md (~106 lines) - scannable in <2 minutes
Tier 2: knowledge/*.md - deep knowledge on architecture, tools, APIs, DB, infra
Tier 3: knowledge/reference/*.md - coding conventions, API reference, troubleshooting
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* chore: bump version to 4.3.29-20260501 [version bump]
* fix: mobile toolbar active button style matches desktop, fix icon alignment
- All mobile toolbar buttons (ToolButton, MobileCoordButton, MobileTimeUIToggle)
now use display:flex with align-items/justify-content center for proper
vertical icon centering
- MobileCoordButton: changed 'active' class to 'toolButtonActive' to match
the global CSS active style (color-mmgis + color-i background)
- Removed inline color overrides so CSS .toolButtonActive takes effect
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* Add Devin knowledge notes from past MMGIS sessions
Include curated lessons learned from past Devin sessions:
- CI/CD: ignore build-arm64/amd64 failures, focus on required checks
- Child sessions: no separate PRs when consolidating
- ENV triple-update rule (.env, sample.env, ENVs.md)
- Error handling: use logger with infrastructure_error for fatal startup errors
- Path traversal security: stay within /Missions, handle subpath serving
- Database initialization architecture and migration patterns
- API authentication behavior across AUTH modes
- Auto-generated MMGIS concept index
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix: mobile toolbar active button style, icon alignment, tool deactivation
- Active toolbar buttons get desktop-matching margin (1px 0) and
border-radius (8px) via .toolButton.toolButtonActive CSS rule
- Removed line-height: 40px from .toolButton (flex centering handles
vertical alignment, line-height was pushing icons up)
- MobileCoordButton now watches activeToolName store and deactivates
when another tool opens (fixes coords staying active)
- MobileTimeUIToggle sets activeToolName='MobileTimeUI' when opening
so coords/other buttons can detect it and deactivate
- MobileTimeUIToggle clears activeToolName when closing
- Both custom buttons skip self-deactivation via name check
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* Fix cross-references: convert backtick refs to markdown links, add Devin knowledge notes
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix: mobile toolbar icon height 40px, button margins for active padding
- #toolbar .toolButton i: height 40px fixes icon vertical alignment
- #toolbar .toolButton: margin 0 2px gives spacing between buttons
- #toolbar .toolButton.toolButtonActive: margin 1px 2px so active
background has visual padding around the icon
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* Rename knowledge/ to .knowledge/ for consistency with .specify/ convention
Dot-prefix signals agent infrastructure (not source code), consistent with
.specify/, .github/, .vscode/ conventions. All cross-references updated.
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix: mobile toolbar icon line-height 40px, active button padding via height
- Coord and TimeUI button <i> icons get line-height: 40px
- Active buttons: height 34px (vs 40px toolbar) creates visual padding
around the active background, centered by flex align-items
- Buttons get margin: 0 1px for horizontal spacing
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* Fix broken cross-reference: 06.2 -> 06.1-configure-rest-api.md
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix: close active tool + cancel deferred cleanup in MobileCoordButton/TimeUI
- MobileCoordButton: call closeActiveTool() before opening, destroy
_pendingCloseTool if set, increment _closeSeq to cancel deferred
tools.innerHTML clear
- MobileTimeUIToggle: same _pendingCloseTool + _closeSeq fix after
closeActiveTool() to prevent 420ms deferred cleanup from wiping
#timeUI after it's placed in #tools
- Removed redundant closeActiveTool() from MobileCoordButton close path
(was being called after destroy, not needed)
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix: active mobile toolbar buttons 34x34px (square)
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* Drastically compress .knowledge/ — keep only unique agent content
Remove 33 wiki files that duplicate docs/pages/ content.
Remove 9 reference/ files derivable from source code.
Keep only 5 files (down from 46):
- AI-GETTING-STARTED.md (agent setup walkthrough)
- AI-DEVELOPMENT.md (spec-kit workflow)
- conventions-and-gotchas.md (naming, code style, common issues)
- 12-devin-knowledge-notes.md (CI, auth, DB init, security gotchas)
- README.md (index pointing to docs/pages/ for everything else)
Principle: don't duplicate docs/ — only keep what's uniquely agent-optimized.
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* Rename to knowledge-notes.md, remove Devin branding and fork-specific CI section
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix: hide mmgis-map-logo on mobile
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* Restore Database Safety Rules for AI Agents section in AGENTS.md
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix: shift compass and map scale 6px to the right (both mobile and desktop)
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* Add back Important Instructions, code pattern templates, and detailed project structure
- Important Instructions in AGENTS.md: MCP tools, hot-reload, Reference Mission
- .knowledge/code-patterns.md: full directory tree with key directory annotations,
plus copy-paste templates for Express routes, Sequelize models, Tool plugins,
and WebSocket handlers
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* Update project structure trees to reflect current filesystem
Add missing directories: tests/, .knowledge/, .specify/, .github/, views/,
private/, spice/, build/, examples/, scripts/middleware.js.
Both abbreviated (AGENTS.md) and detailed (.knowledge/code-patterns.md) trees
now match the actual repo layout.
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* chore: bump version to 4.3.30-20260501 [version bump]
* Add Layers_.js to project structure (key singleton L_)
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* Fix project structure: correct API layout, frontend modules, code templates
API/Backend/ uses feature-domain modules (Draw/, Users/, Config/, etc.)
with setup.js + routes/ + models/ per feature — not APIs/ or Databases/.
Frontend essence/ has Components/, Helpers/, LandingPage/, mmgisAPI/,
services/ — not Ancillary/. Basics/ includes all singletons (Globe_,
Formulae_, ToolController_, Viewer_, ComponentController_, Test_).
Code templates updated to match actual patterns (setup.js, module.exports).
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* refactor: remove test infrastructure (Test_ module, testModules, DrawTool.test)
- Delete src/essence/Basics/Test_/ directory
- Delete src/essence/Tools/Draw/DrawTool.test.js
- Remove Test_ import and Shift+T keydown handler from essence.js
- Remove tests key from Draw tool config.json
- Remove testModules generation logic from API/updateTools.js
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* chore: bump version to 4.3.31-20260501 [version bump]
* style: move Cesium link button to top-right and match Leaflet zoom button styling
- Change control container from top-left to top-right positioning
- Update button size from 26px to 30px to match Leaflet zoom controls
- Use CSS variables (--color-a, --color-f, --color-mmgis) instead of hardcoded colors
- Add border-radius and box-shadow matching Leaflet control appearance
- Update hover/inactive states to use themed colors
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix: anchor map logo to viewport instead of Leaflet map panel
- Change MapLogo parent from .leaflet-bottom.leaflet-right to #main-container
- Switch CSS position from absolute to fixed for viewport anchoring
- Add explicit bottom-offset positioning in BottomElementPositioner (desktop)
- Add explicit bottom-offset positioning in BottomElementPositioner (mobile)
- Logo stays at viewport right edge regardless of open side panels
- Retains smooth bottom offset transitions when bottom bar appears
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* docs: remove references to deleted test infrastructure (Test_, DrawTool.test)
- Remove Test_/ from project structure in .knowledge/code-patterns.md
- Remove DrawTool.test.js references from specs/006 spec, plan, and tasks
- Remove Draw Tool Testing section from tasks.md
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* chore: bump version to 4.3.32-20260501 [version bump]
* fix: append logo to document.body to avoid filter containing block
#main-container has a CSS filter property which creates a new containing
block per the CSS spec, causing position:fixed to behave like absolute.
Appending to document.body ensures true viewport-fixed positioning.
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix: prevent mobile topBarTitleName text wrapping by replacing max-width with white-space: nowrap
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* chore: bump version to 4.3.33-20260501 [version bump]
* chore: bump version to 5.0.0 and update changelog
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* refactor(ui): move Screenshot/Fullscreen to BottomBar, About to TopBar kebab
TopBar kebab menu now contains only Keyboard Shortcuts, Settings, and About
(About now shows on both desktop and mobile).
BottomBarReact now renders Screenshot, Fullscreen, and Copy Link buttons
(top to bottom) following the same IconButton + Tooltip pattern. The
About button has been removed from BottomBarReact.
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* chore: bump version to 5.0.1-20260505 [version bump]
* feat(mobile): enforce exclusive panel toggling on mobile in TopBar
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* chore: bump version to 5.0.1-20260505 [version bump]
* style: reposition LithoSphere globe controls to match Leaflet/Cesium theme
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* chore: bump version to 5.0.1-20260505 [version bump]
* feat(topbar): hide Viewer/Globe toggles based on configured panels
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* style(bottombar): reorder buttons (Copy Link, Screenshot, Fullscreen) and unify size
Reorder the BottomBarReact buttons top-to-bottom to: Copy Link, Screenshot,
Fullscreen.
Move the 24x24 button sizing from the #topBarLink id selector in mmgis.css
into the .barButton class in BottomBarReact.module.css so all three buttons
share the same compact size as the original Copy Link button. Drop the now
redundant #topBarLink rule.
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* style(bottombar): increase padding-bottom to 12px and button margin to 3px 0
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* style: rearrange globe controls — compass top-right circular, nav row, vertical column, panels open left
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* chore: bump version to 5.0.2-20260505 [version bump]
* chore: bump version to 5.0.2-20260505 [version bump]
* style: anchor observe settings panel right:34px and float nav hover panels
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* feat(theming): add 5 new themes, --color-shadow variable, and configure ThemePreview
- Add Dark Terra, Dark Nebula, Dark Lunar, Dark Supernova, Light Botanical themes
- Add --color-shadow CSS variable to every theme + :root fallback
- Replace hardcoded rgba shadow colors with var(--color-shadow) in TopBar,
Toolbar, SeparatedTools, ToolPanel, FloatingElements, Dropdown, Modal,
and SplitScreens
- Add Custom shadowcolor color picker in tab-ui-config and apply it via Stylize
- Add ThemePreview component (configure/src) wired through Maker.js as
a new 'themepreview' row type so the configure UI shows a live mini
mockup of the selected theme
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* chore: bump version to 5.0.2-20260505 [version bump]
* fix(configure/ThemePreview): tighten top spacing and live-preview Custom theme
- Pull the preview up by 12px so the gap below the theme dropdown is tighter.
- Read the Custom color pickers (look.primarycolor / secondarycolor /
tertiarycolor / accentcolor / shadowcolor / topbarcolor / toolbarcolor /
mapcolor) from the configuration and overlay them on Dark Default so
the preview reflects Custom theme edits live, matching Stylize.js's
runtime behavior.
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* chore: bump version to 5.0.3-20260505 [version bump]
* feat(themes): add Dark Heliosphere, Dark Monokai, and Light Solarized
- Dark Heliosphere: deep night purple surface with corona-orange accent.
- Dark Monokai: warm graphite surface with lime accent (Monokai-inspired).
- Light Solarized: classic solarized base3/base02 with blue accent.
Mirror added to configure/src/themes/themes.js for the ThemePreview, and
the three names appended to the Color Theme dropdown options.
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix(coordinates): respect time.initiallyOpen when live deep-link is set
* chore: bump version to 5.0.3-20260505 [version bump]
* refactor(theming): remove Custom theme + per-field color overrides
- Drop the 'Custom' option from the Color Theme dropdown.
- Remove all Custom Color Options (look.primarycolor, .secondarycolor,
.tertiarycolor, .accentcolor, .bodycolor, .topbarcolor, .toolbarcolor,
.mapcolor, .hightlightcolor, .shadowcolor) from tab-ui-config.json.
- Strip the matching DOM/CSS-variable override block from Stylize.js;
Stylize now just applies the selected preset theme (and the page logo).
- Drop the empty bodycolor/topbarcolor/toolbarcolor/mapcolor/shadowcolor
defaults from API/templates/config_template.js.
- Simplify ThemePreview to render the selected preset directly — no
Custom branch, no overlay logic.
Preset themes cover all the looks we want and keep the configure surface
much smaller.
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* style(time-ui): round corners on TimeUI shell, action wrappers, mode dropdown
- #timeUI: 10px border-radius on the outer time control bar.
- #mmgisTimeUIActionsLeft / #mmgisTimeUIActionsRight: 10px border-radius
so the action clusters sit as rounded chips.
- #mmgisTimeUIActionsRight > div (excluding #mmgisTimeUIPresent): 10px
border-radius on each action button so they match the wrapper.
- #mmgisTimeUIModeDropdown: 40px height + 10px border-radius to align
with the rest of the bar; clear the dropy default border-color so the
rounded edge isn't outlined.
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* chore: bump version to 5.0.4-20260505 [version bump]
* feat(configure): mark light themes as (experimental) in dropdown label
Light themes still have outstanding contrast issues, so flag them in the
Color Theme dropdown without changing the saved value.
- Maker dropdown now accepts options as either a plain string (current
behavior) or { value, label } so the rendered label can differ from
the persisted value.
- tab-ui-config switches the six light themes to { value, label } form
with '(experimental)' appended to the label only. Existing mission
configs that already saved 'Light Default' etc. continue to match.
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* Fix timeUI border radius
* fix(mobile): rescue #timeUI before tool make() destroys it
Clicking Layers -> Time -> Layers -> Time on mobile caused the bottom
panel to render LayersTool content with TimeUI height. The #timeUI DOM
element was destroyed when LayersTool.make() called $('#tools').empty(),
before the async React useEffect in MobileTimeUIToggle could rescue it
to its staging container.
- ToolController_.makeTool: synchronously move #timeUI from #tools back
to #timeUIMobileStaging (and reset TimeUI store flags) on mobile,
before invoking the new tool's make().
- MobileTimeUIToggle.handleClick: defensive fallback that re-initializes
TimeUI if #timeUI no longer exists when the toggle is activated.
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix(mobile): move re-initialized #timeUI from staging into #tools
TimeUI.init() on mobile appends the new #timeUI to the hidden
#timeUIMobileStaging container, so the fallback branch must also move
it into #tools — otherwise the user sees an empty tool panel after
the destroyed-element recovery path.
Caught by Devin Review.
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix(mobile): preserve #timeUI when Coordinates tool empties #tools
On mobile, opening or closing the Coordinates tool runs
$('#tools').empty() inside interfaceWithMMWebGIS / separateFromMMWebGIS.
After the previous PR commits, clicking Coordinates -> Time still left
the bottom panel empty because:
- Coordinates.make() empties #tools while #timeUI is in staging (fine
on its own), but the Coordinates teardown that fires after the user
switches to the Time toggle (via MobileCoordButton's useEffect on
activeToolName change) calls Coordinates.destroy() ->
separateFromMMWebGIS(), which empties #tools wholesale and destroys
the freshly-placed #timeUI.
Add a rescueMobileTimeUI() helper that moves #timeUI from #tools back
to #timeUIMobileStaging before each tools.empty() call in Coordinates,
mirroring the rescue already done in ToolController_.makeTool().
Coordinates -> Time now correctly shows the TimeUI.
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix(mobile): harden TimeUI fallback recovery (call fina(), de-dupe popovers)
Devin Review correctly flagged that the safety-net path in
MobileTimeUIToggle.handleClick was producing a partially-broken TimeUI
when it fired:
- TimeUI.init() unconditionally appends a new #timeUIPlayPopover_global
to <body>, so a second init() left two elements with the same id.
- TimeUI.init() alone does not wire up date pickers or per-button click
handlers — that's TimeUI.fina()'s job. Without fina(), the recovered
TimeUI rendered visually but Play / Previous / Next / Fit / Follow /
Present / Expand were all dead.
Before re-initializing, remove the stale #timeUIPlayPopover_global and
#timeUIQuickSelectPopover_global divs to avoid duplicate ids. After the
new #timeUI is moved into #tools, call TimeUI.fina() to populate the
date pickers, attach the button click handlers, build the histogram,
and populate the expanded mobile rows.
Some delegated body/document handlers in attachEvents() will still be
duplicated on this path; that is acceptable for a degraded recovery
that should never run in practice now that the primary rescues in
ToolController_.makeTool() and Coordinates.js cover all known paths.
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* chore: bump version to 5.0.5-20260505 [version bump]
* fix(mobile): Coordinates teardown only removes its own DOM
The previous Coordinates fix was racing with itself: after the Time
toggle synchronously moved #timeUI into #tools, MobileCoordButton's
useEffect (triggered by the activeToolName change) ran on the next
React tick and called L_.Coordinates.destroy(). That called
separateFromMMWebGIS(), whose rescue moved #timeUI right back into the
hidden staging div before tools.empty() — so the bottom panel ended up
empty even though the time toggle was 'active'.
Make separateFromMMWebGIS selective: only remove the
Coordinates-specific DOM (#coordUIHeader and #CoordinatesDiv) instead
of wiping all of #tools. Any other content already in #tools (e.g.
#timeUI placed there by the Time toggle) is left alone.
interfaceWithMMWebGIS still keeps the rescue + tools.empty() pattern
on the open path so Coordinates always starts from a clean panel.
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* Bump DrawTool Temporal Drawings upward
* chore: bump version to 5.0.6-20260505 [version bump]
* chore: reset version to 5.0.0
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* test(e2e): fix 9 pre-existing failures (test-only changes)
- mmgis-api.spec.js: add form-fill login under AUTH=local; serialize
describe to avoid concurrent-login race in the session store
- coordinates.spec.js: TimeUI toggle was moved from the coordinates bar
to the Settings modal; navigate via topbar kebab menu and assert the
checkbox is rendered
- widgets.spec.js: target .leaflet-control-zoom-in/-out specifically;
the bare .leaflet-control-zoom class is also used by the home/reset
control, so the original assertion was always false
- sites.spec.js: scope panel selector to #toolPanel; both the toolbar
icon and the panel container share id="SitesTool"
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* chore: bump version to 5.0.1-20260505 [version bump]
* Revert "chore: bump version to 5.0.1-20260505 [version bump]"
This reverts commit 4880204c1163be5d1d7fa96d14a0ed018c6f586c.
* fix: prevent filter operator dropdown clipping in Layers panel
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* chore: bump version to 5.0.1-20260507 [version bump]
* revert: keep dropy openUp:true for operator dropdowns
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* Revert "chore: bump version to 5.0.1-20260507 [version bump]"
This reverts commit d67c369ed437e47d658ae051348d377978dc48ed.
* chore: bump version to 5.0.1-20260507 [version bump]
* Revert "chore: bump version to 5.0.1-20260507 [version bump]"
This reverts commit 29565ed829a55e9c241a789c9a3901d11cb5ca67.
* chore: bump version to 5.0.1-20260507 [version bump]
* Revert "chore: bump version to 5.0.1-20260507 [version bump]"
This reverts commit 50e357604ebe9378564619b34c508b63cfb62c1d.
* chore: bump version to 5.0.1-20260507 [version bump]
* chore: bump version to 5.0.2-20260511 [version bump]
* fix: render Globe panel immediately on first open without window resize
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* chore: bump version to 5.0.3-20260511 [version bump]
* feat: add theme borders to panels and gradient backgrounds to splitters
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* chore: bump version to 5.0.4-20260511 [version bump]
* style: bump split shadow gradient opacity to 0.4
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* style: hotkeys modal 3-col grid + smaller leaflet zoom button gap
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* style: prevent hotkey label/value wrapping (ellipsis instead)
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* style: hotkeys modal single column, no wrap, no truncation
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* chore: bump version to 5.0.4-20260511 [version bump]
* style: hotkeys modal dividers, invert title/subtitle colors, rename title, margin above subtitles
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* style: move splitter gradient to themed CSS class, restore hover feedback
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* chore: bump version to 5.0.5-20260511 [version bump]
* style: hotkeys section titles use --color-h (matches rest of app)
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* chore: bump version to 5.0.5-20260511 [version bump]
* fix: guard Globe_.init() inside rAF to prevent double instantiation
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* chore: bump version to 5.0.6-20260512 [version bump]
* feat(plugins): per-plugin deps, lazy tool loading, validation, shared discovery
Phase 3 — Plugin config validation + override warnings:
- New API/pluginValidation.js with validatePluginConfig() for tool, component,
and backend manifests. Validates required fields (name, paths), object/string
shape of paths, dependencies block (npm/python.pip/python.conda), and warns
on unknown top-level fields.
- updateTools()/updateComponents() now skip invalid plugins and emit override
warnings (matching what components already logged for tools).
Phase 2 — Shared discoverPlugins() utility:
- New API/pluginDiscovery.js consolidates the duplicated scanning logic from
updateTools(), updateComponents(), and getBackendSetups(). Supports exact-
name and substring container patterns, JSON/require/no-op loaders, and skips
dot/underscore-prefixed dirs.
- updateTools.js and setups.js refactored on top of the shared helper.
Phase 1 — Per-plugin dependency declaration + build-time aggregation:
- Plugin config.json may now declare a 'dependencies' block (npm + python.pip +
python.conda). validatePluginConfig() also validates this shape.
- New scripts/resolve-plugin-deps.js scans every tool/component/backend plugin
and writes plugin-package.json, plugin-python-requirements.txt, and
plugin-conda-deps.txt. Detects version conflicts and fails loudly.
- scripts/build.js calls resolvePluginDeps() before updateTools().
- Dockerfile installs the aggregated plugin npm and pip deps after the root
npm ci, using --no-save / --no-package-lock / --ignore-scripts so the root
lockfile is untouched.
- Animation tool migrated: ffmpeg/gifshot/html2canvas now declared in its
config.json (kept in root package.json for transitional compat).
- Generated artifacts gitignored.
Phase 4 — Lazy loading of tool bundles:
- updateTools() now emits dynamic-import arrow functions in the generated
src/pre/tools.js with webpackChunkName hints so each tool is split into
its own chunk (Kinds stays static because it's required synchronously).
- ToolController_ gains ensureToolLoaded(name) and getLoadedTool(name) helpers
and makeTool is async; init/finalizeTools and the separated-tool auto-open
flow are updated to handle lazy modules.
- Toolbar.jsx, SeparatedTools.jsx, SitesTool.js, and Layers_.js migrated to
resolve LayersTool/etc. via the new helpers instead of poking toolModules
directly.
Tests & docs:
- tests/fixtures/test-plugin-tools/{TestPlugin,InvalidPlugin,OverridePlugin}
+ tests/helpers/plugin-helpers.js with install/uninstall helpers.
- New unit specs: pluginValidation, pluginDiscovery, updateTools,
resolvePluginDeps, toolLazyLoading (57 tests, all passing).
- CONTRIBUTING.md and docs/pages/Contributing/Contributing.md updated with
schema, override behaviour, dependency declaration, build-time aggregation,
conflict detection, and Docker integration.
* chore: bump version to 5.0.7-20260512 [version bump]
* fix: make Globe_.init() idempotent against multi-init
Globe_.init() previously constructed a fresh GlobeRenderer on every call,
which after #71 could happen multiple times for a single toggle (uiStore
setTimeout + TopBar rAF). Each extra construction appends another
.cesium-widget / _lithosphere_scene to #globe and leaves event handlers
wired to dereferenced renderer state, which has been observed to break
LithoSphere globe control buttons on configurations where the globe panel
starts closed at boot.
Add a top-of-init() guard that bails out and calls invalidateSize() when
a renderer already exists. Single small, surgical change; no behavior
change for the !L_.hasGlobe mock-swap path or for first-time construction.
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* chore: bump version to 5.0.7-20260512 [version bump]
* test(plugins): generate src/pre/tools.js on demand in toolLazyLoading spec
The Playwright unit-tests CI step runs before `npm run build` so the
gitignored `src/pre/tools.js` artifact does not yet exist on disk.
Add a beforeAll hook that invokes `updateTools()` to regenerate it
when missing, keeping the spec self-contained on both CI and dev
machines that already built locally.
* fix(tools): defensive getTool() + preload flag for cross-referenced tools
Devin Review flagged a behavioural regression introduced by Phase 4:
`ToolController_.getTool(name)` previously always returned a method-
callable object (real module or `{ use(){} }` stub) because every tool
was statically imported. After Phase 4, unresolved lazy loaders are
`() => import(...)` functions, so callers like `Map_.getTool('InfoTool').use(...)`,
`mmgisAPI.getTool('DrawTool').filesOn`, and `LegendTool` calling
`LayersTool.populateCogScale` would crash with TypeError until the
target tool was opened.
Two fixes:
1. **Defensive getTool()**: Returns the legacy fallback stub when the
tool module is still a lazy-loader function, and fires off
`ensureToolLoaded(name)` in the background so subsequent calls see
the resolved module. Prevents all crashes immediately.
2. **`preload: true` config flag**: Tools reached synchronously from
other code paths (Info, Draw, Layers, Chemistry) now declare
`"preload": true` in their `config.json`. `ToolController_.init()`
calls `preloadEagerTools()` which fires `ensureToolLoaded` for
every such tool right after toolbar setup — the chunks download
in parallel with the rest of the page becoming interactive, so by
the time a user clicks a feature the InfoTool module is already
resolved.
`validatePluginConfig` now accepts `preload` as a known tool field;
CONTRIBUTING.md and docs/pages/Contributing/Contributing.md updated to
document when to set it. Added a unit test covering the defensive
getTool behaviour and the `preload` propagation through
`toolConfigs`.
* chore: bump version to 5.0.8-20260512 [version bump]
* revert(plugins): remove Phase 4 lazy tool loading and preload mechanism
Phase 4 lazy emission caused cross-tool consumers (Map_ feature-click,
mmgisAPI, LegendTool) to receive raw '() => import(...)' arrows from
ToolController_.getTool(), breaking InfoTool open. Reverting to the
pre-Phase-4 behavior of static tool imports.
- API/updateTools.js: generated src/pre/tools.js now emits
'import FooTool from ...' for every tool (Kinds stays static too).
- ToolController_.js: getTool/makeTool back to sync; ensureToolLoaded,
getLoadedTool, preloadEagerTools deleted; separated-tool auto-open
flow simplified to direct sync calls.
- Toolbar.jsx, SeparatedTools.jsx, Layers_.js: revert async/lazy
patterns to sync ToolController_.toolModules[name] access.
- API/pluginValidation.js: drop 'preload' from KNOWN_FIELDS.
- src/essence/Tools/{Info,Draw,Layers,Chemistry}/config.json: drop
'preload: true'.
- CONTRIBUTING.md + docs: remove preload documentation.
- tests/unit/toolLazyLoading.spec.js: rewrite to verify static
imports instead of lazy loaders.
Also: log standard backends at startup (parity with plugin backends
and with tools/components), so all backends now produce
'info Loaded backend: <name> from <container>' at boot.
Phases 1-3 (per-plugin dependency aggregation, shared discoverPlugins,
config validation + override warnings) are unaffected.
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* feat(logger): new 'loaded' level (purple bg) for tool/component/backend startup
Previously the 'Loaded tool/component/backend: X from Y' lines used
the generic blue 'info' tag. They now use a dedicated 'loaded' level
rendered with a purple (#a855f7) background, so plugin discovery
output is visually distinct from other info messages.
- API/logger.js: add 'loaded' case to the dev-mode switch (white text
on purple bg) and suppress the redundant 'Caller:' echo for it
(matches how 'info' and 'success' are handled).
- API/updateTools.js: registerPlugin now logs at level 'loaded'.
Drops the redundant 'Loaded ' prefix since the level tag now reads
'loaded'.
- API/setups.js: standard and plugin backend startup logs use the
new level, same drop of the 'Loaded ' prefix.
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* feat(logs): 'Plugging in Tools/Components/Backends...' headings
Rename the cyan banner messages in scripts/build.js and scripts/server.js
from 'Updating Tools...' / 'Updating Components...' to 'Plugging in
Tools...' / 'Plugging in Components...' so the headings match the
plugin terminology used everywhere else (plugin-package.json,
discoverPlugins, etc.).
Also add a matching 'Plugging in Backends...' banner before
setups.getBackendSetups() in scripts/server.js so backends get an
equivalent title block to tools and components.
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* style(logs): cyan banner lines lead with a blank line instead of trailing one
Move the \n from the end to the beginning of every cyan banner in
scripts/build.js and scripts/server.js (Resolving Plugin Dependencies,
Plugging in Tools/Components/Backends, Validating Environment
Variables, Starting websocket, Starting the development server) so
that the blank line visually separates each section above its title
rather than below it.
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* feat(plugins): postinstall hook auto-installs plugin npm deps
Plain `npm install` (or `npm ci`) on a fresh clone now resolves and
installs every plugin's declared npm dependencies automatically, so
new developers don't need to remember a second command.
- scripts/install-plugin-deps.js (new): reads plugin-package.json,
filters out deps already declared in root package.json with the
same version specifier (no-op for the Animation transitional case),
installs the remainder with `npm install --no-save --no-package-lock
--ignore-scripts <pkg@ver> ...`. `--no-package-lock` keeps the
root lockfile clean; `--ignore-scripts` prevents the inner install
from re-entering postinstall and matches the Dockerfile.
- package.json: postinstall guards against the Dockerfile's
package.json-only layer (`scripts/` not copied yet) by checking
for the two script files via `node -e` before invoking them.
Adds a `plugins:install` npm script for on-demand runs.
- CONTRIBUTING.md + docs/pages/Contributing/Contributing.md: replace
the manual-install paragraph with a note about the postinstall
hook and the filtering behavior.
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* docs(plugins): manual Python install step for plugin pip/conda deps
Document that the npm postinstall hook only handles plugin npm deps —
plugin pip/conda deps must be installed manually after creating the
Python environment, since there's no portable way to detect which
interpreter or environment to target from a Node script.
- CONTRIBUTING.md: added a 'For local development ... Python' block
with the explicit `node scripts/resolve-plugin-deps.js` +
`micromamba run -n mmgis pip install -r plugin-python-requirements.txt`
+ optional conda install commands.
- docs/pages/Contributing/Contributing.md: matching short blurb in
the user-facing docs.
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* docs(install): plugin Python install step in Installation.md
Add a numbered step in docs/pages/Setup/Installation/Installation.md's
Setup sequence (right after `micromamba activate mmgis`) with the
`pip install -r plugin-python-requirements.txt` and optional
`micromamba install --file plugin-conda-deps.txt` commands, so
non-Docker installs have the step in their main flow rather than
buried in CONTRIBUTING.md.
Also adds a pointer from the Python Environment section earlier in
the same file (after the env-create + activate steps) back to the
numbered Setup step.
CONTRIBUTING.md and docs/pages/Contributing/Contributing.md are
slimmed: instead of duplicating the install commands, both now link
to the Installation page (this matches the user request — the install
commands live in installation docs, not contribution docs).
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix: serialize concurrent layer reloads and stop mutating layer.url
Concurrent mmgisAPI.reloadLayer() calls for the same layer were
silently dropped by the _layersBeingMade guard, and reloadLayer
mutated layer.url in-place during async work — causing a race where
a second reload would capture the resolved URL as its 'original' and
permanently corrupt the {starttime}/{endtime} template placeholders.
Changes:
- TimeControl.reloadLayer: compute resolvedUrl into a local variable
instead of mutating layer.url. Pass resolvedUrl through to
Map_.refreshLayer.
- Map_.refreshLayer: accept resolvedUrl; temporarily swap layer.url
inside a try/finally for the makeLayer call so the fetched URL is
the resolved one and the placeholder template is always restored.
- Map_.refreshLayer: when a reload is already in flight for the
same layer, queue the request (coalesced by name) instead of
dropping it with a 'Cannot make layer' warning.
- Map_.makeLayer: after releasing the lock, drain any queued reload
for this layer via setTimeout 0.
- TimeControl.reloadTimeLayers: become async and await Promise.all
of every per-layer reload; remove the setTimeout(500) workaround
around active-feature restoration and follow-pan logic.
- mmgisAPI.reloadLayers: new batch API that reloads multiple
time-enabled layers concurrently and returns a Promise<boolean[]>.
- Tests: new tests/e2e/map/concurrent-layer-reload.spec.js covers
the seven scenarios from the plan (single reload, URL preservation,
multi-layer concurrent reload, rapid same-layer reload, reloadLayers
API surface). tests/e2e/api/mmgis-api.spec.js gains a surface
check that reloadLayer/reloadLayers are exposed.
- Docs: Main.md documents the new reloadLayers entry.
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* chore: bump version to 5.0.8-20260513 [version bump]
* fix(TimeControl): use Promise.allSettled in reloadTimeLayers
Promise.all short-circuits on first rejection, which would skip the
active-feature restoration and follow-pan logic if any single layer
reload threw (network error, malformed config, etc.). The old
setTimeout(500) approach ran those steps unconditionally; switching
to Promise.allSettled preserves that robustness.
Addresses Devin Review feedback on PR #78.
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix(mmgisAPI): use Promise.allSettled in reloadLayers batch API
Mirrors the reloadTimeLayers fix: a single failing TimeControl.reloadLayer
call (e.g. unknown layer name throws inside asLayerUUID, network error,
malformed config) no longer rejects the whole batch. The returned array
preserves order and reports failed entries as false instead, matching
the documented Promise<boolean[]> contract.
Addresses second Devin Review finding on PR #78.
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* docs: note that reloadTimeLayers is now async (Promise<string[]>)
The implementation changed from sync to async in commit a096cfe9 (the
function now uses await + Promise.allSettled internally to coordinate
per-layer reloads), but the public API JSDoc and Main.md still
documented the old synchronous return type. External consumers using
the old synchronous return value would get a Promise instead of an
array.
Updates JSDoc on mmgisAPI.reloadTimeLayers to declare
Promise<string[]>, and rewrites the Main.md example to use 'await'.
Also fixes the previous example, which had a syntactically-malformed
trailing tuple-style index.
Addresses third Devin Review finding on PR #78.
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* test: split time-related e2e specs into tests/e2e/time/
Moves the two time-feature specs out of tests/e2e/map/ so the map suite
stays focused on map-UI behavior and the time/time-enabled-layer suite
can be run (and reasoned about) on its own:
- tests/e2e/map/time-control.spec.js
-> tests/e2e/time/time-control.spec.js
- tests/e2e/map/concurrent-layer-reload.spec.js
-> tests/e2e/time/concurrent-layer-reload.spec.js
Relative imports (../../helpers, ../../pages, ../../fixtures) are
unchanged because the new directory is the same depth. Playwright
picks the files up automatically via testDir './tests' +
testMatch '**/*.spec.js'.
Adds a matching 'npm run test:e2e:time' script and documents the new
suite in tests/README.md alongside the existing per-suite scripts.
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* test: cap local Playwright workers at 4
The previous `workers: process.env.CI ? 1 : undefined` resolved to
Playwright's default (~half the CPU cores). On higher-core machines
(e.g. 16 cores -> 8 workers) the dev server gets overloaded and the
suite actually runs slower. CI behavior is unchanged (1 worker).
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix: thread resolvedUrl through makeLayer/captureVector instead of mutating layer.url
The previous fix in Map_.refreshLayer temporarily swapped
`layerObj.url = resolvedUrl` during the `await makeLayer` call and
restored it in a finally block. Since `layerObj` is the shared
`L_.layers.data[name]` object, any concurrent code reading
`layer.url` during that async window could observe the resolved URL
instead of the template. Most importantly, a second
`TimeControl.reloadLayer()` call would then capture the resolved
URL as its 'template' and corrupt the placeholders for every
subsequent reload.
Surfaced by tests/e2e/time/concurrent-layer-reload.spec.js Test 5,
which after Promise.all of two reloads observed
`layer.url ==='geodatasets:...?from=...&to=...'` instead of the
expected `{starttime}/{endtime}` template.
Fix: thread `resolvedUrl` as an explicit parameter through
`Map_.refreshLayer` -> `makeLayer` -> `makeVectorLayer` ->
`captureVector` (via options.resolvedUrl). `captureVector` uses
`options.resolvedUrl` when provided and skips the
`{starttime}`/`{endtime}`/`{customtime.*}` regex replacement
(which TimeControl.reloadLayer already performed). `layer.url` is
NEVER mutated for the duration of the async operation, so the
template is preserved across overlapping reloads.
This also fully resolves the Devin Review #4 finding which flagged
the temporary URL swap as reintroducing the same race the PR was
meant to fix.
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix: also replace {time} placeholder when computing resolvedUrl
In the previous fix, captureVector skips its time-replacement block when
the caller supplies options.resolvedUrl (because TimeControl.reloadLayer
already performed those replacements). However TimeControl.reloadLayer
was only replacing {starttime}/{endtime}/{customtime.*} on resolvedUrl,
not {time} — which captureVector previously handled at
LayerCapturer.js:97 by mapping {time} -> endTime. This caused vector
layers using the documented {time} placeholder (see
docs/pages/Configure/Layers/Tile/Tile.md:90 and
docs/pages/APIs/JavaScript/Main/Main.md:482) to fetch URLs containing
the literal text '{time}' on time-triggered reloads.
Mirror the existing captureVector behavior: replace {time} with the
formatted end-time value alongside {starttime}/{endtime}, before the
resolved URL is threaded through Map_.refreshLayer -> makeLayer ->
captureVector.
Addresses Devin Review finding on commit fcba8101.
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix: always run time-placeholder replacement in captureVector (idempotent)
The previous fix gated captureVector's time-placeholder replacement
block on `!hasResolvedUrl`, on the assumption that any caller passing
options.resolvedUrl had already done the replacement.
That assumption only holds for time.type === 'global' / 'requery' /
forceRequery. For other time types that still flow through
Map_.refreshLayer into captureVector (most importantly
time.type === 'local' with endProp == null per TimeControl.js:276-287),
TimeControl.reloadLayer's resolved-URL replacement block at lines 249-273
is skipped, so the resolvedUrl arrives at captureVector still containing
literal {starttime}/{endtime}/{time} placeholders. The fetch then goes
out with unreplaced placeholders.
Fix: drop the !hasResolvedUrl guard and always run the replacement,
reading the source from `layerUrl` (which is already either
options.resolvedUrl or layerObj.url per the choice above). The
.replace(/{starttime}/g, ...) chain is idempotent on URLs that have
already been resolved — the regexes simply don't match — so the
correct path is restored without re-introducing the mutate-in-place
bug fcba8101 fixed.
Addresses Devin Review finding on commit ddb90dbb.
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* test: remove GIBS MODIS time tile test (always skips on external dependency)
The test at tests/e2e/time/time-control.spec.js depended on
gibs.earthdata.nasa.gov being reachable from the test environment and
served only as a placeholder — it skips every run since the test
infrastructure does not have external network access by policy. It
provides no signal in CI or locally, so removing it reduces noise in
the suite output without losing coverage.
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* test: add coverage for {time}, local-endProp-null, stress, and feature-presence cases
Four new e2e tests in tests/e2e/time/concurrent-layer-reload.spec.js,
each targeting a gap the original suite missed:
Test 8 — {time} placeholder preservation. Mirrors Test 2/5 but uses
`{time}` instead of `{starttime}/{endtime}`. Catches the Devin
Review #5 regression where captureVector's gating on !hasResolvedUrl
silently dropped the {time} -> endTime replacement (the literal
'{time}' would have ended up in the fetch URL).
Test 9 — local + endProp==null path. Sets layer.time.type='local'
and layer.time.endProp=null to force the TimeControl.reloadLayer
branch (TimeControl.js:276-287) that bypasses the resolved-URL
placeholder block and falls through to the else clause. Inspects
outgoing /geodatasets/* requests via page.on('request', ...) and
asserts no literal {starttime}/{endtime}/{time} remain. Catches the
Devin Review #6 regression: when this branch hit captureVector with
hasResolvedUrl=true, the !hasResolvedUrl gate previously short-
circuited the only remaining replacement site.
Test 10 — 20-reload stress burst. Extends Test 4's two-reload check
to 20 concurrent reloadLayer() calls, capturing 'Cannot make layer'
warnings to verify the queue coalesces requests instead of silently
dropping them. Also re-asserts layer.url template integrity post-
burst.
Test 11 — Feature-presence after concurrent reload. Captures
L_.layers.layer[key].getLayers().length before and after a 5-reload
burst. Asserts the count is still > 0 afterwards — the user-visible
'gaps where dynamically-appearing data doesn't show up' symptom from
the original bug report.
All four tests skip gracefully when their fixture layer is absent or
the dataset returns no rows, to avoid spurious failures across
mission configurations.
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* test: add 5 edge-case tests probing the blast radius of the URL fix
Five additional tests in tests/e2e/time/concurrent-layer-reload.spec.js
covering paths adjacent to the URL-mutation fix that were not exercised
by tests 1-11. Each was chosen because the production code on that path
changed (or now has a different contract) and a regression would not be
caught by the original race-condition tests.
Test 12 — {customtime.N} placeholder preservation. TimeControl.reloadLayer's
customtime replacement loop was migrated from `layer.url = ...` to
`resolvedUrl = ...`. Seeds TimeControl.customTimes.times so the loop
actually runs, then asserts the {customtime.0} placeholder remains
literally on layer.url after reload.
Test 13 — mmgisAPI.reloadTimeLayers() returns a Promise. This is the
backward-incompatible behavior change documented in
docs/pages/APIs/JavaScript/Main/Main.md (previously synchronous).
Asserts the returned value is a thenable that resolves to an array,
pinning the new contract so it does not silently regress.
Test 14 — mmgisAPI.reloadLayers handles unknown layer names. The
Promise.allSettled change requires that a failing per-layer reload
surfaces as `false` at the same array position as its input name,
without throwing. Mixes a valid name + an unknown name + another
valid name to verify the order and the boolean mapping.
Test 15 — Reloading a time-DISABLED vector layer leaves layer.url
unchanged. Discovers a candidate layer from L_.layers.data at runtime
(skips if none exist), reloads it, and asserts the URL is byte-equal
afterwards. Catches any accidental URL mutation introduced for the
non-time code path.
Test 16 — mmgisAPI.reloadLayers handles empty array, null, undefined,
and string inputs without throwing — verifying the Array.isArray guard
at mmgisAPI.js:618. Returns `[]` in all cases.
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix(makeLayer): wrap dispatch in try/finally so lock + queue drain always run
Address Devin Review finding: a thrown exception from any layer-type
builder (makeVectorLayer, makeVelocityLayer, etc.) inside makeLayer's
switch statement previously left lockRegistry[layerName] set to true
and skipped the queue drain entirely, since the release statement and
queue-drain block both lived AFTER the awaited dispatch.
Effect of the bug: any subsequent refreshLayer call for that layer
would queue against a permanently-locked entry that never drains.
The new queue mechanism inherits this pre-existing issue and makes
the failure mode worse — silent accumulation in _layerReloadQueue
instead of a visible 'Cannot make layer' warning.
Additional concern: the outer 'new Promise(async (resolve, reject) =>
{...})' is the async-executor anti-pattern. A throw inside the async
executor escapes to the unhandled-rejection handler instead of
rejecting the outer Promise — so the caller's 'await makeLayer(...)'
would hang indefinitely, compounding the lock-leak symptom.
Fix:
- Wrap the type-dispatch switch + Filtering.updateGeoJSON/
triggerFilter calls in a try/catch/finally.
- catch logs the error and tracks success via 'madeSuccessfully'.
- finally runs unconditionally: lockRegistry[layerName] = false,
drain L_._layerReloadQueue[layerObj.name] if present, then
resolve(madeSuccessfully). This ensures the lock release and
queue drain happen regardless of whether the inner builder
threw or completed normally.
Test (concurrent-layer-reload.spec.js):
Added probe-style test '_layersBeingMade lock is released after
single and concurrent reloads' that asserts the lock invariant:
1. After mmgisAPI.reloadLayer() resolves +
a 100ms drain window, _layersBeingMade[key] is false.
2. After 5 concurrent mmgisAPI.reloadLayer() calls resolve +
a 1000ms drain window, _layersBeingMade[key] is false.
3. _layerReloadQueue is empty afterwards (otherwise a future
reload would mistakenly trigger an immediate drain instead
of doing its own work).
This is a positive-invariant test — it catches accidental lock
retention even without force-triggering exceptions, which would
require monkey-patching webpack-internal module references that
aren't exposed on window.
Per user direction, NOT addressing Devin Review's separate finding
about the reloadTimeLayers sync->async breaking change at this
time (no version bump requested).
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* chore: bump version to 5.0.9-20260513 [version bump]
* fix(OperationsClock): bump z-index above bottomFloatingBar so clock stays visible
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix(plugin-deps): respect override semantics when aggregating dependencies
When a plugin tool/component/backend overrides a standard one by reusing
the same directory name, only the override's deps should contribute to
the aggregated plugin manifests. Previously, gatherDependencies()
concatenated standard + plugin entries and fed both to mergeNpm/
mergePython, which could spuriously flag the same package as
conflicting between the standard and override versions.
Extract winnersByName() (mirroring API/updateTools.js +
API/setups.js override behavior) and use it for all three plugin
kinds. Add unit tests covering the override case and the
spurious-conflict regression.
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix(Dockerfile): re-install plugin npm deps in runtime stage
Plugin deps installed in the builder via `npm install --no-save
--no-package-lock` aren't recorded in package.json/package-lock.json,
so the runtime stage's `npm ci --only=production` would lose them.
Frontend deps are bundled by webpack into ./build so they're fine, but
backend plugins that `require()` their declared npm dependencies at
runtime would crash with 'Cannot find module'.
Copy plugin-package.json from the builder and re-run the same
conditional install in the runtime stage so backend plugin deps land
in the runtime image's node_modules. `--ignore-scripts` prevents the
inner install from re-entering the root postinstall hook.
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* feat(OperationsClock): lift to 58px bottom when TimeUI is open
Add #operationsClock to BottomElementPositioner's reactive positioning
so it shifts to bottom:58px when timeUIActive is true (TimeUI dock
visible) and back to bottom:40px when closed. Avoids overlap with
the bottom floating bar without adding new state to OperationsClock
itself. Mobile path is unchanged — OperationsClock.setupMobilePositioning
manages mobile positioning separately.
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix(OperationsClock): always sit at bottom:58px
Revert the dynamic positioning in BottomElementPositioner and just
hardcode bottom:58px in OperationsClock.css. Simpler, no cross-cutting
state dependency.
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* feat: add Lunar South Pole reference mission variant (IAU2000:30120)
- Add REFERENCE_MISSION_VARIANTS registry and resolveVariantBlueprintPath
helper to missionTemplates.js for dynamic variant resolution
- Update configs.js to accept referenceMissionVariant parameter and
validate against the registry
- Add variant dropdown to NewMissionModal UI when Reference Mission
checkbox is enabled
- Create blueprint directory with south polar stereographic config
(IAU2000:30120, +proj=stere +lat_0=-90, bounds ±1095700/1095600)
- Add unit tests for variant registry, blueprint path resolution, and
Lunar-SouthPole projection assertions (15 tests)
- Add E2E smoke tests for Lunar-SouthPole mission variant
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* feat(api): pass through reloadLayer flags in reloadLayers
Forward evenIfOff, evenIfControlled, forceRequery, and
skipOrderedBringToFront parameters to TimeControl.reloadLayer()
for each layer in the batch. Fully backward-compatible — existing
callers that pass only layerNames get undefined for all flags.
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* chore: bump version to 5.0.10-20260518 [version bump]
* chore: bump version to 5.0.10-20260518 [version bump]
* docs(api): add forceRequery & skipOrderedBringToFront to reloadLayer/reloadLayers docs
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* Add SPole basemap to lunar ref mission
* chore: bump version to 5.0.11-20260518 [version bump]
* chore: remove unused blueprints/Missions/Test directory
The Test blueprint is no longer needed — the Reference-Mission and
Reference-Mission-Lunar-SouthPole blueprints serve as the basis for
demo, development, and testing.
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix: use setupReferenceMission string value as variant key fallback
Address Devin Review feedback: when setupReferenceMission is a non-empty
string (e.g. 'Lunar-SouthPole'), use it as the variant key if
referenceMissionVariant is not provided. Also guard against empty string
triggering reference mission creation.
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix(measure): rename config DEM field from 'dem' to 'url' and handle HTTP URLs
- Rename 'field': 'dem' → 'url' and 'name': 'DEM Path' → 'DEM URL' in
src/essence/Tools/Measure/config.json (layer-specific DEM objectarray)
- Rename same in configure/src/metaconfigs/layer-tile-config.json
- Add http:// and https:// prefix handling in makeProfile() so external
URLs are not mangled with the mission path prefix
The top-level variables.dem field (prima…
…986) * fix: mobile TimeUI opens in tool panel with header, end time, expanded rows - MobileTimeUIToggle now opens/closes the tool panel via ToolController_ - Closes any active tool before showing TimeUI - Forces expanded state when opening - CSS hides start time inputs, positions expanded content properly - Overrides absolute positioning of expanded content for tool panel flow Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * feat: rewrite separated tools system from jQuery to React components - Add separatedToolsList/activeSeparatedTools state to Zustand uiStore - Rewrite SeparatedTools.jsx with glassmorphism panels, CSS Module styling - Replace SepToolsContainer (setInterval hack) with SepToolButton/SepToolsSection - Remove ~170 lines of jQuery DOM construction from ToolController_.js - Fix hardcoded rgba(26,26,27,0.88) to theme-aware var(--color-a-rgb) - Remove separated tool entries from themeApplier.js - Remove separated tool overrides from FloatingElements.css - Move Legend CSS overrides from Toolbar.module.css to SeparatedTools.module.css - Remove jQuery active-state manipulation from IdentifierTool.js - Add store sync in Map_.js displayOnStart logic - Preserve all DOM IDs for backward compatibility (mmgisAPI, tool make()) Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * chore: bump version to 4.3.28-20260501 [version bump] * fix: TimeUI mobile checks — use Zustand store instead of L_.UserInterface_ L_.UserInterface_ is null when TimeUI.init() runs (TimeControl.init is called before L_.link sets UserInterface_). All 16 isMobile checks now read from useUIStore.getState().isMobile which is set at startup. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * chore: bump version to 4.3.29-20260501 [version bump] * fix: move displayOnStart logic from Map_.js to ToolController_.finalizeTools() - Map_ no longer references specific tools (LegendTool) - displayOnStart is now handled generically for all separated tools - Added DOM element polling (tryMake) to handle React render timing Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * revert: remove all TimeUI-related mobile changes Reverts TimeUI.js and BottomBar.js to development base. Restores #timeUI DOM removal in UserInterfaceBridge.fina(). Removes MobileTimeUIToggle component from Toolbar.jsx. Removes TimeUI mobile CSS overrides from UserInterfaceMobile_.css. Non-TimeUI refinements (toolbar height, scalebar positioning, etc.) preserved. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * simplify: remove DOM polling, use simple setTimeout(0) for auto-open LegendTool handles its own content lifecycle via subscribeOnLayerToggle. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * feat: mobile TimeUI — fix isMobile detection, staging container, toolbar toggle - TimeUI.js: import useUIStore and replace all 16 L_.UserInterface_?.isMobile checks with useUIStore.getState().isMobile (L_.UserInterface_ is null when TimeUI.init() runs, so mobile conditionals were dead code) - TimeUI.js: stage mobile #timeUI in hidden #timeUIMobileStaging instead of placing directly in #tools (which gets cleared by other tools) - UserInterfaceBridge.js: stop removing #timeUI from DOM on mobile - Toolbar.jsx: add MobileTimeUIToggle that moves #timeUI between staging and #tools, opens/closes tool panel via ToolController_ - BottomBar.js: hide TimeUI toggle from settings modal on mobile Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * fix: rescue #timeUI back to staging when another tool opens Subscribe to activeToolName changes — when a tool becomes active while TimeUI is showing, move #timeUI back to #timeUIMobileStaging before the new tool's make() clears #tools. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * chore: remove separatedTool/justification config toggles, fix review issues - Remove separatedTool checkbox and justification dropdown from Legend and Identifier config.json (these are always separated, not configurable) - Remove justification property/code from LegendTool.js, IdentifierTool.js - Simplify Globe_.js separated tool count (no justification filter) - Remove justification from Reference-Mission config blueprint - Update LegendTool help docs and Legend.md documentation - Add --color-a-rgb fallback (29,31,32) in SeparatedTools.module.css - Add display:none !important to .panelIdentifier to prevent 12px gap - Update e2e test comment Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * fix: circular import in TimeUI.js, toolbar/bottomFloatingBar position sync - TimeUI.js: replace top-level useUIStore import with lazy _getUIStore() accessor to avoid 'Cannot access useUIStore before initialization' circular import error at _remakeTimeSlider - SplitScreens.jsx: skip #timeUI reparenting observer on mobile (mobile uses MobileTimeUIToggle to manage #timeUI placement in #tools) - BottomElementPositioner.jsx: unify mobile transition to 0.3s (matches toolsWrapper and toolbar), guard pxIsTools against undefined - Toolbar.jsx: align toolbar transition to 0.3s ease-out Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * LegendTool fix empty message * chore: remove separated tools offset logic from Globe_.js Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * fix: skip _makeHistogram on mobile (no timeline slider, timestamps unset) _makeHistogram renders inside the timeline slider which doesn't exist on mobile. Without it, _timelineStartTimestamp is NaN, causing 'Invalid time value' RangeError at toISOString(). Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * fix: mobile TimeUI — populate expanded rows, fix Invalid date, fix panel height - TimeUI.js attachEvents: use _initialStart/_initialEnd on mobile (same as desktop) instead of L_.TimeControl_ which isn't set yet at init time. Fixes 'Invalid date' in start/end time inputs. - TimeUI.js fina: set expanded=true on mobile and call _populateExpandedRows() so year/month/day/hour rows actually render. Removed position:absolute and pointer-events:none overrides. - Toolbar.jsx: set tool panel height to 217px (TimeUI.height) instead of 45% viewport — matches actual TimeUI content height. - UserInterfaceMobile_.css: expanded content flows naturally (position:relative), hide start time inputs, allow overflow scroll, flex-wrap topbar. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * fix: mobile TimeUI justify-content center, restore toolbar border-bottom - Add justify-content: center to #mmgisTimeUIMain on mobile - Remove border-bottom: none override so toolbar keeps its default border Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * fix: mobile TimeUI overflow hidden, scalebar/compass fixed at 40px offset - #timeUI overflow-y: hidden (was auto, causing 2px scroll) - Scalebar/compass/map controls stay at fixed 40px offset (above toolbar) regardless of tool panel state — no longer shift up by pxIsTools Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Implement multi-tier knowledge architecture - Restructure AGENTS.md from 745 lines to 106 lines (Tier 1: essential context) - Create knowledge/ directory with 30+ wiki-style documentation files (Tier 2: deep knowledge) - Create knowledge/reference/ with 8 detailed reference files (Tier 3: lookup material) - Move AI-GETTING-STARTED.md and AI-DEVELOPMENT.md to knowledge/ - Update all file references in .specify/templates and blueprints - Create knowledge/README.md as the full knowledge base index - Create knowledge/reference/README.md as reference material index Three-tier knowledge discovery system: Tier 1: AGENTS.md (~106 lines) - scannable in <2 minutes Tier 2: knowledge/*.md - deep knowledge on architecture, tools, APIs, DB, infra Tier 3: knowledge/reference/*.md - coding conventions, API reference, troubleshooting Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * chore: bump version to 4.3.29-20260501 [version bump] * fix: mobile toolbar active button style matches desktop, fix icon alignment - All mobile toolbar buttons (ToolButton, MobileCoordButton, MobileTimeUIToggle) now use display:flex with align-items/justify-content center for proper vertical icon centering - MobileCoordButton: changed 'active' class to 'toolButtonActive' to match the global CSS active style (color-mmgis + color-i background) - Removed inline color overrides so CSS .toolButtonActive takes effect Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Add Devin knowledge notes from past MMGIS sessions Include curated lessons learned from past Devin sessions: - CI/CD: ignore build-arm64/amd64 failures, focus on required checks - Child sessions: no separate PRs when consolidating - ENV triple-update rule (.env, sample.env, ENVs.md) - Error handling: use logger with infrastructure_error for fatal startup errors - Path traversal security: stay within /Missions, handle subpath serving - Database initialization architecture and migration patterns - API authentication behavior across AUTH modes - Auto-generated MMGIS concept index Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * fix: mobile toolbar active button style, icon alignment, tool deactivation - Active toolbar buttons get desktop-matching margin (1px 0) and border-radius (8px) via .toolButton.toolButtonActive CSS rule - Removed line-height: 40px from .toolButton (flex centering handles vertical alignment, line-height was pushing icons up) - MobileCoordButton now watches activeToolName store and deactivates when another tool opens (fixes coords staying active) - MobileTimeUIToggle sets activeToolName='MobileTimeUI' when opening so coords/other buttons can detect it and deactivate - MobileTimeUIToggle clears activeToolName when closing - Both custom buttons skip self-deactivation via name check Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Fix cross-references: convert backtick refs to markdown links, add Devin knowledge notes Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * fix: mobile toolbar icon height 40px, button margins for active padding - #toolbar .toolButton i: height 40px fixes icon vertical alignment - #toolbar .toolButton: margin 0 2px gives spacing between buttons - #toolbar .toolButton.toolButtonActive: margin 1px 2px so active background has visual padding around the icon Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Rename knowledge/ to .knowledge/ for consistency with .specify/ convention Dot-prefix signals agent infrastructure (not source code), consistent with .specify/, .github/, .vscode/ conventions. All cross-references updated. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * fix: mobile toolbar icon line-height 40px, active button padding via height - Coord and TimeUI button <i> icons get line-height: 40px - Active buttons: height 34px (vs 40px toolbar) creates visual padding around the active background, centered by flex align-items - Buttons get margin: 0 1px for horizontal spacing Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Fix broken cross-reference: 06.2 -> 06.1-configure-rest-api.md Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * fix: close active tool + cancel deferred cleanup in MobileCoordButton/TimeUI - MobileCoordButton: call closeActiveTool() before opening, destroy _pendingCloseTool if set, increment _closeSeq to cancel deferred tools.innerHTML clear - MobileTimeUIToggle: same _pendingCloseTool + _closeSeq fix after closeActiveTool() to prevent 420ms deferred cleanup from wiping #timeUI after it's placed in #tools - Removed redundant closeActiveTool() from MobileCoordButton close path (was being called after destroy, not needed) Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * fix: active mobile toolbar buttons 34x34px (square) Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Drastically compress .knowledge/ — keep only unique agent content Remove 33 wiki files that duplicate docs/pages/ content. Remove 9 reference/ files derivable from source code. Keep only 5 files (down from 46): - AI-GETTING-STARTED.md (agent setup walkthrough) - AI-DEVELOPMENT.md (spec-kit workflow) - conventions-and-gotchas.md (naming, code style, common issues) - 12-devin-knowledge-notes.md (CI, auth, DB init, security gotchas) - README.md (index pointing to docs/pages/ for everything else) Principle: don't duplicate docs/ — only keep what's uniquely agent-optimized. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Rename to knowledge-notes.md, remove Devin branding and fork-specific CI section Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * fix: hide mmgis-map-logo on mobile Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Restore Database Safety Rules for AI Agents section in AGENTS.md Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * fix: shift compass and map scale 6px to the right (both mobile and desktop) Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Add back Important Instructions, code pattern templates, and detailed project structure - Important Instructions in AGENTS.md: MCP tools, hot-reload, Reference Mission - .knowledge/code-patterns.md: full directory tree with key directory annotations, plus copy-paste templates for Express routes, Sequelize models, Tool plugins, and WebSocket handlers Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Update project structure trees to reflect current filesystem Add missing directories: tests/, .knowledge/, .specify/, .github/, views/, private/, spice/, build/, examples/, scripts/middleware.js. Both abbreviated (AGENTS.md) and detailed (.knowledge/code-patterns.md) trees now match the actual repo layout. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * chore: bump version to 4.3.30-20260501 [version bump] * Add Layers_.js to project structure (key singleton L_) Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Fix project structure: correct API layout, frontend modules, code templates API/Backend/ uses feature-domain modules (Draw/, Users/, Config/, etc.) with setup.js + routes/ + models/ per feature — not APIs/ or Databases/. Frontend essence/ has Components/, Helpers/, LandingPage/, mmgisAPI/, services/ — not Ancillary/. Basics/ includes all singletons (Globe_, Formulae_, ToolController_, Viewer_, ComponentController_, Test_). Code templates updated to match actual patterns (setup.js, module.exports). Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * refactor: remove test infrastructure (Test_ module, testModules, DrawTool.test) - Delete src/essence/Basics/Test_/ directory - Delete src/essence/Tools/Draw/DrawTool.test.js - Remove Test_ import and Shift+T keydown handler from essence.js - Remove tests key from Draw tool config.json - Remove testModules generation logic from API/updateTools.js Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * chore: bump version to 4.3.31-20260501 [version bump] * style: move Cesium link button to top-right and match Leaflet zoom button styling - Change control container from top-left to top-right positioning - Update button size from 26px to 30px to match Leaflet zoom controls - Use CSS variables (--color-a, --color-f, --color-mmgis) instead of hardcoded colors - Add border-radius and box-shadow matching Leaflet control appearance - Update hover/inactive states to use themed colors Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * fix: anchor map logo to viewport instead of Leaflet map panel - Change MapLogo parent from .leaflet-bottom.leaflet-right to #main-container - Switch CSS position from absolute to fixed for viewport anchoring - Add explicit bottom-offset positioning in BottomElementPositioner (desktop) - Add explicit bottom-offset positioning in BottomElementPositioner (mobile) - Logo stays at viewport right edge regardless of open side panels - Retains smooth bottom offset transitions when bottom bar appears Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * docs: remove references to deleted test infrastructure (Test_, DrawTool.test) - Remove Test_/ from project structure in .knowledge/code-patterns.md - Remove DrawTool.test.js references from specs/006 spec, plan, and tasks - Remove Draw Tool Testing section from tasks.md Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * chore: bump version to 4.3.32-20260501 [version bump] * fix: append logo to document.body to avoid filter containing block #main-container has a CSS filter property which creates a new containing block per the CSS spec, causing position:fixed to behave like absolute. Appending to document.body ensures true viewport-fixed positioning. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * fix: prevent mobile topBarTitleName text wrapping by replacing max-width with white-space: nowrap Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * chore: bump version to 4.3.33-20260501 [version bump] * chore: bump version to 5.0.0 and update changelog Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * refactor(ui): move Screenshot/Fullscreen to BottomBar, About to TopBar kebab TopBar kebab menu now contains only Keyboard Shortcuts, Settings, and About (About now shows on both desktop and mobile). BottomBarReact now renders Screenshot, Fullscreen, and Copy Link buttons (top to bottom) following the same IconButton + Tooltip pattern. The About button has been removed from BottomBarReact. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * chore: bump version to 5.0.1-20260505 [version bump] * feat(mobile): enforce exclusive panel toggling on mobile in TopBar Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * chore: bump version to 5.0.1-20260505 [version bump] * style: reposition LithoSphere globe controls to match Leaflet/Cesium theme Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * chore: bump version to 5.0.1-20260505 [version bump] * feat(topbar): hide Viewer/Globe toggles based on configured panels Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * style(bottombar): reorder buttons (Copy Link, Screenshot, Fullscreen) and unify size Reorder the BottomBarReact buttons top-to-bottom to: Copy Link, Screenshot, Fullscreen. Move the 24x24 button sizing from the #topBarLink id selector in mmgis.css into the .barButton class in BottomBarReact.module.css so all three buttons share the same compact size as the original Copy Link button. Drop the now redundant #topBarLink rule. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * style(bottombar): increase padding-bottom to 12px and button margin to 3px 0 Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * style: rearrange globe controls — compass top-right circular, nav row, vertical column, panels open left Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * chore: bump version to 5.0.2-20260505 [version bump] * chore: bump version to 5.0.2-20260505 [version bump] * style: anchor observe settings panel right:34px and float nav hover panels Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * feat(theming): add 5 new themes, --color-shadow variable, and configure ThemePreview - Add Dark Terra, Dark Nebula, Dark Lunar, Dark Supernova, Light Botanical themes - Add --color-shadow CSS variable to every theme + :root fallback - Replace hardcoded rgba shadow colors with var(--color-shadow) in TopBar, Toolbar, SeparatedTools, ToolPanel, FloatingElements, Dropdown, Modal, and SplitScreens - Add Custom shadowcolor color picker in tab-ui-config and apply it via Stylize - Add ThemePreview component (configure/src) wired through Maker.js as a new 'themepreview' row type so the configure UI shows a live mini mockup of the selected theme Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * chore: bump version to 5.0.2-20260505 [version bump] * fix(configure/ThemePreview): tighten top spacing and live-preview Custom theme - Pull the preview up by 12px so the gap below the theme dropdown is tighter. - Read the Custom color pickers (look.primarycolor / secondarycolor / tertiarycolor / accentcolor / shadowcolor / topbarcolor / toolbarcolor / mapcolor) from the configuration and overlay them on Dark Default so the preview reflects Custom theme edits live, matching Stylize.js's runtime behavior. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * chore: bump version to 5.0.3-20260505 [version bump] * feat(themes): add Dark Heliosphere, Dark Monokai, and Light Solarized - Dark Heliosphere: deep night purple surface with corona-orange accent. - Dark Monokai: warm graphite surface with lime accent (Monokai-inspired). - Light Solarized: classic solarized base3/base02 with blue accent. Mirror added to configure/src/themes/themes.js for the ThemePreview, and the three names appended to the Color Theme dropdown options. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * fix(coordinates): respect time.initiallyOpen when live deep-link is set * chore: bump version to 5.0.3-20260505 [version bump] * refactor(theming): remove Custom theme + per-field color overrides - Drop the 'Custom' option from the Color Theme dropdown. - Remove all Custom Color Options (look.primarycolor, .secondarycolor, .tertiarycolor, .accentcolor, .bodycolor, .topbarcolor, .toolbarcolor, .mapcolor, .hightlightcolor, .shadowcolor) from tab-ui-config.json. - Strip the matching DOM/CSS-variable override block from Stylize.js; Stylize now just applies the selected preset theme (and the page logo). - Drop the empty bodycolor/topbarcolor/toolbarcolor/mapcolor/shadowcolor defaults from API/templates/config_template.js. - Simplify ThemePreview to render the selected preset directly — no Custom branch, no overlay logic. Preset themes cover all the looks we want and keep the configure surface much smaller. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * style(time-ui): round corners on TimeUI shell, action wrappers, mode dropdown - #timeUI: 10px border-radius on the outer time control bar. - #mmgisTimeUIActionsLeft / #mmgisTimeUIActionsRight: 10px border-radius so the action clusters sit as rounded chips. - #mmgisTimeUIActionsRight > div (excluding #mmgisTimeUIPresent): 10px border-radius on each action button so they match the wrapper. - #mmgisTimeUIModeDropdown: 40px height + 10px border-radius to align with the rest of the bar; clear the dropy default border-color so the rounded edge isn't outlined. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * chore: bump version to 5.0.4-20260505 [version bump] * feat(configure): mark light themes as (experimental) in dropdown label Light themes still have outstanding contrast issues, so flag them in the Color Theme dropdown without changing the saved value. - Maker dropdown now accepts options as either a plain string (current behavior) or { value, label } so the rendered label can differ from the persisted value. - tab-ui-config switches the six light themes to { value, label } form with '(experimental)' appended to the label only. Existing mission configs that already saved 'Light Default' etc. continue to match. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Fix timeUI border radius * fix(mobile): rescue #timeUI before tool make() destroys it Clicking Layers -> Time -> Layers -> Time on mobile caused the bottom panel to render LayersTool content with TimeUI height. The #timeUI DOM element was destroyed when LayersTool.make() called $('#tools').empty(), before the async React useEffect in MobileTimeUIToggle could rescue it to its staging container. - ToolController_.makeTool: synchronously move #timeUI from #tools back to #timeUIMobileStaging (and reset TimeUI store flags) on mobile, before invoking the new tool's make(). - MobileTimeUIToggle.handleClick: defensive fallback that re-initializes TimeUI if #timeUI no longer exists when the toggle is activated. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * fix(mobile): move re-initialized #timeUI from staging into #tools TimeUI.init() on mobile appends the new #timeUI to the hidden #timeUIMobileStaging container, so the fallback branch must also move it into #tools — otherwise the user sees an empty tool panel after the destroyed-element recovery path. Caught by Devin Review. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * fix(mobile): preserve #timeUI when Coordinates tool empties #tools On mobile, opening or closing the Coordinates tool runs $('#tools').empty() inside interfaceWithMMWebGIS / separateFromMMWebGIS. After the previous PR commits, clicking Coordinates -> Time still left the bottom panel empty because: - Coordinates.make() empties #tools while #timeUI is in staging (fine on its own), but the Coordinates teardown that fires after the user switches to the Time toggle (via MobileCoordButton's useEffect on activeToolName change) calls Coordinates.destroy() -> separateFromMMWebGIS(), which empties #tools wholesale and destroys the freshly-placed #timeUI. Add a rescueMobileTimeUI() helper that moves #timeUI from #tools back to #timeUIMobileStaging before each tools.empty() call in Coordinates, mirroring the rescue already done in ToolController_.makeTool(). Coordinates -> Time now correctly shows the TimeUI. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * fix(mobile): harden TimeUI fallback recovery (call fina(), de-dupe popovers) Devin Review correctly flagged that the safety-net path in MobileTimeUIToggle.handleClick was producing a partially-broken TimeUI when it fired: - TimeUI.init() unconditionally appends a new #timeUIPlayPopover_global to <body>, so a second init() left two elements with the same id. - TimeUI.init() alone does not wire up date pickers or per-button click handlers — that's TimeUI.fina()'s job. Without fina(), the recovered TimeUI rendered visually but Play / Previous / Next / Fit / Follow / Present / Expand were all dead. Before re-initializing, remove the stale #timeUIPlayPopover_global and #timeUIQuickSelectPopover_global divs to avoid duplicate ids. After the new #timeUI is moved into #tools, call TimeUI.fina() to populate the date pickers, attach the button click handlers, build the histogram, and populate the expanded mobile rows. Some delegated body/document handlers in attachEvents() will still be duplicated on this path; that is acceptable for a degraded recovery that should never run in practice now that the primary rescues in ToolController_.makeTool() and Coordinates.js cover all known paths. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * chore: bump version to 5.0.5-20260505 [version bump] * fix(mobile): Coordinates teardown only removes its own DOM The previous Coordinates fix was racing with itself: after the Time toggle synchronously moved #timeUI into #tools, MobileCoordButton's useEffect (triggered by the activeToolName change) ran on the next React tick and called L_.Coordinates.destroy(). That called separateFromMMWebGIS(), whose rescue moved #timeUI right back into the hidden staging div before tools.empty() — so the bottom panel ended up empty even though the time toggle was 'active'. Make separateFromMMWebGIS selective: only remove the Coordinates-specific DOM (#coordUIHeader and #CoordinatesDiv) instead of wiping all of #tools. Any other content already in #tools (e.g. #timeUI placed there by the Time toggle) is left alone. interfaceWithMMWebGIS still keeps the rescue + tools.empty() pattern on the open path so Coordinates always starts from a clean panel. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Bump DrawTool Temporal Drawings upward * chore: bump version to 5.0.6-20260505 [version bump] * chore: reset version to 5.0.0 Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * test(e2e): fix 9 pre-existing failures (test-only changes) - mmgis-api.spec.js: add form-fill login under AUTH=local; serialize describe to avoid concurrent-login race in the session store - coordinates.spec.js: TimeUI toggle was moved from the coordinates bar to the Settings modal; navigate via topbar kebab menu and assert the checkbox is rendered - widgets.spec.js: target .leaflet-control-zoom-in/-out specifically; the bare .leaflet-control-zoom class is also used by the home/reset control, so the original assertion was always false - sites.spec.js: scope panel selector to #toolPanel; both the toolbar icon and the panel container share id="SitesTool" Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * chore: bump version to 5.0.1-20260505 [version bump] * Revert "chore: bump version to 5.0.1-20260505 [version bump]" This reverts commit 4880204c1163be5d1d7fa96d14a0ed018c6f586c. * fix: prevent filter operator dropdown clipping in Layers panel Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * chore: bump version to 5.0.1-20260507 [version bump] * revert: keep dropy openUp:true for operator dropdowns Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Revert "chore: bump version to 5.0.1-20260507 [version bump]" This reverts commit d67c369ed437e47d658ae051348d377978dc48ed. * chore: bump version to 5.0.1-20260507 [version bump] * Revert "chore: bump version to 5.0.1-20260507 [version bump]" This reverts commit 29565ed829a55e9c241a789c9a3901d11cb5ca67. * chore: bump version to 5.0.1-20260507 [version bump] * Revert "chore: bump version to 5.0.1-20260507 [version bump]" This reverts commit 50e357604ebe9378564619b34c508b63cfb62c1d. * chore: bump version to 5.0.1-20260507 [version bump] * chore: bump version to 5.0.2-20260511 [version bump] * fix: render Globe panel immediately on first open without window resize Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * chore: bump version to 5.0.3-20260511 [version bump] * feat: add theme borders to panels and gradient backgrounds to splitters Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * chore: bump version to 5.0.4-20260511 [version bump] * style: bump split shadow gradient opacity to 0.4 Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * style: hotkeys modal 3-col grid + smaller leaflet zoom button gap Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * style: prevent hotkey label/value wrapping (ellipsis instead) Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * style: hotkeys modal single column, no wrap, no truncation Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * chore: bump version to 5.0.4-20260511 [version bump] * style: hotkeys modal dividers, invert title/subtitle colors, rename title, margin above subtitles Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * style: move splitter gradient to themed CSS class, restore hover feedback Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * chore: bump version to 5.0.5-20260511 [version bump] * style: hotkeys section titles use --color-h (matches rest of app) Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * chore: bump version to 5.0.5-20260511 [version bump] * fix: guard Globe_.init() inside rAF to prevent double instantiation Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * chore: bump version to 5.0.6-20260512 [version bump] * feat(plugins): per-plugin deps, lazy tool loading, validation, shared discovery Phase 3 — Plugin config validation + override warnings: - New API/pluginValidation.js with validatePluginConfig() for tool, component, and backend manifests. Validates required fields (name, paths), object/string shape of paths, dependencies block (npm/python.pip/python.conda), and warns on unknown top-level fields. - updateTools()/updateComponents() now skip invalid plugins and emit override warnings (matching what components already logged for tools). Phase 2 — Shared discoverPlugins() utility: - New API/pluginDiscovery.js consolidates the duplicated scanning logic from updateTools(), updateComponents(), and getBackendSetups(). Supports exact- name and substring container patterns, JSON/require/no-op loaders, and skips dot/underscore-prefixed dirs. - updateTools.js and setups.js refactored on top of the shared helper. Phase 1 — Per-plugin dependency declaration + build-time aggregation: - Plugin config.json may now declare a 'dependencies' block (npm + python.pip + python.conda). validatePluginConfig() also validates this shape. - New scripts/resolve-plugin-deps.js scans every tool/component/backend plugin and writes plugin-package.json, plugin-python-requirements.txt, and plugin-conda-deps.txt. Detects version conflicts and fails loudly. - scripts/build.js calls resolvePluginDeps() before updateTools(). - Dockerfile installs the aggregated plugin npm and pip deps after the root npm ci, using --no-save / --no-package-lock / --ignore-scripts so the root lockfile is untouched. - Animation tool migrated: ffmpeg/gifshot/html2canvas now declared in its config.json (kept in root package.json for transitional compat). - Generated artifacts gitignored. Phase 4 — Lazy loading of tool bundles: - updateTools() now emits dynamic-import arrow functions in the generated src/pre/tools.js with webpackChunkName hints so each tool is split into its own chunk (Kinds stays static because it's required synchronously). - ToolController_ gains ensureToolLoaded(name) and getLoadedTool(name) helpers and makeTool is async; init/finalizeTools and the separated-tool auto-open flow are updated to handle lazy modules. - Toolbar.jsx, SeparatedTools.jsx, SitesTool.js, and Layers_.js migrated to resolve LayersTool/etc. via the new helpers instead of poking toolModules directly. Tests & docs: - tests/fixtures/test-plugin-tools/{TestPlugin,InvalidPlugin,OverridePlugin} + tests/helpers/plugin-helpers.js with install/uninstall helpers. - New unit specs: pluginValidation, pluginDiscovery, updateTools, resolvePluginDeps, toolLazyLoading (57 tests, all passing). - CONTRIBUTING.md and docs/pages/Contributing/Contributing.md updated with schema, override behaviour, dependency declaration, build-time aggregation, conflict detection, and Docker integration. * chore: bump version to 5.0.7-20260512 [version bump] * fix: make Globe_.init() idempotent against multi-init Globe_.init() previously constructed a fresh GlobeRenderer on every call, which after #71 could happen multiple times for a single toggle (uiStore setTimeout + TopBar rAF). Each extra construction appends another .cesium-widget / _lithosphere_scene to #globe and leaves event handlers wired to dereferenced renderer state, which has been observed to break LithoSphere globe control buttons on configurations where the globe panel starts closed at boot. Add a top-of-init() guard that bails out and calls invalidateSize() when a renderer already exists. Single small, surgical change; no behavior change for the !L_.hasGlobe mock-swap path or for first-time construction. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * chore: bump version to 5.0.7-20260512 [version bump] * test(plugins): generate src/pre/tools.js on demand in toolLazyLoading spec The Playwright unit-tests CI step runs before `npm run build` so the gitignored `src/pre/tools.js` artifact does not yet exist on disk. Add a beforeAll hook that invokes `updateTools()` to regenerate it when missing, keeping the spec self-contained on both CI and dev machines that already built locally. * fix(tools): defensive getTool() + preload flag for cross-referenced tools Devin Review flagged a behavioural regression introduced by Phase 4: `ToolController_.getTool(name)` previously always returned a method- callable object (real module or `{ use(){} }` stub) because every tool was statically imported. After Phase 4, unresolved lazy loaders are `() => import(...)` functions, so callers like `Map_.getTool('InfoTool').use(...)`, `mmgisAPI.getTool('DrawTool').filesOn`, and `LegendTool` calling `LayersTool.populateCogScale` would crash with TypeError until the target tool was opened. Two fixes: 1. **Defensive getTool()**: Returns the legacy fallback stub when the tool module is still a lazy-loader function, and fires off `ensureToolLoaded(name)` in the background so subsequent calls see the resolved module. Prevents all crashes immediately. 2. **`preload: true` config flag**: Tools reached synchronously from other code paths (Info, Draw, Layers, Chemistry) now declare `"preload": true` in their `config.json`. `ToolController_.init()` calls `preloadEagerTools()` which fires `ensureToolLoaded` for every such tool right after toolbar setup — the chunks download in parallel with the rest of the page becoming interactive, so by the time a user clicks a feature the InfoTool module is already resolved. `validatePluginConfig` now accepts `preload` as a known tool field; CONTRIBUTING.md and docs/pages/Contributing/Contributing.md updated to document when to set it. Added a unit test covering the defensive getTool behaviour and the `preload` propagation through `toolConfigs`. * chore: bump version to 5.0.8-20260512 [version bump] * revert(plugins): remove Phase 4 lazy tool loading and preload mechanism Phase 4 lazy emission caused cross-tool consumers (Map_ feature-click, mmgisAPI, LegendTool) to receive raw '() => import(...)' arrows from ToolController_.getTool(), breaking InfoTool open. Reverting to the pre-Phase-4 behavior of static tool imports. - API/updateTools.js: generated src/pre/tools.js now emits 'import FooTool from ...' for every tool (Kinds stays static too). - ToolController_.js: getTool/makeTool back to sync; ensureToolLoaded, getLoadedTool, preloadEagerTools deleted; separated-tool auto-open flow simplified to direct sync calls. - Toolbar.jsx, SeparatedTools.jsx, Layers_.js: revert async/lazy patterns to sync ToolController_.toolModules[name] access. - API/pluginValidation.js: drop 'preload' from KNOWN_FIELDS. - src/essence/Tools/{Info,Draw,Layers,Chemistry}/config.json: drop 'preload: true'. - CONTRIBUTING.md + docs: remove preload documentation. - tests/unit/toolLazyLoading.spec.js: rewrite to verify static imports instead of lazy loaders. Also: log standard backends at startup (parity with plugin backends and with tools/components), so all backends now produce 'info Loaded backend: <name> from <container>' at boot. Phases 1-3 (per-plugin dependency aggregation, shared discoverPlugins, config validation + override warnings) are unaffected. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * feat(logger): new 'loaded' level (purple bg) for tool/component/backend startup Previously the 'Loaded tool/component/backend: X from Y' lines used the generic blue 'info' tag. They now use a dedicated 'loaded' level rendered with a purple (#a855f7) background, so plugin discovery output is visually distinct from other info messages. - API/logger.js: add 'loaded' case to the dev-mode switch (white text on purple bg) and suppress the redundant 'Caller:' echo for it (matches how 'info' and 'success' are handled). - API/updateTools.js: registerPlugin now logs at level 'loaded'. Drops the redundant 'Loaded ' prefix since the level tag now reads 'loaded'. - API/setups.js: standard and plugin backend startup logs use the new level, same drop of the 'Loaded ' prefix. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * feat(logs): 'Plugging in Tools/Components/Backends...' headings Rename the cyan banner messages in scripts/build.js and scripts/server.js from 'Updating Tools...' / 'Updating Components...' to 'Plugging in Tools...' / 'Plugging in Components...' so the headings match the plugin terminology used everywhere else (plugin-package.json, discoverPlugins, etc.). Also add a matching 'Plugging in Backends...' banner before setups.getBackendSetups() in scripts/server.js so backends get an equivalent title block to tools and components. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * style(logs): cyan banner lines lead with a blank line instead of trailing one Move the \n from the end to the beginning of every cyan banner in scripts/build.js and scripts/server.js (Resolving Plugin Dependencies, Plugging in Tools/Components/Backends, Validating Environment Variables, Starting websocket, Starting the development server) so that the blank line visually separates each section above its title rather than below it. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * feat(plugins): postinstall hook auto-installs plugin npm deps Plain `npm install` (or `npm ci`) on a fresh clone now resolves and installs every plugin's declared npm dependencies automatically, so new developers don't need to remember a second command. - scripts/install-plugin-deps.js (new): reads plugin-package.json, filters out deps already declared in root package.json with the same version specifier (no-op for the Animation transitional case), installs the remainder with `npm install --no-save --no-package-lock --ignore-scripts <pkg@ver> ...`. `--no-package-lock` keeps the root lockfile clean; `--ignore-scripts` prevents the inner install from re-entering postinstall and matches the Dockerfile. - package.json: postinstall guards against the Dockerfile's package.json-only layer (`scripts/` not copied yet) by checking for the two script files via `node -e` before invoking them. Adds a `plugins:install` npm script for on-demand runs. - CONTRIBUTING.md + docs/pages/Contributing/Contributing.md: replace the manual-install paragraph with a note about the postinstall hook and the filtering behavior. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * docs(plugins): manual Python install step for plugin pip/conda deps Document that the npm postinstall hook only handles plugin npm deps — plugin pip/conda deps must be installed manually after creating the Python environment, since there's no portable way to detect which interpreter or environment to target from a Node script. - CONTRIBUTING.md: added a 'For local development ... Python' block with the explicit `node scripts/resolve-plugin-deps.js` + `micromamba run -n mmgis pip install -r plugin-python-requirements.txt` + optional conda install commands. - docs/pages/Contributing/Contributing.md: matching short blurb in the user-facing docs. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * docs(install): plugin Python install step in Installation.md Add a numbered step in docs/pages/Setup/Installation/Installation.md's Setup sequence (right after `micromamba activate mmgis`) with the `pip install -r plugin-python-requirements.txt` and optional `micromamba install --file plugin-conda-deps.txt` commands, so non-Docker installs have the step in their main flow rather than buried in CONTRIBUTING.md. Also adds a pointer from the Python Environment section earlier in the same file (after the env-create + activate steps) back to the numbered Setup step. CONTRIBUTING.md and docs/pages/Contributing/Contributing.md are slimmed: instead of duplicating the install commands, both now link to the Installation page (this matches the user request — the install commands live in installation docs, not contribution docs). Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * fix: serialize concurrent layer reloads and stop mutating layer.url Concurrent mmgisAPI.reloadLayer() calls for the same layer were silently dropped by the _layersBeingMade guard, and reloadLayer mutated layer.url in-place during async work — causing a race where a second reload would capture the resolved URL as its 'original' and permanently corrupt the {starttime}/{endtime} template placeholders. Changes: - TimeControl.reloadLayer: compute resolvedUrl into a local variable instead of mutating layer.url. Pass resolvedUrl through to Map_.refreshLayer. - Map_.refreshLayer: accept resolvedUrl; temporarily swap layer.url inside a try/finally for the makeLayer call so the fetched URL is the resolved one and the placeholder template is always restored. - Map_.refreshLayer: when a reload is already in flight for the same layer, queue the request (coalesced by name) instead of dropping it with a 'Cannot make layer' warning. - Map_.makeLayer: after releasing the lock, drain any queued reload for this layer via setTimeout 0. - TimeControl.reloadTimeLayers: become async and await Promise.all of every per-layer reload; remove the setTimeout(500) workaround around active-feature restoration and follow-pan logic. - mmgisAPI.reloadLayers: new batch API that reloads multiple time-enabled layers concurrently and returns a Promise<boolean[]>. - Tests: new tests/e2e/map/concurrent-layer-reload.spec.js covers the seven scenarios from the plan (single reload, URL preservation, multi-layer concurrent reload, rapid same-layer reload, reloadLayers API surface). tests/e2e/api/mmgis-api.spec.js gains a surface check that reloadLayer/reloadLayers are exposed. - Docs: Main.md documents the new reloadLayers entry. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * chore: bump version to 5.0.8-20260513 [version bump] * fix(TimeControl): use Promise.allSettled in reloadTimeLayers Promise.all short-circuits on first rejection, which would skip the active-feature restoration and follow-pan logic if any single layer reload threw (network error, malformed config, etc.). The old setTimeout(500) approach ran those steps unconditionally; switching to Promise.allSettled preserves that robustness. Addresses Devin Review feedback on PR #78. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * fix(mmgisAPI): use Promise.allSettled in reloadLayers batch API Mirrors the reloadTimeLayers fix: a single failing TimeControl.reloadLayer call (e.g. unknown layer name throws inside asLayerUUID, network error, malformed config) no longer rejects the whole batch. The returned array preserves order and reports failed entries as false instead, matching the documented Promise<boolean[]> contract. Addresses second Devin Review finding on PR #78. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * docs: note that reloadTimeLayers is now async (Promise<string[]>) The implementation changed from sync to async in commit a096cfe9 (the function now uses await + Promise.allSettled internally to coordinate per-layer reloads), but the public API JSDoc and Main.md still documented the old synchronous return type. External consumers using the old synchronous return value would get a Promise instead of an array. Updates JSDoc on mmgisAPI.reloadTimeLayers to declare Promise<string[]>, and rewrites the Main.md example to use 'await'. Also fixes the previous example, which had a syntactically-malformed trailing tuple-style index. Addresses third Devin Review finding on PR #78. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * test: split time-related e2e specs into tests/e2e/time/ Moves the two time-feature specs out of tests/e2e/map/ so the map suite stays focused on map-UI behavior and the time/time-enabled-layer suite can be run (and reasoned about) on its own: - tests/e2e/map/time-control.spec.js -> tests/e2e/time/time-control.spec.js - tests/e2e/map/concurrent-layer-reload.spec.js -> tests/e2e/time/concurrent-layer-reload.spec.js Relative imports (../../helpers, ../../pages, ../../fixtures) are unchanged because the new directory is the same depth. Playwright picks the files up automatically via testDir './tests' + testMatch '**/*.spec.js'. Adds a matching 'npm run test:e2e:time' script and documents the new suite in tests/README.md alongside the existing per-suite scripts. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * test: cap local Playwright workers at 4 The previous `workers: process.env.CI ? 1 : undefined` resolved to Playwright's default (~half the CPU cores). On higher-core machines (e.g. 16 cores -> 8 workers) the dev server gets overloaded and the suite actually runs slower. CI behavior is unchanged (1 worker). Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * fix: thread resolvedUrl through makeLayer/captureVector instead of mutating layer.url The previous fix in Map_.refreshLayer temporarily swapped `layerObj.url = resolvedUrl` during the `await makeLayer` call and restored it in a finally block. Since `layerObj` is the shared `L_.layers.data[name]` object, any concurrent code reading `layer.url` during that async window could observe the resolved URL instead of the template. Most importantly, a second `TimeControl.reloadLayer()` call would then capture the resolved URL as its 'template' and corrupt the placeholders for every subsequent reload. Surfaced by tests/e2e/time/concurrent-layer-reload.spec.js Test 5, which after Promise.all of two reloads observed `layer.url ==='geodatasets:...?from=...&to=...'` instead of the expected `{starttime}/{endtime}` template. Fix: thread `resolvedUrl` as an explicit parameter through `Map_.refreshLayer` -> `makeLayer` -> `makeVectorLayer` -> `captureVector` (via options.resolvedUrl). `captureVector` uses `options.resolvedUrl` when provided and skips the `{starttime}`/`{endtime}`/`{customtime.*}` regex replacement (which TimeControl.reloadLayer already performed). `layer.url` is NEVER mutated for the duration of the async operation, so the template is preserved across overlapping reloads. This also fully resolves the Devin Review #4 finding which flagged the temporary URL swap as reintroducing the same race the PR was meant to fix. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * fix: also replace {time} placeholder when computing resolvedUrl In the previous fix, captureVector skips its time-replacement block when the caller supplies options.resolvedUrl (because TimeControl.reloadLayer already performed those replacements). However TimeControl.reloadLayer was only replacing {starttime}/{endtime}/{customtime.*} on resolvedUrl, not {time} — which captureVector previously handled at LayerCapturer.js:97 by mapping {time} -> endTime. This caused vector layers using the documented {time} placeholder (see docs/pages/Configure/Layers/Tile/Tile.md:90 and docs/pages/APIs/JavaScript/Main/Main.md:482) to fetch URLs containing the literal text '{time}' on time-triggered reloads. Mirror the existing captureVector behavior: replace {time} with the formatted end-time value alongside {starttime}/{endtime}, before the resolved URL is threaded through Map_.refreshLayer -> makeLayer -> captureVector. Addresses Devin Review finding on commit fcba8101. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * fix: always run time-placeholder replacement in captureVector (idempotent) The previous fix gated captureVector's time-placeholder replacement block on `!hasResolvedUrl`, on the assumption that any caller passing options.resolvedUrl had already done the replacement. That assumption only holds for time.type === 'global' / 'requery' / forceRequery. For other time types that still flow through Map_.refreshLayer into captureVector (most importantly time.type === 'local' with endProp == null per TimeControl.js:276-287), TimeControl.reloadLayer's resolved-URL replacement block at lines 249-273 is skipped, so the resolvedUrl arrives at captureVector still containing literal {starttime}/{endtime}/{time} placeholders. The fetch then goes out with unreplaced placeholders. Fix: drop the !hasResolvedUrl guard and always run the replacement, reading the source from `layerUrl` (which is already either options.resolvedUrl or layerObj.url per the choice above). The .replace(/{starttime}/g, ...) chain is idempotent on URLs that have already been resolved — the regexes simply don't match — so the correct path is restored without re-introducing the mutate-in-place bug fcba8101 fixed. Addresses Devin Review finding on commit ddb90dbb. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * test: remove GIBS MODIS time tile test (always skips on external dependency) The test at tests/e2e/time/time-control.spec.js depended on gibs.earthdata.nasa.gov being reachable from the test environment and served only as a placeholder — it skips every run since the test infrastructure does not have external network access by policy. It provides no signal in CI or locally, so removing it reduces noise in the suite output without losing coverage. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * test: add coverage for {time}, local-endProp-null, stress, and feature-presence cases Four new e2e tests in tests/e2e/time/concurrent-layer-reload.spec.js, each targeting a gap the original suite missed: Test 8 — {time} placeholder preservation. Mirrors Test 2/5 but uses `{time}` instead of `{starttime}/{endtime}`. Catches the Devin Review #5 regression where captureVector's gating on !hasResolvedUrl silently dropped the {time} -> endTime replacement (the literal '{time}' would have ended up in the fetch URL). Test 9 — local + endProp==null path. Sets layer.time.type='local' and layer.time.endProp=null to force the TimeControl.reloadLayer branch (TimeControl.js:276-287) that bypasses the resolved-URL placeholder block and falls through to the else clause. Inspects outgoing /geodatasets/* requests via page.on('request', ...) and asserts no literal {starttime}/{endtime}/{time} remain. Catches the Devin Review #6 regression: when this branch hit captureVector with hasResolvedUrl=true, the !hasResolvedUrl gate previously short- circuited the only remaining replacement site. Test 10 — 20-reload stress burst. Extends Test 4's two-reload check to 20 concurrent reloadLayer() calls, capturing 'Cannot make layer' warnings to verify the queue coalesces requests instead of silently dropping them. Also re-asserts layer.url template integrity post- burst. Test 11 — Feature-presence after concurrent reload. Captures L_.layers.layer[key].getLayers().length before and after a 5-reload burst. Asserts the count is still > 0 afterwards — the user-visible 'gaps where dynamically-appearing data doesn't show up' symptom from the original bug report. All four tests skip gracefully when their fixture layer is absent or the dataset returns no rows, to avoid spurious failures across mission configurations. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * test: add 5 edge-case tests probing the blast radius of the URL fix Five additional tests in tests/e2e/time/concurrent-layer-reload.spec.js covering paths adjacent to the URL-mutation fix that were not exercised by tests 1-11. Each was chosen because the production code on that path changed (or now has a different contract) and a regression would not be caught by the original race-condition tests. Test 12 — {customtime.N} placeholder preservation. TimeControl.reloadLayer's customtime replacement loop was migrated from `layer.url = ...` to `resolvedUrl = ...`. Seeds TimeControl.customTimes.times so the loop actually runs, then asserts the {customtime.0} placeholder remains literally on layer.url after reload. Test 13 — mmgisAPI.reloadTimeLayers() returns a Promise. This is the backward-incompatible behavior change documented in docs/pages/APIs/JavaScript/Main/Main.md (previously synchronous). Asserts the returned value is a thenable that resolves to an array, pinning the new contract so it does not silently regress. Test 14 — mmgisAPI.reloadLayers handles unknown layer names. The Promise.allSettled change requires that a failing per-layer reload surfaces as `false` at the same array position as its input name, without throwing. Mixes a valid name + an unknown name + another valid name to verify the order and the boolean mapping. Test 15 — Reloading a time-DISABLED vector layer leaves layer.url unchanged. Discovers a candidate layer from L_.layers.data at runtime (skips if none exist), reloads it, and asserts the URL is byte-equal afterwards. Catches any accidental URL mutation introduced for the non-time code path. Test 16 — mmgisAPI.reloadLayers handles empty array, null, undefined, and string inputs without throwing — verifying the Array.isArray guard at mmgisAPI.js:618. Returns `[]` in all cases. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * fix(makeLayer): wrap dispatch in try/finally so lock + queue drain always run Address Devin Review finding: a thrown exception from any layer-type builder (makeVectorLayer, makeVelocityLayer, etc.) inside makeLayer's switch statement previously left lockRegistry[layerName] set to true and skipped the queue drain entirely, since the release statement and queue-drain block both lived AFTER the awaited dispatch. Effect of the bug: any subsequent refreshLayer call for that layer would queue against a permanently-locked entry that never drains. The new queue mechanism inherits this pre-existing issue and makes the failure mode worse — silent accumulation in _layerReloadQueue instead of a visible 'Cannot make layer' warning. Additional concern: the outer 'new Promise(async (resolve, reject) => {...})' is the async-executor anti-pattern. A throw inside the async executor escapes to the unhandled-rejection handler instead of rejecting the outer Promise — so the caller's 'await makeLayer(...)' would hang indefinitely, compounding the lock-leak symptom. Fix: - Wrap the type-dispatch switch + Filtering.updateGeoJSON/ triggerFilter calls in a try/catch/finally. - catch logs the error and tracks success via 'madeSuccessfully'. - finally runs unconditionally: lockRegistry[layerName] = false, drain L_._layerReloadQueue[layerObj.name] if present, then resolve(madeSuccessfully). This ensures the lock release and queue drain happen regardless of whether the inner builder threw or completed normally. Test (concurrent-layer-reload.spec.js): Added probe-style test '_layersBeingMade lock is released after single and concurrent reloads' that asserts the lock invariant: 1. After mmgisAPI.reloadLayer() resolves + a 100ms drain window, _layersBeingMade[key] is false. 2. After 5 concurrent mmgisAPI.reloadLayer() calls resolve + a 1000ms drain window, _layersBeingMade[key] is false. 3. _layerReloadQueue is empty afterwards (otherwise a future reload would mistakenly trigger an immediate drain instead of doing its own work). This is a positive-invariant test — it catches accidental lock retention even without force-triggering exceptions, which would require monkey-patching webpack-internal module references that aren't exposed on window. Per user direction, NOT addressing Devin Review's separate finding about the reloadTimeLayers sync->async breaking change at this time (no version bump requested). Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * chore: bump version to 5.0.9-20260513 [version bump] * fix(OperationsClock): bump z-index above bottomFloatingBar so clock stays visible Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * fix(plugin-deps): respect override semantics when aggregating dependencies When a plugin tool/component/backend overrides a standard one by reusing the same directory name, only the override's deps should contribute to the aggregated plugin manifests. Previously, gatherDependencies() concatenated standard + plugin entries and fed both to mergeNpm/ mergePython, which could spuriously flag the same package as conflicting between the standard and override versions. Extract winnersByName() (mirroring API/updateTools.js + API/setups.js override behavior) and use it for all three plugin kinds. Add unit tests covering the override case and the spurious-conflict regression. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * fix(Dockerfile): re-install plugin npm deps in runtime stage Plugin deps installed in the builder via `npm install --no-save --no-package-lock` aren't recorded in package.json/package-lock.json, so the runtime stage's `npm ci --only=production` would lose them. Frontend deps are bundled by webpack into ./build so they're fine, but backend plugins that `require()` their declared npm dependencies at runtime would crash with 'Cannot find module'. Copy plugin-package.json from the builder and re-run the same conditional install in the runtime stage so backend plugin deps land in the runtime image's node_modules. `--ignore-scripts` prevents the inner install from re-entering the root postinstall hook. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * feat(OperationsClock): lift to 58px bottom when TimeUI is open Add #operationsClock to BottomElementPositioner's reactive positioning so it shifts to bottom:58px when timeUIActive is true (TimeUI dock visible) and back to bottom:40px when closed. Avoids overlap with the bottom floating bar without adding new state to OperationsClock itself. Mobile path is unchanged — OperationsClock.setupMobilePositioning manages mobile positioning separately. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * fix(OperationsClock): always sit at bottom:58px Revert the dynamic positioning in BottomElementPositioner and just hardcode bottom:58px in OperationsClock.css. Simpler, no cross-cutting state dependency. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * feat: add Lunar South Pole reference mission variant (IAU2000:30120) - Add REFERENCE_MISSION_VARIANTS registry and resolveVariantBlueprintPath helper to missionTemplates.js for dynamic variant resolution - Update configs.js to accept referenceMissionVariant parameter and validate against the registry - Add variant dropdown to NewMissionModal UI when Reference Mission checkbox is enabled - Create blueprint directory with south polar stereographic config (IAU2000:30120, +proj=stere +lat_0=-90, bounds ±1095700/1095600) - Add unit tests for variant registry, blueprint path resolution, and Lunar-SouthPole projection assertions (15 tests) - Add E2E smoke tests for Lunar-SouthPole mission variant Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * feat(api): pass through reloadLayer flags in reloadLayers Forward evenIfOff, evenIfControlled, forceRequery, and skipOrderedBringToFront parameters to TimeControl.reloadLayer() for each layer in the batch. Fully backward-compatible — existing callers that pass only layerNames get undefined for all flags. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * chore: bump version to 5.0.10-20260518 [version bump] * chore: bump version to 5.0.10-20260518 [version bump] * docs(api): add forceRequery & skipOrderedBringToFront to reloadLayer/reloadLayers docs Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Add SPole basemap to lunar ref mission * chore: bump version to 5.0.11-20260518 [version bump] * chore: remove unused blueprints/Missions/Test directory The Test blueprint is no longer needed — the Reference-Mission and Reference-Mission-Lunar-SouthPole blueprints serve as the basis for demo, development, and testing. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * fix: use setupReferenceMission string value as variant key fallback Address Devin Review feedback: when setupReferenceMission is a non-empty string (e.g. 'Lunar-SouthPole'), use it as the variant key if referenceMissionVariant is not provided. Also guard against empty string triggering reference mission creation. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * fix(measure): rename config DEM field from 'dem' to 'url' and handle HTTP URLs - Rename 'field': 'dem' → 'url' and 'name': 'DEM Path' → 'DEM URL' in src/essence/Tools/Measure/config.json (layer-specific DEM objectarray) - Rename same in configure/src/metaconfigs/layer-tile-config.json - Add http:// and https:// prefix handling in makeProfile() so external URLs are not mangled with the mission path prefix The top-level variables.dem field (primary DEM) is unchanged. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * chore: bump version to 5.0.11-20260519 [version bump] * revert: remove http/https URL prefix handling in makeProfile() Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * fix(measure): rename 'dem' to 'url' in Reference Mission layerDems blueprint Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * fix(measure): accept both 'url' and 'dem' fields in layerDems for backward compatibility Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * chore: bump version to 5.0.12-20260519 [version bump] * feat: add SPole_100m basemap layer to Lunar South Pole blueprint config Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * fix: show Save to Base Blueprint button for all reference mission variants Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Update lunar ref mission * style: add text-align right to mission list buttons in configure sidebar Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * fix(ui): SegmentTool background, clipboard API, context menu test selectors -…
…s removal (#987) * chore: remove separated tools offset logic from Globe_.js Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * fix: skip _makeHistogram on mobile (no timeline slider, timestamps unset) _makeHistogram renders inside the timeline slider which doesn't exist on mobile. Without it, _timelineStartTimestamp is NaN, causing 'Invalid time value' RangeError at toISOString(). Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * fix: mobile TimeUI — populate expanded rows, fix Invalid date, fix panel height - TimeUI.js attachEvents: use _initialStart/_initialEnd on mobile (same as desktop) instead of L_.TimeControl_ which isn't set yet at init time. Fixes 'Invalid date' in start/end time inputs. - TimeUI.js fina: set expanded=true on mobile and call _populateExpandedRows() so year/month/day/hour rows actually render. Removed position:absolute and pointer-events:none overrides. - Toolbar.jsx: set tool panel height to 217px (TimeUI.height) instead of 45% viewport — matches actual TimeUI content height. - UserInterfaceMobile_.css: expanded content flows naturally (position:relative), hide start time inputs, allow overflow scroll, flex-wrap topbar. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * fix: mobile TimeUI justify-content center, restore toolbar border-bottom - Add justify-content: center to #mmgisTimeUIMain on mobile - Remove border-bottom: none override so toolbar keeps its default border Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * fix: mobile TimeUI overflow hidden, scalebar/compass fixed at 40px offset - #timeUI overflow-y: hidden (was auto, causing 2px scroll) - Scalebar/compass/map controls stay at fixed 40px offset (above toolbar) regardless of tool panel state — no longer shift up by pxIsTools Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Implement multi-tier knowledge architecture - Restructure AGENTS.md from 745 lines to 106 lines (Tier 1: essential context) - Create knowledge/ directory with 30+ wiki-style documentation files (Tier 2: deep knowledge) - Create knowledge/reference/ with 8 detailed reference files (Tier 3: lookup material) - Move AI-GETTING-STARTED.md and AI-DEVELOPMENT.md to knowledge/ - Update all file references in .specify/templates and blueprints - Create knowledge/README.md as the full knowledge base index - Create knowledge/reference/README.md as reference material index Three-tier knowledge discovery system: Tier 1: AGENTS.md (~106 lines) - scannable in <2 minutes Tier 2: knowledge/*.md - deep knowledge on architecture, tools, APIs, DB, infra Tier 3: knowledge/reference/*.md - coding conventions, API reference, troubleshooting Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * chore: bump version to 4.3.29-20260501 [version bump] * fix: mobile toolbar active button style matches desktop, fix icon alignment - All mobile toolbar buttons (ToolButton, MobileCoordButton, MobileTimeUIToggle) now use display:flex with align-items/justify-content center for proper vertical icon centering - MobileCoordButton: changed 'active' class to 'toolButtonActive' to match the global CSS active style (color-mmgis + color-i background) - Removed inline color overrides so CSS .toolButtonActive takes effect Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Add Devin knowledge notes from past MMGIS sessions Include curated lessons learned from past Devin sessions: - CI/CD: ignore build-arm64/amd64 failures, focus on required checks - Child sessions: no separate PRs when consolidating - ENV triple-update rule (.env, sample.env, ENVs.md) - Error handling: use logger with infrastructure_error for fatal startup errors - Path traversal security: stay within /Missions, handle subpath serving - Database initialization architecture and migration patterns - API authentication behavior across AUTH modes - Auto-generated MMGIS concept index Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * fix: mobile toolbar active button style, icon alignment, tool deactivation - Active toolbar buttons get desktop-matching margin (1px 0) and border-radius (8px) via .toolButton.toolButtonActive CSS rule - Removed line-height: 40px from .toolButton (flex centering handles vertical alignment, line-height was pushing icons up) - MobileCoordButton now watches activeToolName store and deactivates when another tool opens (fixes coords staying active) - MobileTimeUIToggle sets activeToolName='MobileTimeUI' when opening so coords/other buttons can detect it and deactivate - MobileTimeUIToggle clears activeToolName when closing - Both custom buttons skip self-deactivation via name check Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Fix cross-references: convert backtick refs to markdown links, add Devin knowledge notes Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * fix: mobile toolbar icon height 40px, button margins for active padding - #toolbar .toolButton i: height 40px fixes icon vertical alignment - #toolbar .toolButton: margin 0 2px gives spacing between buttons - #toolbar .toolButton.toolButtonActive: margin 1px 2px so active background has visual padding around the icon Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Rename knowledge/ to .knowledge/ for consistency with .specify/ convention Dot-prefix signals agent infrastructure (not source code), consistent with .specify/, .github/, .vscode/ conventions. All cross-references updated. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * fix: mobile toolbar icon line-height 40px, active button padding via height - Coord and TimeUI button <i> icons get line-height: 40px - Active buttons: height 34px (vs 40px toolbar) creates visual padding around the active background, centered by flex align-items - Buttons get margin: 0 1px for horizontal spacing Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Fix broken cross-reference: 06.2 -> 06.1-configure-rest-api.md Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * fix: close active tool + cancel deferred cleanup in MobileCoordButton/TimeUI - MobileCoordButton: call closeActiveTool() before opening, destroy _pendingCloseTool if set, increment _closeSeq to cancel deferred tools.innerHTML clear - MobileTimeUIToggle: same _pendingCloseTool + _closeSeq fix after closeActiveTool() to prevent 420ms deferred cleanup from wiping #timeUI after it's placed in #tools - Removed redundant closeActiveTool() from MobileCoordButton close path (was being called after destroy, not needed) Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * fix: active mobile toolbar buttons 34x34px (square) Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Drastically compress .knowledge/ — keep only unique agent content Remove 33 wiki files that duplicate docs/pages/ content. Remove 9 reference/ files derivable from source code. Keep only 5 files (down from 46): - AI-GETTING-STARTED.md (agent setup walkthrough) - AI-DEVELOPMENT.md (spec-kit workflow) - conventions-and-gotchas.md (naming, code style, common issues) - 12-devin-knowledge-notes.md (CI, auth, DB init, security gotchas) - README.md (index pointing to docs/pages/ for everything else) Principle: don't duplicate docs/ — only keep what's uniquely agent-optimized. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Rename to knowledge-notes.md, remove Devin branding and fork-specific CI section Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * fix: hide mmgis-map-logo on mobile Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Restore Database Safety Rules for AI Agents section in AGENTS.md Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * fix: shift compass and map scale 6px to the right (both mobile and desktop) Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Add back Important Instructions, code pattern templates, and detailed project structure - Important Instructions in AGENTS.md: MCP tools, hot-reload, Reference Mission - .knowledge/code-patterns.md: full directory tree with key directory annotations, plus copy-paste templates for Express routes, Sequelize models, Tool plugins, and WebSocket handlers Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Update project structure trees to reflect current filesystem Add missing directories: tests/, .knowledge/, .specify/, .github/, views/, private/, spice/, build/, examples/, scripts/middleware.js. Both abbreviated (AGENTS.md) and detailed (.knowledge/code-patterns.md) trees now match the actual repo layout. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * chore: bump version to 4.3.30-20260501 [version bump] * Add Layers_.js to project structure (key singleton L_) Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Fix project structure: correct API layout, frontend modules, code templates API/Backend/ uses feature-domain modules (Draw/, Users/, Config/, etc.) with setup.js + routes/ + models/ per feature — not APIs/ or Databases/. Frontend essence/ has Components/, Helpers/, LandingPage/, mmgisAPI/, services/ — not Ancillary/. Basics/ includes all singletons (Globe_, Formulae_, ToolController_, Viewer_, ComponentController_, Test_). Code templates updated to match actual patterns (setup.js, module.exports). Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * refactor: remove test infrastructure (Test_ module, testModules, DrawTool.test) - Delete src/essence/Basics/Test_/ directory - Delete src/essence/Tools/Draw/DrawTool.test.js - Remove Test_ import and Shift+T keydown handler from essence.js - Remove tests key from Draw tool config.json - Remove testModules generation logic from API/updateTools.js Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * chore: bump version to 4.3.31-20260501 [version bump] * style: move Cesium link button to top-right and match Leaflet zoom button styling - Change control container from top-left to top-right positioning - Update button size from 26px to 30px to match Leaflet zoom controls - Use CSS variables (--color-a, --color-f, --color-mmgis) instead of hardcoded colors - Add border-radius and box-shadow matching Leaflet control appearance - Update hover/inactive states to use themed colors Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * fix: anchor map logo to viewport instead of Leaflet map panel - Change MapLogo parent from .leaflet-bottom.leaflet-right to #main-container - Switch CSS position from absolute to fixed for viewport anchoring - Add explicit bottom-offset positioning in BottomElementPositioner (desktop) - Add explicit bottom-offset positioning in BottomElementPositioner (mobile) - Logo stays at viewport right edge regardless of open side panels - Retains smooth bottom offset transitions when bottom bar appears Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * docs: remove references to deleted test infrastructure (Test_, DrawTool.test) - Remove Test_/ from project structure in .knowledge/code-patterns.md - Remove DrawTool.test.js references from specs/006 spec, plan, and tasks - Remove Draw Tool Testing section from tasks.md Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * chore: bump version to 4.3.32-20260501 [version bump] * fix: append logo to document.body to avoid filter containing block #main-container has a CSS filter property which creates a new containing block per the CSS spec, causing position:fixed to behave like absolute. Appending to document.body ensures true viewport-fixed positioning. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * fix: prevent mobile topBarTitleName text wrapping by replacing max-width with white-space: nowrap Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * chore: bump version to 4.3.33-20260501 [version bump] * chore: bump version to 5.0.0 and update changelog Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * refactor(ui): move Screenshot/Fullscreen to BottomBar, About to TopBar kebab TopBar kebab menu now contains only Keyboard Shortcuts, Settings, and About (About now shows on both desktop and mobile). BottomBarReact now renders Screenshot, Fullscreen, and Copy Link buttons (top to bottom) following the same IconButton + Tooltip pattern. The About button has been removed from BottomBarReact. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * chore: bump version to 5.0.1-20260505 [version bump] * feat(mobile): enforce exclusive panel toggling on mobile in TopBar Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * chore: bump version to 5.0.1-20260505 [version bump] * style: reposition LithoSphere globe controls to match Leaflet/Cesium theme Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * chore: bump version to 5.0.1-20260505 [version bump] * feat(topbar): hide Viewer/Globe toggles based on configured panels Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * style(bottombar): reorder buttons (Copy Link, Screenshot, Fullscreen) and unify size Reorder the BottomBarReact buttons top-to-bottom to: Copy Link, Screenshot, Fullscreen. Move the 24x24 button sizing from the #topBarLink id selector in mmgis.css into the .barButton class in BottomBarReact.module.css so all three buttons share the same compact size as the original Copy Link button. Drop the now redundant #topBarLink rule. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * style(bottombar): increase padding-bottom to 12px and button margin to 3px 0 Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * style: rearrange globe controls — compass top-right circular, nav row, vertical column, panels open left Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * chore: bump version to 5.0.2-20260505 [version bump] * chore: bump version to 5.0.2-20260505 [version bump] * style: anchor observe settings panel right:34px and float nav hover panels Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * feat(theming): add 5 new themes, --color-shadow variable, and configure ThemePreview - Add Dark Terra, Dark Nebula, Dark Lunar, Dark Supernova, Light Botanical themes - Add --color-shadow CSS variable to every theme + :root fallback - Replace hardcoded rgba shadow colors with var(--color-shadow) in TopBar, Toolbar, SeparatedTools, ToolPanel, FloatingElements, Dropdown, Modal, and SplitScreens - Add Custom shadowcolor color picker in tab-ui-config and apply it via Stylize - Add ThemePreview component (configure/src) wired through Maker.js as a new 'themepreview' row type so the configure UI shows a live mini mockup of the selected theme Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * chore: bump version to 5.0.2-20260505 [version bump] * fix(configure/ThemePreview): tighten top spacing and live-preview Custom theme - Pull the preview up by 12px so the gap below the theme dropdown is tighter. - Read the Custom color pickers (look.primarycolor / secondarycolor / tertiarycolor / accentcolor / shadowcolor / topbarcolor / toolbarcolor / mapcolor) from the configuration and overlay them on Dark Default so the preview reflects Custom theme edits live, matching Stylize.js's runtime behavior. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * chore: bump version to 5.0.3-20260505 [version bump] * feat(themes): add Dark Heliosphere, Dark Monokai, and Light Solarized - Dark Heliosphere: deep night purple surface with corona-orange accent. - Dark Monokai: warm graphite surface with lime accent (Monokai-inspired). - Light Solarized: classic solarized base3/base02 with blue accent. Mirror added to configure/src/themes/themes.js for the ThemePreview, and the three names appended to the Color Theme dropdown options. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * fix(coordinates): respect time.initiallyOpen when live deep-link is set * chore: bump version to 5.0.3-20260505 [version bump] * refactor(theming): remove Custom theme + per-field color overrides - Drop the 'Custom' option from the Color Theme dropdown. - Remove all Custom Color Options (look.primarycolor, .secondarycolor, .tertiarycolor, .accentcolor, .bodycolor, .topbarcolor, .toolbarcolor, .mapcolor, .hightlightcolor, .shadowcolor) from tab-ui-config.json. - Strip the matching DOM/CSS-variable override block from Stylize.js; Stylize now just applies the selected preset theme (and the page logo). - Drop the empty bodycolor/topbarcolor/toolbarcolor/mapcolor/shadowcolor defaults from API/templates/config_template.js. - Simplify ThemePreview to render the selected preset directly — no Custom branch, no overlay logic. Preset themes cover all the looks we want and keep the configure surface much smaller. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * style(time-ui): round corners on TimeUI shell, action wrappers, mode dropdown - #timeUI: 10px border-radius on the outer time control bar. - #mmgisTimeUIActionsLeft / #mmgisTimeUIActionsRight: 10px border-radius so the action clusters sit as rounded chips. - #mmgisTimeUIActionsRight > div (excluding #mmgisTimeUIPresent): 10px border-radius on each action button so they match the wrapper. - #mmgisTimeUIModeDropdown: 40px height + 10px border-radius to align with the rest of the bar; clear the dropy default border-color so the rounded edge isn't outlined. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * chore: bump version to 5.0.4-20260505 [version bump] * feat(configure): mark light themes as (experimental) in dropdown label Light themes still have outstanding contrast issues, so flag them in the Color Theme dropdown without changing the saved value. - Maker dropdown now accepts options as either a plain string (current behavior) or { value, label } so the rendered label can differ from the persisted value. - tab-ui-config switches the six light themes to { value, label } form with '(experimental)' appended to the label only. Existing mission configs that already saved 'Light Default' etc. continue to match. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Fix timeUI border radius * fix(mobile): rescue #timeUI before tool make() destroys it Clicking Layers -> Time -> Layers -> Time on mobile caused the bottom panel to render LayersTool content with TimeUI height. The #timeUI DOM element was destroyed when LayersTool.make() called $('#tools').empty(), before the async React useEffect in MobileTimeUIToggle could rescue it to its staging container. - ToolController_.makeTool: synchronously move #timeUI from #tools back to #timeUIMobileStaging (and reset TimeUI store flags) on mobile, before invoking the new tool's make(). - MobileTimeUIToggle.handleClick: defensive fallback that re-initializes TimeUI if #timeUI no longer exists when the toggle is activated. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * fix(mobile): move re-initialized #timeUI from staging into #tools TimeUI.init() on mobile appends the new #timeUI to the hidden #timeUIMobileStaging container, so the fallback branch must also move it into #tools — otherwise the user sees an empty tool panel after the destroyed-element recovery path. Caught by Devin Review. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * fix(mobile): preserve #timeUI when Coordinates tool empties #tools On mobile, opening or closing the Coordinates tool runs $('#tools').empty() inside interfaceWithMMWebGIS / separateFromMMWebGIS. After the previous PR commits, clicking Coordinates -> Time still left the bottom panel empty because: - Coordinates.make() empties #tools while #timeUI is in staging (fine on its own), but the Coordinates teardown that fires after the user switches to the Time toggle (via MobileCoordButton's useEffect on activeToolName change) calls Coordinates.destroy() -> separateFromMMWebGIS(), which empties #tools wholesale and destroys the freshly-placed #timeUI. Add a rescueMobileTimeUI() helper that moves #timeUI from #tools back to #timeUIMobileStaging before each tools.empty() call in Coordinates, mirroring the rescue already done in ToolController_.makeTool(). Coordinates -> Time now correctly shows the TimeUI. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * fix(mobile): harden TimeUI fallback recovery (call fina(), de-dupe popovers) Devin Review correctly flagged that the safety-net path in MobileTimeUIToggle.handleClick was producing a partially-broken TimeUI when it fired: - TimeUI.init() unconditionally appends a new #timeUIPlayPopover_global to <body>, so a second init() left two elements with the same id. - TimeUI.init() alone does not wire up date pickers or per-button click handlers — that's TimeUI.fina()'s job. Without fina(), the recovered TimeUI rendered visually but Play / Previous / Next / Fit / Follow / Present / Expand were all dead. Before re-initializing, remove the stale #timeUIPlayPopover_global and #timeUIQuickSelectPopover_global divs to avoid duplicate ids. After the new #timeUI is moved into #tools, call TimeUI.fina() to populate the date pickers, attach the button click handlers, build the histogram, and populate the expanded mobile rows. Some delegated body/document handlers in attachEvents() will still be duplicated on this path; that is acceptable for a degraded recovery that should never run in practice now that the primary rescues in ToolController_.makeTool() and Coordinates.js cover all known paths. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * chore: bump version to 5.0.5-20260505 [version bump] * fix(mobile): Coordinates teardown only removes its own DOM The previous Coordinates fix was racing with itself: after the Time toggle synchronously moved #timeUI into #tools, MobileCoordButton's useEffect (triggered by the activeToolName change) ran on the next React tick and called L_.Coordinates.destroy(). That called separateFromMMWebGIS(), whose rescue moved #timeUI right back into the hidden staging div before tools.empty() — so the bottom panel ended up empty even though the time toggle was 'active'. Make separateFromMMWebGIS selective: only remove the Coordinates-specific DOM (#coordUIHeader and #CoordinatesDiv) instead of wiping all of #tools. Any other content already in #tools (e.g. #timeUI placed there by the Time toggle) is left alone. interfaceWithMMWebGIS still keeps the rescue + tools.empty() pattern on the open path so Coordinates always starts from a clean panel. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Bump DrawTool Temporal Drawings upward * chore: bump version to 5.0.6-20260505 [version bump] * chore: reset version to 5.0.0 Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * test(e2e): fix 9 pre-existing failures (test-only changes) - mmgis-api.spec.js: add form-fill login under AUTH=local; serialize describe to avoid concurrent-login race in the session store - coordinates.spec.js: TimeUI toggle was moved from the coordinates bar to the Settings modal; navigate via topbar kebab menu and assert the checkbox is rendered - widgets.spec.js: target .leaflet-control-zoom-in/-out specifically; the bare .leaflet-control-zoom class is also used by the home/reset control, so the original assertion was always false - sites.spec.js: scope panel selector to #toolPanel; both the toolbar icon and the panel container share id="SitesTool" Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * chore: bump version to 5.0.1-20260505 [version bump] * Revert "chore: bump version to 5.0.1-20260505 [version bump]" This reverts commit 4880204c1163be5d1d7fa96d14a0ed018c6f586c. * fix: prevent filter operator dropdown clipping in Layers panel Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * chore: bump version to 5.0.1-20260507 [version bump] * revert: keep dropy openUp:true for operator dropdowns Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Revert "chore: bump version to 5.0.1-20260507 [version bump]" This reverts commit d67c369ed437e47d658ae051348d377978dc48ed. * chore: bump version to 5.0.1-20260507 [version bump] * Revert "chore: bump version to 5.0.1-20260507 [version bump]" This reverts commit 29565ed829a55e9c241a789c9a3901d11cb5ca67. * chore: bump version to 5.0.1-20260507 [version bump] * Revert "chore: bump version to 5.0.1-20260507 [version bump]" This reverts commit 50e357604ebe9378564619b34c508b63cfb62c1d. * chore: bump version to 5.0.1-20260507 [version bump] * chore: bump version to 5.0.2-20260511 [version bump] * fix: render Globe panel immediately on first open without window resize Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * chore: bump version to 5.0.3-20260511 [version bump] * feat: add theme borders to panels and gradient backgrounds to splitters Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * chore: bump version to 5.0.4-20260511 [version bump] * style: bump split shadow gradient opacity to 0.4 Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * style: hotkeys modal 3-col grid + smaller leaflet zoom button gap Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * style: prevent hotkey label/value wrapping (ellipsis instead) Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * style: hotkeys modal single column, no wrap, no truncation Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * chore: bump version to 5.0.4-20260511 [version bump] * style: hotkeys modal dividers, invert title/subtitle colors, rename title, margin above subtitles Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * style: move splitter gradient to themed CSS class, restore hover feedback Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * chore: bump version to 5.0.5-20260511 [version bump] * style: hotkeys section titles use --color-h (matches rest of app) Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * chore: bump version to 5.0.5-20260511 [version bump] * fix: guard Globe_.init() inside rAF to prevent double instantiation Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * chore: bump version to 5.0.6-20260512 [version bump] * feat(plugins): per-plugin deps, lazy tool loading, validation, shared discovery Phase 3 — Plugin config validation + override warnings: - New API/pluginValidation.js with validatePluginConfig() for tool, component, and backend manifests. Validates required fields (name, paths), object/string shape of paths, dependencies block (npm/python.pip/python.conda), and warns on unknown top-level fields. - updateTools()/updateComponents() now skip invalid plugins and emit override warnings (matching what components already logged for tools). Phase 2 — Shared discoverPlugins() utility: - New API/pluginDiscovery.js consolidates the duplicated scanning logic from updateTools(), updateComponents(), and getBackendSetups(). Supports exact- name and substring container patterns, JSON/require/no-op loaders, and skips dot/underscore-prefixed dirs. - updateTools.js and setups.js refactored on top of the shared helper. Phase 1 — Per-plugin dependency declaration + build-time aggregation: - Plugin config.json may now declare a 'dependencies' block (npm + python.pip + python.conda). validatePluginConfig() also validates this shape. - New scripts/resolve-plugin-deps.js scans every tool/component/backend plugin and writes plugin-package.json, plugin-python-requirements.txt, and plugin-conda-deps.txt. Detects version conflicts and fails loudly. - scripts/build.js calls resolvePluginDeps() before updateTools(). - Dockerfile installs the aggregated plugin npm and pip deps after the root npm ci, using --no-save / --no-package-lock / --ignore-scripts so the root lockfile is untouched. - Animation tool migrated: ffmpeg/gifshot/html2canvas now declared in its config.json (kept in root package.json for transitional compat). - Generated artifacts gitignored. Phase 4 — Lazy loading of tool bundles: - updateTools() now emits dynamic-import arrow functions in the generated src/pre/tools.js with webpackChunkName hints so each tool is split into its own chunk (Kinds stays static because it's required synchronously). - ToolController_ gains ensureToolLoaded(name) and getLoadedTool(name) helpers and makeTool is async; init/finalizeTools and the separated-tool auto-open flow are updated to handle lazy modules. - Toolbar.jsx, SeparatedTools.jsx, SitesTool.js, and Layers_.js migrated to resolve LayersTool/etc. via the new helpers instead of poking toolModules directly. Tests & docs: - tests/fixtures/test-plugin-tools/{TestPlugin,InvalidPlugin,OverridePlugin} + tests/helpers/plugin-helpers.js with install/uninstall helpers. - New unit specs: pluginValidation, pluginDiscovery, updateTools, resolvePluginDeps, toolLazyLoading (57 tests, all passing). - CONTRIBUTING.md and docs/pages/Contributing/Contributing.md updated with schema, override behaviour, dependency declaration, build-time aggregation, conflict detection, and Docker integration. * chore: bump version to 5.0.7-20260512 [version bump] * fix: make Globe_.init() idempotent against multi-init Globe_.init() previously constructed a fresh GlobeRenderer on every call, which after #71 could happen multiple times for a single toggle (uiStore setTimeout + TopBar rAF). Each extra construction appends another .cesium-widget / _lithosphere_scene to #globe and leaves event handlers wired to dereferenced renderer state, which has been observed to break LithoSphere globe control buttons on configurations where the globe panel starts closed at boot. Add a top-of-init() guard that bails out and calls invalidateSize() when a renderer already exists. Single small, surgical change; no behavior change for the !L_.hasGlobe mock-swap path or for first-time construction. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * chore: bump version to 5.0.7-20260512 [version bump] * test(plugins): generate src/pre/tools.js on demand in toolLazyLoading spec The Playwright unit-tests CI step runs before `npm run build` so the gitignored `src/pre/tools.js` artifact does not yet exist on disk. Add a beforeAll hook that invokes `updateTools()` to regenerate it when missing, keeping the spec self-contained on both CI and dev machines that already built locally. * fix(tools): defensive getTool() + preload flag for cross-referenced tools Devin Review flagged a behavioural regression introduced by Phase 4: `ToolController_.getTool(name)` previously always returned a method- callable object (real module or `{ use(){} }` stub) because every tool was statically imported. After Phase 4, unresolved lazy loaders are `() => import(...)` functions, so callers like `Map_.getTool('InfoTool').use(...)`, `mmgisAPI.getTool('DrawTool').filesOn`, and `LegendTool` calling `LayersTool.populateCogScale` would crash with TypeError until the target tool was opened. Two fixes: 1. **Defensive getTool()**: Returns the legacy fallback stub when the tool module is still a lazy-loader function, and fires off `ensureToolLoaded(name)` in the background so subsequent calls see the resolved module. Prevents all crashes immediately. 2. **`preload: true` config flag**: Tools reached synchronously from other code paths (Info, Draw, Layers, Chemistry) now declare `"preload": true` in their `config.json`. `ToolController_.init()` calls `preloadEagerTools()` which fires `ensureToolLoaded` for every such tool right after toolbar setup — the chunks download in parallel with the rest of the page becoming interactive, so by the time a user clicks a feature the InfoTool module is already resolved. `validatePluginConfig` now accepts `preload` as a known tool field; CONTRIBUTING.md and docs/pages/Contributing/Contributing.md updated to document when to set it. Added a unit test covering the defensive getTool behaviour and the `preload` propagation through `toolConfigs`. * chore: bump version to 5.0.8-20260512 [version bump] * revert(plugins): remove Phase 4 lazy tool loading and preload mechanism Phase 4 lazy emission caused cross-tool consumers (Map_ feature-click, mmgisAPI, LegendTool) to receive raw '() => import(...)' arrows from ToolController_.getTool(), breaking InfoTool open. Reverting to the pre-Phase-4 behavior of static tool imports. - API/updateTools.js: generated src/pre/tools.js now emits 'import FooTool from ...' for every tool (Kinds stays static too). - ToolController_.js: getTool/makeTool back to sync; ensureToolLoaded, getLoadedTool, preloadEagerTools deleted; separated-tool auto-open flow simplified to direct sync calls. - Toolbar.jsx, SeparatedTools.jsx, Layers_.js: revert async/lazy patterns to sync ToolController_.toolModules[name] access. - API/pluginValidation.js: drop 'preload' from KNOWN_FIELDS. - src/essence/Tools/{Info,Draw,Layers,Chemistry}/config.json: drop 'preload: true'. - CONTRIBUTING.md + docs: remove preload documentation. - tests/unit/toolLazyLoading.spec.js: rewrite to verify static imports instead of lazy loaders. Also: log standard backends at startup (parity with plugin backends and with tools/components), so all backends now produce 'info Loaded backend: <name> from <container>' at boot. Phases 1-3 (per-plugin dependency aggregation, shared discoverPlugins, config validation + override warnings) are unaffected. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * feat(logger): new 'loaded' level (purple bg) for tool/component/backend startup Previously the 'Loaded tool/component/backend: X from Y' lines used the generic blue 'info' tag. They now use a dedicated 'loaded' level rendered with a purple (#a855f7) background, so plugin discovery output is visually distinct from other info messages. - API/logger.js: add 'loaded' case to the dev-mode switch (white text on purple bg) and suppress the redundant 'Caller:' echo for it (matches how 'info' and 'success' are handled). - API/updateTools.js: registerPlugin now logs at level 'loaded'. Drops the redundant 'Loaded ' prefix since the level tag now reads 'loaded'. - API/setups.js: standard and plugin backend startup logs use the new level, same drop of the 'Loaded ' prefix. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * feat(logs): 'Plugging in Tools/Components/Backends...' headings Rename the cyan banner messages in scripts/build.js and scripts/server.js from 'Updating Tools...' / 'Updating Components...' to 'Plugging in Tools...' / 'Plugging in Components...' so the headings match the plugin terminology used everywhere else (plugin-package.json, discoverPlugins, etc.). Also add a matching 'Plugging in Backends...' banner before setups.getBackendSetups() in scripts/server.js so backends get an equivalent title block to tools and components. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * style(logs): cyan banner lines lead with a blank line instead of trailing one Move the \n from the end to the beginning of every cyan banner in scripts/build.js and scripts/server.js (Resolving Plugin Dependencies, Plugging in Tools/Components/Backends, Validating Environment Variables, Starting websocket, Starting the development server) so that the blank line visually separates each section above its title rather than below it. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * feat(plugins): postinstall hook auto-installs plugin npm deps Plain `npm install` (or `npm ci`) on a fresh clone now resolves and installs every plugin's declared npm dependencies automatically, so new developers don't need to remember a second command. - scripts/install-plugin-deps.js (new): reads plugin-package.json, filters out deps already declared in root package.json with the same version specifier (no-op for the Animation transitional case), installs the remainder with `npm install --no-save --no-package-lock --ignore-scripts <pkg@ver> ...`. `--no-package-lock` keeps the root lockfile clean; `--ignore-scripts` prevents the inner install from re-entering postinstall and matches the Dockerfile. - package.json: postinstall guards against the Dockerfile's package.json-only layer (`scripts/` not copied yet) by checking for the two script files via `node -e` before invoking them. Adds a `plugins:install` npm script for on-demand runs. - CONTRIBUTING.md + docs/pages/Contributing/Contributing.md: replace the manual-install paragraph with a note about the postinstall hook and the filtering behavior. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * docs(plugins): manual Python install step for plugin pip/conda deps Document that the npm postinstall hook only handles plugin npm deps — plugin pip/conda deps must be installed manually after creating the Python environment, since there's no portable way to detect which interpreter or environment to target from a Node script. - CONTRIBUTING.md: added a 'For local development ... Python' block with the explicit `node scripts/resolve-plugin-deps.js` + `micromamba run -n mmgis pip install -r plugin-python-requirements.txt` + optional conda install commands. - docs/pages/Contributing/Contributing.md: matching short blurb in the user-facing docs. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * docs(install): plugin Python install step in Installation.md Add a numbered step in docs/pages/Setup/Installation/Installation.md's Setup sequence (right after `micromamba activate mmgis`) with the `pip install -r plugin-python-requirements.txt` and optional `micromamba install --file plugin-conda-deps.txt` commands, so non-Docker installs have the step in their main flow rather than buried in CONTRIBUTING.md. Also adds a pointer from the Python Environment section earlier in the same file (after the env-create + activate steps) back to the numbered Setup step. CONTRIBUTING.md and docs/pages/Contributing/Contributing.md are slimmed: instead of duplicating the install commands, both now link to the Installation page (this matches the user request — the install commands live in installation docs, not contribution docs). Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * fix: serialize concurrent layer reloads and stop mutating layer.url Concurrent mmgisAPI.reloadLayer() calls for the same layer were silently dropped by the _layersBeingMade guard, and reloadLayer mutated layer.url in-place during async work — causing a race where a second reload would capture the resolved URL as its 'original' and permanently corrupt the {starttime}/{endtime} template placeholders. Changes: - TimeControl.reloadLayer: compute resolvedUrl into a local variable instead of mutating layer.url. Pass resolvedUrl through to Map_.refreshLayer. - Map_.refreshLayer: accept resolvedUrl; temporarily swap layer.url inside a try/finally for the makeLayer call so the fetched URL is the resolved one and the placeholder template is always restored. - Map_.refreshLayer: when a reload is already in flight for the same layer, queue the request (coalesced by name) instead of dropping it with a 'Cannot make layer' warning. - Map_.makeLayer: after releasing the lock, drain any queued reload for this layer via setTimeout 0. - TimeControl.reloadTimeLayers: become async and await Promise.all of every per-layer reload; remove the setTimeout(500) workaround around active-feature restoration and follow-pan logic. - mmgisAPI.reloadLayers: new batch API that reloads multiple time-enabled layers concurrently and returns a Promise<boolean[]>. - Tests: new tests/e2e/map/concurrent-layer-reload.spec.js covers the seven scenarios from the plan (single reload, URL preservation, multi-layer concurrent reload, rapid same-layer reload, reloadLayers API surface). tests/e2e/api/mmgis-api.spec.js gains a surface check that reloadLayer/reloadLayers are exposed. - Docs: Main.md documents the new reloadLayers entry. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * chore: bump version to 5.0.8-20260513 [version bump] * fix(TimeControl): use Promise.allSettled in reloadTimeLayers Promise.all short-circuits on first rejection, which would skip the active-feature restoration and follow-pan logic if any single layer reload threw (network error, malformed config, etc.). The old setTimeout(500) approach ran those steps unconditionally; switching to Promise.allSettled preserves that robustness. Addresses Devin Review feedback on PR #78. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * fix(mmgisAPI): use Promise.allSettled in reloadLayers batch API Mirrors the reloadTimeLayers fix: a single failing TimeControl.reloadLayer call (e.g. unknown layer name throws inside asLayerUUID, network error, malformed config) no longer rejects the whole batch. The returned array preserves order and reports failed entries as false instead, matching the documented Promise<boolean[]> contract. Addresses second Devin Review finding on PR #78. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * docs: note that reloadTimeLayers is now async (Promise<string[]>) The implementation changed from sync to async in commit a096cfe9 (the function now uses await + Promise.allSettled internally to coordinate per-layer reloads), but the public API JSDoc and Main.md still documented the old synchronous return type. External consumers using the old synchronous return value would get a Promise instead of an array. Updates JSDoc on mmgisAPI.reloadTimeLayers to declare Promise<string[]>, and rewrites the Main.md example to use 'await'. Also fixes the previous example, which had a syntactically-malformed trailing tuple-style index. Addresses third Devin Review finding on PR #78. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * test: split time-related e2e specs into tests/e2e/time/ Moves the two time-feature specs out of tests/e2e/map/ so the map suite stays focused on map-UI behavior and the time/time-enabled-layer suite can be run (and reasoned about) on its own: - tests/e2e/map/time-control.spec.js -> tests/e2e/time/time-control.spec.js - tests/e2e/map/concurrent-layer-reload.spec.js -> tests/e2e/time/concurrent-layer-reload.spec.js Relative imports (../../helpers, ../../pages, ../../fixtures) are unchanged because the new directory is the same depth. Playwright picks the files up automatically via testDir './tests' + testMatch '**/*.spec.js'. Adds a matching 'npm run test:e2e:time' script and documents the new suite in tests/README.md alongside the existing per-suite scripts. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * test: cap local Playwright workers at 4 The previous `workers: process.env.CI ? 1 : undefined` resolved to Playwright's default (~half the CPU cores). On higher-core machines (e.g. 16 cores -> 8 workers) the dev server gets overloaded and the suite actually runs slower. CI behavior is unchanged (1 worker). Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * fix: thread resolvedUrl through makeLayer/captureVector instead of mutating layer.url The previous fix in Map_.refreshLayer temporarily swapped `layerObj.url = resolvedUrl` during the `await makeLayer` call and restored it in a finally block. Since `layerObj` is the shared `L_.layers.data[name]` object, any concurrent code reading `layer.url` during that async window could observe the resolved URL instead of the template. Most importantly, a second `TimeControl.reloadLayer()` call would then capture the resolved URL as its 'template' and corrupt the placeholders for every subsequent reload. Surfaced by tests/e2e/time/concurrent-layer-reload.spec.js Test 5, which after Promise.all of two reloads observed `layer.url ==='geodatasets:...?from=...&to=...'` instead of the expected `{starttime}/{endtime}` template. Fix: thread `resolvedUrl` as an explicit parameter through `Map_.refreshLayer` -> `makeLayer` -> `makeVectorLayer` -> `captureVector` (via options.resolvedUrl). `captureVector` uses `options.resolvedUrl` when provided and skips the `{starttime}`/`{endtime}`/`{customtime.*}` regex replacement (which TimeControl.reloadLayer already performed). `layer.url` is NEVER mutated for the duration of the async operation, so the template is preserved across overlapping reloads. This also fully resolves the Devin Review #4 finding which flagged the temporary URL swap as reintroducing the same race the PR was meant to fix. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * fix: also replace {time} placeholder when computing resolvedUrl In the previous fix, captureVector skips its time-replacement block when the caller supplies options.resolvedUrl (because TimeControl.reloadLayer already performed those replacements). However TimeControl.reloadLayer was only replacing {starttime}/{endtime}/{customtime.*} on resolvedUrl, not {time} — which captureVector previously handled at LayerCapturer.js:97 by mapping {time} -> endTime. This caused vector layers using the documented {time} placeholder (see docs/pages/Configure/Layers/Tile/Tile.md:90 and docs/pages/APIs/JavaScript/Main/Main.md:482) to fetch URLs containing the literal text '{time}' on time-triggered reloads. Mirror the existing captureVector behavior: replace {time} with the formatted end-time value alongside {starttime}/{endtime}, before the resolved URL is threaded through Map_.refreshLayer -> makeLayer -> captureVector. Addresses Devin Review finding on commit fcba8101. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * fix: always run time-placeholder replacement in captureVector (idempotent) The previous fix gated captureVector's time-placeholder replacement block on `!hasResolvedUrl`, on the assumption that any caller passing options.resolvedUrl had already done the replacement. That assumption only holds for time.type === 'global' / 'requery' / forceRequery. For other time types that still flow through Map_.refreshLayer into captureVector (most importantly time.type === 'local' with endProp == null per TimeControl.js:276-287), TimeControl.reloadLayer's resolved-URL replacement block at lines 249-273 is skipped, so the resolvedUrl arrives at captureVector still containing literal {starttime}/{endtime}/{time} placeholders. The fetch then goes out with unreplaced placeholders. Fix: drop the !hasResolvedUrl guard and always run the replacement, reading the source from `layerUrl` (which is already either options.resolvedUrl or layerObj.url per the choice above). The .replace(/{starttime}/g, ...) chain is idempotent on URLs that have already been resolved — the regexes simply don't match — so the correct path is restored without re-introducing the mutate-in-place bug fcba8101 fixed. Addresses Devin Review finding on commit ddb90dbb. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * test: remove GIBS MODIS time tile test (always skips on external dependency) The test at tests/e2e/time/time-control.spec.js depended on gibs.earthdata.nasa.gov being reachable from the test environment and served only as a placeholder — it skips every run since the test infrastructure does not have external network access by policy. It provides no signal in CI or locally, so removing it reduces noise in the suite output without losing coverage. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * test: add coverage for {time}, local-endProp-null, stress, and feature-presence cases Four new e2e tests in tests/e2e/time/concurrent-layer-reload.spec.js, each targeting a gap the original suite missed: Test 8 — {time} placeholder preservation. Mirrors Test 2/5 but uses `{time}` instead of `{starttime}/{endtime}`. Catches the Devin Review #5 regression where captureVector's gating on !hasResolvedUrl silently dropped the {time} -> endTime replacement (the literal '{time}' would have ended up in the fetch URL). Test 9 — local + endProp==null path. Sets layer.time.type='local' and layer.time.endProp=null to force the TimeControl.reloadLayer branch (TimeControl.js:276-287) that bypasses the resolved-URL placeholder block and falls through to the else clause. Inspects outgoing /geodatasets/* requests via page.on('request', ...) and asserts no literal {starttime}/{endtime}/{time} remain. Catches the Devin Review #6 regression: when this branch hit captureVector with hasResolvedUrl=true, the !hasResolvedUrl gate previously short- circuited the only remaining replacement site. Test 10 — 20-reload stress burst. Extends Test 4's two-reload check to 20 concurrent reloadLayer() calls, capturing 'Cannot make layer' warnings to verify the queue coalesces requests instead of silently dropping them. Also re-asserts layer.url template integrity post- burst. Test 11 — Feature-presence after concurrent reload. Captures L_.layers.layer[key].getLayers().length before and after a 5-reload burst. Asserts the count is still > 0 afterwards — the user-visible 'gaps where dynamically-appearing data doesn't show up' symptom from the original bug report. All four tests skip gracefully when their fixture layer is absent or the dataset returns no rows, to avoid spurious failures across mission configurations. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * test: add 5 edge-case tests probing the blast radius of the URL fix Five additional tests in tests/e2e/time/concurrent-layer-reload.spec.js covering paths adjacent to the URL-mutation fix that were not exercised by tests 1-11. Each was chosen because the production code on that path changed (or now has a different contract) and a regression would not be caught by the original race-condition tests. Test 12 — {customtime.N} placeholder preservation. TimeControl.reloadLayer's customtime replacement loop was migrated from `layer.url = ...` to `resolvedUrl = ...`. Seeds TimeControl.customTimes.times so the loop actually runs, then asserts the {customtime.0} placeholder remains literally on layer.url after reload. Test 13 — mmgisAPI.reloadTimeLayers() returns a Promise. This is the backward-incompatible behavior change documented in docs/pages/APIs/JavaScript/Main/Main.md (previously synchronous). Asserts the returned value is a thenable that resolves to an array, pinning the new contract so it does not silently regress. Test 14 — mmgisAPI.reloadLayers handles unknown layer names. The Promise.allSettled change requires that a failing per-layer reload surfaces as `false` at the same array position as its input name, without throwing. Mixes a valid name + an unknown name + another valid name to verify the order and the boolean mapping. Test 15 — Reloading a time-DISABLED vector layer leaves layer.url unchanged. Discovers a candidate layer from L_.layers.data at runtime (skips if none exist), reloads it, and asserts the URL is byte-equal afterwards. Catches any accidental URL mutation introduced for the non-time code path. Test 16 — mmgisAPI.reloadLayers handles empty array, null, undefined, and string inputs without throwing — verifying the Array.isArray guard at mmgisAPI.js:618. Returns `[]` in all cases. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * fix(makeLayer): wrap dispatch in try/finally so lock + queue drain always run Address Devin Review finding: a thrown exception from any layer-type builder (makeVectorLayer, makeVelocityLayer, etc.) inside makeLayer's switch statement previously left lockRegistry[layerName] set to true and skipped the queue drain entirely, since the release statement and queue-drain block both lived AFTER the awaited dispatch. Effect of the bug: any subsequent refreshLayer call for that layer would queue against a permanently-locked entry that never drains. The new queue mechanism inherits this pre-existing issue and makes the failure mode worse — silent accumulation in _layerReloadQueue instead of a visible 'Cannot make layer' warning. Additional concern: the outer 'new Promise(async (resolve, reject) => {...})' is the async-executor anti-pattern. A throw inside the async executor escapes to the unhandled-rejection handler instead of rejecting the outer Promise — so the caller's 'await makeLayer(...)' would hang indefinitely, compounding the lock-leak symptom. Fix: - Wrap the type-dispatch switch + Filtering.updateGeoJSON/ triggerFilter calls in a try/catch/finally. - catch logs the error and tracks success via 'madeSuccessfully'. - finally runs unconditionally: lockRegistry[layerName] = false, drain L_._layerReloadQueue[layerObj.name] if present, then resolve(madeSuccessfully). This ensures the lock release and queue drain happen regardless of whether the inner builder threw or completed normally. Test (concurrent-layer-reload.spec.js): Added probe-style test '_layersBeingMade lock is released after single and concurrent reloads' that asserts the lock invariant: 1. After mmgisAPI.reloadLayer() resolves + a 100ms drain window, _layersBeingMade[key] is false. 2. After 5 concurrent mmgisAPI.reloadLayer() calls resolve + a 1000ms drain window, _layersBeingMade[key] is false. 3. _layerReloadQueue is empty afterwards (otherwise a future reload would mistakenly trigger an immediate drain instead of doing its own work). This is a positive-invariant test — it catches accidental lock retention even without force-triggering exceptions, which would require monkey-patching webpack-internal module references that aren't exposed on window. Per user direction, NOT addressing Devin Review's separate finding about the reloadTimeLayers sync->async breaking change at this time (no version bump requested). Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * chore: bump version to 5.0.9-20260513 [version bump] * fix(OperationsClock): bump z-index above bottomFloatingBar so clock stays visible Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * fix(plugin-deps): respect override semantics when aggregating dependencies When a plugin tool/component/backend overrides a standard one by reusing the same directory name, only the override's deps should contribute to the aggregated plugin manifests. Previously, gatherDependencies() concatenated standard + plugin entries and fed both to mergeNpm/ mergePython, which could spuriously flag the same package as conflicting between the standard and override versions. Extract winnersByName() (mirroring API/updateTools.js + API/setups.js override behavior) and use it for all three plugin kinds. Add unit tests covering the override case and the spurious-conflict regression. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * fix(Dockerfile): re-install plugin npm deps in runtime stage Plugin deps installed in the builder via `npm install --no-save --no-package-lock` aren't recorded in package.json/package-lock.json, so the runtime stage's `npm ci --only=production` would lose them. Frontend deps are bundled by webpack into ./build so they're fine, but backend plugins that `require()` their declared npm dependencies at runtime would crash with 'Cannot find module'. Copy plugin-package.json from the builder and re-run the same conditional install in the runtime stage so backend plugin deps land in the runtime image's node_modules. `--ignore-scripts` prevents the inner install from re-entering the root postinstall hook. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * feat(OperationsClock): lift to 58px bottom when TimeUI is open Add #operationsClock to BottomElementPositioner's reactive positioning so it shifts to bottom:58px when timeUIActive is true (TimeUI dock visible) and back to bottom:40px when closed. Avoids overlap with the bottom floating bar without adding new state to OperationsClock itself. Mobile path is unchanged — OperationsClock.setupMobilePositioning manages mobile positioning separately. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * fix(OperationsClock): always sit at bottom:58px Revert the dynamic positioning in BottomElementPositioner and just hardcode bottom:58px in OperationsClock.css. Simpler, no cross-cutting state dependency. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * feat: add Lunar South Pole reference mission variant (IAU2000:30120) - Add REFERENCE_MISSION_VARIANTS registry and resolveVariantBlueprintPath helper to missionTemplates.js for dynamic variant resolution - Update configs.js to accept referenceMissionVariant parameter and validate against the registry - Add variant dropdown to NewMissionModal UI when Reference Mission checkbox is enabled - Create blueprint directory with south polar stereographic config (IAU2000:30120, +proj=stere +lat_0=-90, bounds ±1095700/1095600) - Add unit tests for variant registry, blueprint path resolution, and Lunar-SouthPole projection assertions (15 tests) - Add E2E smoke tests for Lunar-SouthPole mission variant Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * feat(api): pass through reloadLayer flags in reloadLayers Forward evenIfOff, evenIfControlled, forceRequery, and skipOrderedBringToFront parameters to TimeControl.reloadLayer() for each layer in the batch. Fully backward-compatible — existing callers that pass only layerNames get undefined for all flags. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * chore: bump version to 5.0.10-20260518 [version bump] * chore: bump version to 5.0.10-20260518 [version bump] * docs(api): add forceRequery & skipOrderedBringToFront to reloadLayer/reloadLayers docs Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Add SPole basemap to lunar ref mission * chore: bump version to 5.0.11-20260518 [version bump] * chore: remove unused blueprints/Missions/Test directory The Test blueprint is no longer needed — the Reference-Mission and Reference-Mission-Lunar-SouthPole blueprints serve as the basis for demo, development, and testing. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * fix: use setupReferenceMission string value as variant key fallback Address Devin Review feedback: when setupReferenceMission is a non-empty string (e.g. 'Lunar-SouthPole'), use it as the variant key if referenceMissionVariant is not provided. Also guard against empty string triggering reference mission creation. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * fix(measure): rename config DEM field from 'dem' to 'url' and handle HTTP URLs - Rename 'field': 'dem' → 'url' and 'name': 'DEM Path' → 'DEM URL' in src/essence/Tools/Measure/config.json (layer-specific DEM objectarray) - Rename same in configure/src/metaconfigs/layer-tile-config.json - Add http:// and https:// prefix handling in makeProfile() so external URLs are not mangled with the mission path prefix The top-level variables.dem field (primary DEM) is unchanged. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * chore: bump version to 5.0.11-20260519 [version bump] * revert: remove http/https URL prefix handling in makeProfile() Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * fix(measure): rename 'dem' to 'url' in Reference Mission layerDems blueprint Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * fix(measure): accept both 'url' and 'dem' fields in layerDems for backward compatibility Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * chore: bump version to 5.0.12-20260519 [version bump] * feat: add SPole_100m basemap layer to Lunar South Pole blueprint config Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * fix: show Save to Base Blueprint button for all reference mission variants Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Update lunar ref mission * style: add text-align right to mission list buttons in configure sidebar Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * fix(ui): SegmentTool background, clipboard API, context menu test selectors - Add solid background to #segmentTool so panel is opaque - Modernize copyToClipboard to use Clipboard API with execCommand fallback - Add stable .ContextMenuMap class and #contextMenuMapCopyCoords id for E2E tests Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * chore: bump version to 5.0.12-20260521 [version bump] * fix(contextmenu): increase z-index to 9999 so menu renders above other layers Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * chore: bump version to 5.0.13-20260521 [version bump] * fix: use Object.hasOwn for variant key validation to prevent prototype chain leakage Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * fix(security): type coercion for query params in geodatasets.js Add string type checks for starttime, endtime, and format query parameters to prevent SQL injection bypass via array coercion (e.g. ?starttime=a&starttime=b). Fixes both /get and /search endpoint locations. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * fix(security): prototype pollution guards across codebase - Block __proto__, constructor, prototype keys in Utils.setIn2 - Use Object.create(null) for dirStore in middleware.js - Filter dangerous column names in datasets model - Guard field assignments in datasets routes - Protect QueryURL property assignments from prototype pollution Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * fix(security): add auth and compute rate limiters - authLimiter: 10 req/15min on /login to prevent brute force - computeLimiter: 30 req/min on Python-spawning utils routes - Limiters passed through setup system to route files Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * fix(security): harden GitHub Actions workflows - Add permissions blocks to all workflow files - Pin all third-party actions to commit SHAs - Replace node -e require(package.json) with jq in bump-version.yml Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * fix(security): sanitize log output to prevent log injection Add sanitizeForLog() to strip ANSI escape sequences and control characters from log messages, callers, and errors in both dev and production modes. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * docs: move Reference Mission docs to blueprints/README.md with variant guide - Move Reference-Mission/README.md to blueprints/README.md - Add 'Adding a New Reference Mission Variant' step-by-step guide - Document all existing variants (Earth default, Lunar South Pole) - Update cross-references in AGENTS.md and .knowledge/code-patterns.md Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * fix(security): DOM XSS prevention with DOMPurify and .text() - Add DOMPurify dependency and Sanitize.js service module - Apply safeHTML() to CursorInfo, Modal, DrawTool_Files, DrawTool_Templater - Replace .html() with .text() for numeric labels in LayersTool - Escape single quotes in Filtering attribute values Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * fix(security): move public static middleware before session Move /public, /README.md, and /examples static routes before session middleware to avoid setting unnecessary cookies on public assets. Authenticated routes (/build, /configure/*) remain after session. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * fix(tests): resolve CI failures in unit tests - DOM XSS tests: remove document API usage (not available in Node context) - Geodatasets: array format correctly defaults to safe string, not rejected - Prototype pollution: use hasOwnProperty.call instead of direct property access Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * fix: address Devin Review findings - Rate limiters: wrap in deferred closure so lookup happens at request time, not module load time (users.js login + 6 utils.js compute routes) - docker-build.yml: restore step-level env: blocks that were incorrectly replaced with permissions: at wrong indentation level Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * feat: add meandering south pole traverse line layer to Lunar South Pole blueprint Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Improve lunar ref mission blueprint config * fix: skip auth rate limit for token re-auth + type coercion on /intersect - authLimiter: skip for token-based re-auth (req.body.useToken) to avoid locking out legitimate users on page refresh - geodatasets.js: apply same typeof string coercion to POST /intersect endpoint that was applied to GET endpoints Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * fix: require MMGISUser cookie to skip auth rate limiter Prevents bypass where attacker sets useToken:true without a valid cookie to skip the rate limiter and brute-force credentials. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * fix: address Devin Review round 3+4 findings - Auth rate limiter: parse cookie and require valid token before skipping rate limit (prevents bypass via crafted falsy cookie) - Modal.jsx: remove safeHTML (too restrictive for developer-authored modal content with forms, ids, inputs) - DrawTool_Files.js: escape file_name/intent/file_…
* feat(ShadeTool): hover line on horizon chart + red time slider synced with playback
- Add vertical dashed line on horizon chart following mouse (matches map azimuth line)
- Add red draggable time slider on both horizon and visibility charts
- Time slider is bidirectionally synced with playback section slider
- Click/drag on either chart scrubs the playback frame
- Horizon chart scrubs by nearest azimuth match, visibility scrubs by time position
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix(ShadeTool): remove ancillary sun/earth trajectories from horizon chart
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix(ShadeTool): reduce visibility timeline panel height to ~85px
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* feat(ShadeTool): replace horizon vertical slider with horizontal time controls bar
- Remove red vertical azimuth-based slider from horizon chart canvas
- Add horizontal time controls bar below chart: step-back, play/pause, step-fwd buttons + range slider + time label
- Slider scrubs by time (frame index), synced bidirectionally with playback
- Play/pause button animates through frames at sweep speed
- Full-width controls filling bottom panel
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix(ShadeTool): make graphs responsive to resize, remove background from title/controls
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* chore: bump version to 5.0.22-20260603 [version bump]
* perf(ShadeTool): throttle composite hover, debounce time changes, fix graph play interval leak
- Throttle _onCompositeHover with requestAnimationFrame (was triggering
Zustand store updates + React re-renders on every mousemove)
- Debounce _onTimeChange by 300ms (was firing shade() for all elements
on every TimeControl tick during animation)
- Fix graph play interval leak: use module-level var cleared on close()
- Add window resize listener + pxIsTools store subscription for
responsive graph redraw (ResizeObserver alone was insufficient)
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix(ShadeTool): pin horizon/crosshair to sweep center, improve atlas performance
- Horizon profile now uses sweep-time observer center (stored in
sweepElData[id].sweepCenter) instead of current map center
- Azimuth hover line originates from sweep center position
- Crosshair pins to sweep center on pan (tracks via map 'move' event)
- Crosshair now has 2px black border + stronger shadow for visibility
- buildSweepAtlas: reduced chunk size from 16 to 4 frames, added
progress reporting ('Building atlas: XX%'), broken assembleAtlas
into chunked processing (yields to event loop between tiles)
- Toast 'Sweep complete' now shows AFTER atlas is built (was showing
before, giving false sense of completion)
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* perf(ShadeTool): eliminate toDataURL() bottleneck from atlas frame rendering
Root cause: buildSweepAtlas was calling renderResultToTileData() for each
frame, which calls c.toDataURL() per tile (PNG encoding, ~10-20ms each).
With 128 frames × N tiles = thousands of PNG encodes blocking main thread.
Fix: new _renderFrameCanvases() method that only produces canvas objects
(drawImage clone) without any toDataURL() calls. Atlas assembly still calls
toDataURL() once per output tile (unavoidable for GL layer) but that's only
N tiles total instead of N×frames.
Also: crosshair now uses outline (doesn't affect element size) so white
fill is clearly visible with black border.
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix(ShadeTool): accurate progress tracking and trajectory line wrapping
Progress bar now reflects the FULL sweep pipeline:
- 0-50%: shade computation (processChunk)
- 50%: 'Computing heatmap...' (cumulativeVisibility)
- 55-90%: 'Building atlas: X%' (frame rendering)
- 90-100%: 'Building atlas: assembling...' (tile toDataURL)
- Toast only fires at true completion
Previously the bar jumped to 100% after shade computation (fast phase)
then stayed there for 20s during atlas building with no visible progress.
Also: setTimeout(0) yield before cumulativeVisibility so the 'Computing
heatmap...' message actually paints before the synchronous computation.
Horizon chart: break trajectory line at ±180° azimuth boundary to prevent
long wrap-around lines spanning the chart when source crosses South.
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* perf(ShadeTool): rewrite atlas build to eliminate intermediate canvases
Previous approach: created 512+ intermediate canvas objects, performed
1024 drawImage operations, then assembled into atlas. Took ~15-20s.
New approach: renders pixel data directly into atlas canvas via putImageData
at the correct frame offset. Eliminates ALL intermediate canvas allocations
and drawImage calls. Benchmarked at ~2.7s for 128 frames x 8 tiles.
Additional optimizations:
- Reuse a single ImageData/buffer per tile (avoids per-frame allocation)
- Pre-compute color values outside the pixel loop (no object alloc per pixel)
- Use bitwise OR for integer division in hot loop
- Reduce processChunk batch size from 16 to 4 for smoother progress updates
- Fix progress text: show 'Computing shade X/128' instead of misleading '100%'
Also fixes horizon chart rendering order: yellow trajectory renders
BEHIND the brown area chart (rgba(90,62,35,0.8)).
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* feat(ShadeTool): horizon chart polish, TimeUI indicator API, progress rAF fix
- Remove azimuth x-axis labels from horizon chart (cleaner look)
- Add small north arrow indicator at top center of horizon chart
- shadeGraphTimeLabel now shows full ISO time (matching vstSweepFrameLabel)
- Add TimeUI.addIndicator(id, groupId, color, time) / removeIndicator(id, groupId) API
- Renders vertical colored lines on the TimeUI timeline
- Re-renders on timeline zoom/pan/redraw
- CSS: .mmgisTimeUIIndicatorLine styled as absolute-positioned colored lines
- ShadeTool adds red indicator on timeline at current playback time
- Updated on every frame change via sweepShowAllFrames
- Removed on destroy via TimeUI.removeIndicator(null, 'shadetool')
- Switch processChunk and buildSweepAtlas yields from setTimeout(fn,0) to
requestAnimationFrame(fn) so the browser paints between iterations,
making the progress bar update visibly instead of jumping 0->100%
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix: north arrow up, y-axis padding, progress bar lifecycle
- North arrow in horizon chart now points upward (triangle + N label)
- Elevation y-axis label moved slightly for better readability
- Fixed progress bar: onComplete was called before buildSweepAtlas
started, causing 'Done' state to flash before atlas build. Now
onComplete is deferred until atlas finishes in playback mode.
- Added pct updates during tile loading (0-5%) and position API (5-15%)
phases so progress bar moves throughout the entire pipeline.
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix(ShadeTool): throttle sweep progress updates, adjust UI spacing
- Throttle sweepProgressPct Zustand updates to flush at most every 50ms
so React can actually repaint between requestAnimationFrame callbacks.
Previously the store was updating ~200x/sec but the ProgressButton
never re-rendered because React batched all the rapid state changes.
- Elevation axis label: decrease translate x (4px) to give more padding
between the label and the y-axis tick marks
- North arrow: increase gap between triangle and N text (triangle moved
6px higher)
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix(ShadeTool): progress bar now updates during sweep
The ProgressButton in ShadeElement reads el.loadingProgress, but
_flushSweepProgress was only writing to the top-level sweepProgressPct
store field (consumed by the unmounted SweepSection component).
Now _flushSweepProgress also updates the active element's
loadingProgress, so the actually-rendered ProgressButton re-renders
with intermediate percentages during the sweep.
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* feat(ShadeTool): IconTextButton, monotonic progress, single-row time controls
- New design-system IconTextButton (base-ui button with icon + text label)
- Graph buttons now use IconTextButton instead of raw <button> elements
- Progress bar is monotonic: percentage never goes backwards during sweep
- Time controls condensed to single row: [start] → [step|min] → [end]
with start/end read-only (set via TimeUI), only step editable
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* style(ShadeTool): match DrawTool header, UI refinements
- Header now uses shared mmgisToolHeader/mmgisToolTitle pattern (40px, matching DrawTool exactly)
- Tool width increased to 300px
- Time row: arrows removed, start/end are plain text (not inputs)
- vstBinaryLegend + vstTime background: var(--color-a-5)
- Graph time controls: play buttons styled like IconButton md (28px, transparent bg, 18px icons)
- shadeGraphTimeLabel font-size: 14px
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* feat(ShadeTool): div-based multi-source visibility timeline + multi-arc horizon chart
- Replace canvas-based visibility timeline with div-based bars
- Show ALL shade elements with sweep results simultaneously
- Each element gets a labeled row with its own color
- Simplify time labels (no seconds/microseconds, omit year if constant)
- Title changed to 'Visibility Timeline'
- Horizon chart shows trajectory arcs from all linked shade maps
(same center, time, step) with each element's own color
- Brighten dark element colors for visibility on dark backgrounds
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix(ShadeTool): fix horizon/visibility errors + sweep progress tracking
- Add missing 'store' variable in _drawHorizonCanvas (ReferenceError)
- Fix visibility timeline accessing elms[0].results instead of elms[0].ed.results
- Track sweeping element ID in _flushSweepProgress so progress updates
target the correct element, not the (changing) activeElmId
- Reset all elements' loading state when starting a new shadeSweepAll
to prevent stale progress from a cancelled sweep
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix(ShadeTool): fix scrub error, horizon fill, and stuck sweep progress
- Fix _scrubFromVisibilityX: access elms[0].ed.results (not elms[0].results)
- Horizon chart: include source trajectory elevations in auto-fit range,
extend terrain fill to canvas bottom so arcs below horizon are covered
- Reset regenerating/loading state on cancelled sweeps so buttons don't
stay stuck at partial progress when a new sweep replaces an in-flight one
- Clean up all regenerating elements in cancelSweep and shadeSweepElement
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* refactor(ShadeTool): per-element sweep run IDs for simultaneous sweeps
- Replace global _sweepRunId with per-element _sweepRunIds map so
multiple shade maps can sweep simultaneously without cancelling
each other
- _flushSweepProgress now takes elmId as first arg to target the
correct element's loadingProgress
- _highWaterPcts is per-element for monotonic progress tracking
- shadeSweep takes explicit activeElmId parameter instead of reading
store.activeElmId (which changes during concurrent sweeps)
- shadeSweepAll uses separate _sweepAllRunId for its serialization loop
- cancelSweep increments all per-element run IDs to cancel everything
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* feat(ShadeTool): combine horizon + visibility into single panel
- Single 'Charts' toggle button replaces separate Horizon Profile and
Visibility Timeline buttons
- Both charts render in one combined bottom panel (horizon on top,
visibility below, shared time controls at bottom)
- Horizon chart min elevation fixed to exactly 5° below the minimum
terrain horizon point (saves vertical space)
- Trajectory elevations only affect the top bound, not the bottom
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix(ShadeTool): reconnect time controls to combined graph panel
- _scrubToFrame now redraws both horizon canvas and visibility timeline
after updating the store (was only setting store + callback)
- updatePlaybackFrame no longer checks _activeView type — always
redraws both charts since they're combined in one panel
- Fixes play/step/slider not updating charts after panel merge
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* feat(ShadeTool): source section closed by default, +New cycles sources
- Source collapsible section starts closed (was open)
- +New button cycles through non-custom source entities based on the
last element's source (e.g. Sun → Moon → Sun → Moon...)
- Custom source is excluded from the cycle
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix(ShadeTool): visibility timeline uses actual visibilityPct, not horizon re-computation
The visibility timeline was re-computing visibility by comparing source
elevation against the interpolated horizon profile. This disagreed with
the actual shade computation (visibilityPct) because the horizon profile
is a simplified 1D ray-cast while the shade grid is a full terrain shadow
computation.
Now uses visibilityPct > 0 (the actual shade API result) to determine
visible vs occluded segments.
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* feat(ShadeTool): add visibility timeline legend + use center-cell visibility
- Add 'Visibility Timeline' header bar with horizontal legend showing
Visible/Occluded color swatches at the top of the visibility panel
- Store centerVisible (observer grid center cell) in sweep results
instead of using grid-wide visibilityPct > 0 which was too coarse
- Falls back to visibilityPct for backward compatibility with older data
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* feat(ShadeTool): move legend to panel top, invert vis chart colors
- Legend bar at top-left of entire bottom panel (Visible/Terrain/Shaded)
applies to both horizon chart and visibility timeline
- Visibility timeline: shaded segments use element color, visible = white
- Removed vis-specific header/legend (now redundant)
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix(ShadeTool): revert vis colors, remove legend, add occluded label, fix slider alignment
- Reverted vis chart to original colors: element color = visible, gray = occluded
- Removed the panel-level graph legend
- Added 'occluded' suffix text to vis label (lighter weight/opacity)
- Fixed time slider at start/end of vis chart: use playIndex/(frameCount-1)
so slider reaches exact bar edges at first and last frames
- Widened vis label column to accommodate 'occluded' suffix
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix(ShadeTool): vis label text to 'Occultations', full opacity, 12px font
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix(ShadeTool): don't regen static shade when time slider changes during playback
Skip composite/playback elements in _onTimeChange — they should only
scrub through existing sweep frames, not trigger a new shade computation.
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix(ShadeTool): remove horizon canvas right margin, shift elev label 15px right
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix(ShadeTool): invert vis chart colors — colored = occluded, gray = visible
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix(ShadeTool): vis chart uses horizon profile for terrain-aware visibility
Uses _interpolateHorizon(profile, azimuth) to compare source elevation
against the terrain horizon mask — the same data drawn in the horizon
chart. Source is visible only when elevation > terrain at that azimuth.
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* feat(ShadeTool): gradient transitions at vis/occ boundaries in occultation chart
At each visible↔occluded transition, segments now use a CSS
linear-gradient that fades over ~20% of the segment width instead
of a hard color edge.
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix(ShadeTool): vis chart correct on first open + fix double gradient
- Redraw visibility timeline after horizon profile fetch completes
(was rendering with null profile on first open → all occluded)
- Gradient only on trailing edge of each segment (not both sides)
to eliminate the double-gradient artifact at boundaries
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix(ShadeTool): only show results section after sweep completes
Added !el?.regenerating guard to the auto-open effect so the
results/run section stays hidden during the sweep and only opens
once regenerating is false (sweep fully complete).
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix(ShadeTool): center-align vis timeline tick marks with their text labels
Use transform: translateX(-50%) on the tick container and
align-items: center so both the tick line and text are centered
at the same position.
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix(ShadeTool): hide results section during sweep, only show when complete
- Initialize resultsOpen to false for non-static modes
- Add !el.regenerating guard to the render condition so even if
resultsOpen is true, the content is hidden while regenerating
- Auto-open effect still fires when regenerating turns false
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix(ShadeTool): make vis timeline time ticks responsive to label width
Instead of hardcoded labelCol/margin-left values, measure the actual
.shadeVisBar element's left offset at render time. The time labels
container margin and scrub handler now both derive their position
from the real bar bounds, so they stay aligned regardless of label
text length.
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* feat(ShadeTool): add fast-forward button to graph time controls
New fast-forward button (mdi-fast-forward) between play/pause and
step-forward. Toggles 4x playback speed (interval/4, min 50ms).
Active state highlighted with shadeGraphPlayBtnActive class.
Clicking play/pause while fast is active resets to normal speed.
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix(ShadeTool): layer z-ordering after sweeps + vis chart time label timezone
- Re-apply sweepCardOrder z-indices after sweepShowAllFrames,
sweepShowComposite, and showSweepLayers so the first element
stays on top instead of the last-swept one
- Fix _formatSmartTimeLabel using local timezone for month but UTC
for day/hour/min — add timeZone:'UTC' to toLocaleString so
2024-01-01T00:00:00Z shows 'Jan 1' not 'Dec 1'
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix(ShadeTool): charts survive window resize without going fully occluded
Root cause: window resize fires Leaflet moveend → _onPanEnd →
invalidateHorizonCache() nulls _horizonCache. The resize redraw
timeout then finds no profile, so the vis timeline defaults all
frames to occluded and the horizon chart is skipped.
Fix: _scheduleRedraw now calls fetchAndDrawHorizon() which re-uses
the cache when the position hasn't changed or re-fetches when it
has. Also added drawVisibilityTimeline in the cache-hit path of
fetchAndDrawHorizon so both charts always update together.
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* feat(ShadeTool): remove link button, center playback controls, filter charts by center+time
- Removed playbackLinked toggle and all related state (localPlayIndex,
handleToggleLinked, vstLink styles). All elements now share the
global sweepPlayIndex.
- Centered playback controls in the sweep row.
- Charts (horizon + visibility) now only include shade maps whose
sweep center, start time, step, and frame count match the element
whose Charts button was clicked.
- Added info banner at top-left of bottom panel when shade maps are
excluded (shows count and reason).
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* feat(ShadeTool): colored azimuth lines on map while graphs open; fix stale slider on re-sweep
- Added persistent colored dashed azimuth lines on the map for each
shade element while the charts panel is open. Each line uses the
element's color and updates on every frame change (scrub, play,
step). Lines are removed when graphs close or ShadeTool is destroyed.
- Fixed the time slider retaining the old frame count after a new sweep
with fewer steps: updatePlaybackFrame now re-syncs slider.max from
the current sweep results on every call.
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix(ShadeTool): horizon profile skips near-observer DEM pixels and accounts for curvature
- HorizonProfile.py now accepts minSkipRadius (meters) and
planetRadius (meters) parameters. Rays skip DEM pixels within
minSkipRadius of the observer to avoid blocky near-field artifacts.
When planetRadius > 0, applies curvature drop (d²/2R) so distant
terrain dips below the flat-earth projection.
- Frontend sends minSkipRadius=50m and planetRadius from
F_.radiusOfPlanetMajor when vars.curvature config is enabled
(defaults to true).
- Backend route passes the two new params through to the Python script.
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix(ShadeTool): trim seconds from vstTimeReadonly display
Display shows e.g. '2024-01-01T00:00Z' instead of '2024-01-01T00:00:00Z'.
Full timestamp preserved in title tooltip. Stored value unchanged.
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix(ShadeTool): Charts button height 28px, font-size 12px
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix(ShadeTool): vstTimeReadonly styling, hide empty playback section on mode switch
- vstTimeReadonly: font-size 11px, opacity 0.8, first child text-align right
- Close results section on mode change so switching composite→playback
doesn't show an empty run section
- Auto-open now checks mode-specific data (grids for playback, atlas
for composite) instead of just results existence
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix(ShadeTool): prevent empty playback run section after composite→playback switch
Remove shadeMode from auto-open effect deps so that merely switching
modes doesn't re-open the results section with stale data from the
previous mode. The effect now only fires when actual sweep data changes
(results, grids, atlas, regenerating state), ensuring the section
stays closed after a mode switch until a new sweep completes.
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix(ShadeTool): change default icon to mdi-brightness-4
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix(ShadeTool): Charts button color var(--color-a7)
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix(ShadeTool): theme-aware canvases + composite auto-open results
- Az/El indicators, sky dome, horizon chart: fixed dark (#1a1e22)
backgrounds so they remain legible in light theme
- Vis time tick lines/text: use var(--color-a4) instead of hardcoded
white rgba
- Composite sweep auto-open: check el.lastResultGrid (set by composite
path) instead of ed.atlas (only set by playback sweep)
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix(TimeUI): mmgisTimeUITimelineExtent z-index, pointer-events, opacity
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix(ShadeTool): legend hover indicator and colorstop labels always use light text
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix(ShadeTool): lighten az/el canvas backgrounds, theme-aware sky dome labels
- Static az/el: dark fill (#2a3038) + light stroke/axis lines (was white 0.1 fill + black stroke)
- Playback az/el: lightened from #1a1e22 to #2a3038
- Sky dome N/S/E/W labels: read --color-f from CSS so they're dark in light mode
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix(ShadeTool): horizon profile fully theme-responsive
- Background reads --color-a (matches page bg in both themes)
- Grid lines use black in light mode, white in dark mode
- Tick labels, axis title use --color-a4 (muted text)
- North arrow/label uses --color-f (text color)
- Terrain fill lighter/more transparent in light mode
- 0° line adapts to theme
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix(ShadeTool): vis chart visible segments near-white in light mode
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix(ShadeTool): vis chart tick marks now span full time range
Position ticks using i/(N-1) instead of i/N so the last tick
lands at 100%. Always include the final time label. Step size
calculated from (N-1)/(maxLabels-1) to evenly distribute ticks
across the full range.
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix(ShadeTool): vis chart ticks evenly spaced 0%-100%, no drift
Ticks are now placed at evenly-spaced visual positions and each
maps back to the nearest frame index for its label. This avoids
the gradual drift caused by ticks and bar segments using different
coordinate systems (i/(N-1) vs i/N).
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix(ShadeTool): all canvas circle backgrounds to #2c2f30
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix(ShadeTool): canvas circle backgrounds to black
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix(ShadeTool): canvas circle backgrounds to #0f1010
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix(security): add path validation + numeric checks to /gethorizonprofile
- Validate required fields (path, lat, lng)
- Full URL decoding loop to catch encoded traversal
- Restrict paths to /Missions/ (cross-mission ../ allowed)
- Resolved path must stay within Missions directory
- All numeric params validated as finite numbers
- numAzimuths capped at 3600, maxRadius at 100000 (DoS prevention)
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* docs+tests(ShadeTool): E2E tests for horizon profile API, update Shade docs
- E2E tests: input validation, path traversal protection, numeric checks
- Docs: shade modes (static/composite/playback), charts panel (horizon
profile + occultation timeline), sky dome, azimuth lines, time controls
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* refactor(utils): extract shared validateMissionsPath helper
- New validateMissionsPath() handles URL decode loop + /Missions check +
path.resolve guard in one place
- queryTilesetTimesDir and /gethorizonprofile both use the shared helper
- Fix E2E test: replace Infinity test (unrepresentable in JSON) with NaN string
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix(tests): horizon profile E2E tests skip gracefully in AUTH=local mode
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* style(ShadeTool): canvas circle backgrounds to #575d60
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* style(ShadeTool): canvas circle backgrounds to #3a3e40
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix(utils): validateMissionsPath accepts paths without leading slash
Frontend sends 'Missions/...' (no leading slash) via L_.missionPath.
Normalise by prepending '/' before the prefix check.
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix(ShadeTool): pass resolved path to HorizonProfile.py + add tippy tooltips
- gethorizonprofile now passes pathResult.resolved (full filesystem path)
instead of pathResult.decoded (/Missions/...) to the Python script
- Added tippy tooltips: 'Start Time', 'End Time' on vstTimeReadonly spans,
and step size description on the InputWithUnit
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* style(ShadeTool): blue-themed Tooltip for time fields + close button shifted left 6px
- Replaced raw tippy refs with design-system <Tooltip> (blue theme)
- Close button in header shifted left 6px via margin-right: -6px
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix(security): append trailing slash to startsWith check in validateMissionsPath
Prevents /MissionsBackup/... from passing validation. Matches the
pattern already used in scripts/middleware.js (isPathInsideRoot).
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix(HorizonProfile): bounds-check observer pixel before array access
Return flat profile when observer falls outside the DEM read region
instead of crashing with IndexError.
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* feat(ShadeTool): export improvements, color ramps, no-data fix, thicker azimuth lines
1. Export TXT: add bounding box in meters + projection info to grid export
2. Export CSV: row-per-pixel format (entity,time_range/time,lat,lng,percent_visible)
instead of one row per frame — uses heatmap data for both composite and playback
3. Color ramps: add two element-color-based ramps (Fade: transparent→color→transparent,
Edges: color→transparent→color) that use the shademap's user-set color
4. No-data: hide faint red coloring (val===9) — render as fully transparent instead
5. Azimuth lines: increase stroke-width from 2 to 3.5 for better visibility on map
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* chore: bump version to 5.0.23-20260604 [version bump]
* fix(ShadeTool): export TXT bounding box uses CRS project() for projected meters
Use window.mmgisglobal.customCRS.project() to convert SW/NE corners
to proper projected coordinates instead of simple degree-to-meter
conversion. Also includes proj4 string and cell size in projected units.
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix(ShadeTool): color ramps use proper RGBA transparency, 3 stops each
The new ramps (Fade: transparent→color→transparent, Edges:
color→transparent→color) now use 3 RGBA color stops with the 4th
component controlling alpha. evalColor interpolates alpha when present.
Renderer and legend both read the interpolated alpha for these ramps.
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix(ShadeTool): include observer height in horizon cache key
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix(ColorRampPicker): rewrite dropdown to use continuous gradient with RGBA alpha support
- Replace discrete block rendering with continuous linear-gradient matching
the legend's interpolation approach
- Shadow ramp dropdown now matches legend direction (opaque→transparent)
- New alpha-based ramps (_tct, _ctc) properly show transparency over
checkerboard background in both the trigger and popup
- All ramps with hasAlpha flag get a checkerboard underlay
- Standard colormaps (viridis, plasma, etc.) render as smooth gradients
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix(ShadeTool): split composite CSV time range fields
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix(ShadeTool): refresh color-based sweeps after color changes
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* refactor(Sightline): rename ShadeTool → SightlineTool, invert rendering semantics
- Rename entire Shade tool directory to Sightline (git mv)
- Rename all files: ShadeTool.js → SightlineTool.js, etc.
- Rename store: useShadeStore → useSightlineStore
- Rename CSS classes: .shade* → .sightline*, #shadeTool → #sightlineTool
- Rename internal functions: shade() → sightline(), shadeSweep → sightlineSweep
- Rename default color ramp: 'shadow' → 'sightline'
- Update config.json: name 'Shade' → 'Sightline', paths updated
- Update blueprint reference mission config
- Update all test files and fixtures
- Update docs (Shade.md → Sightline.md, SPICE docs)
- Update imports in tools.js, AnalysisTool comment, Globe_ comments
Rendering inversion (done in prior commit, preserved here):
- store.js: invert default 1 → 0 (visible = prominent color)
- Composite heatmap: alpha = alphaFrac (not 1-alphaFrac)
- Vis chart: colored bars = visible (not occluded)
- Color ramp: alpha = t (not 1-t)
- Legend: isSightlineRamp uses alphaFrac directly
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* chore: bump version to 5.0.24-20260606 [version bump]
* fix(SightlineTool): complete rename cleanup, brighter colors, fix Devin Review findings
- Fix tool title 'Shade' → 'Sightline' in panel header
- Rename help file ShadeTool.md → SightlineTool.md, update all content
- Fix default element name 'Shade N' → 'Sightline N'
- Brighter default MULTI_SOURCE_COLORS (amber, coral, blue, green, pink, etc.)
- Fix SweepCard stale 'shadow' colorRamp fallback → 'sightline' (Devin Review)
- Fix ShaderTool_Algorithm import alias → SightlineTool_Algorithm
- Rename internal functions: computeShade → computeSightline,
showShademapLayers → showSightlinemapLayers,
clearAllShadeLayers → clearAllSightlineLayers,
reorderShadeLayers → reorderSightlineLayers
- Fix SightlinePanel calling stale reorderShadeLayers
- Update export labels: 'Shade Map' → 'Sightline Map', 'Shade Grid' → 'Sightline Grid'
- Update progress/toast strings from 'Shade' to 'Sightline'
- Update SPICE docs reference
- Update help file section headers and descriptions
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix(SightlineTool): default sightline color ramp uses element color instead of black
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix(SightlineTool): remove vstBinaryLegend, default opacity 50%
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix(SightlineTool): crosshair redesign — unfilled circle with N/S/E/W tick lines
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* chore: bump version to 5.0.25-20260608 [version bump]
* fix(SightlineTool): full-size pixelated PNG export, mode-aware CSV/TXT, rename to Results
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix(SightlineTool): entity lowercase, ISO UTC times in exports, static TXT header fix, smaller crosshair
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* feat(SightlineTool): playback export as animated GIF with basemap overlay
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix(SightlineTool): header close button margin-right 6px
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix(SightlineTool): GIF export - capture full map container, guard against 0-size canvas
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix(SightlineTool): GIF export - hide UI controls, add timestamp/progress, lower resolution
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix(SightlineTool): GIF export - 720px res, UI progress indicator, remove in-frame progress bar
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix(SightlineTool): GIF progress reaches 100% only after encoding completes
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix(SightlineTool): rewrite CSV exports per mode, remove TXT for playback
- Static: entity,time,lat,lng,visible (single time, binary)
- Playback: entity,time,lat,lng,visible (per-pixel per-frame, each frame's timestamp)
- Composite: entity,start_time,end_time,lat,lng,percent_visible (time range, heatmap)
- Remove TXT grid export option for playback mode
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix(SightlineTool): reset export dropdown to first item on mode change
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* feat(blueprints): add SightlineTool to Lunar South Pole + new Mars reference mission
- Add time section and SightlineTool config to Lunar South Pole mission
- Create new Reference-Mission-Mars with LayersTool + SightlineTool (Sol observers)
- Reference Mission already has SightlineTool on development branch
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* chore: bump version to 5.0.26-20260608 [version bump]
* feat: register Mars variant in reference mission registries
Add Mars entry to REFERENCE_MISSION_VARIANTS in both:
- API/Backend/Utils/missionTemplates.js (backend)
- configure/src/.../NewMissionModal.js (frontend dropdown)
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* Add lunar ref mission dem data
* Add lunar ref mission dem data 2
* fix: sample bbox perimeter for tile bounds in polar projections
The bounding box clamping in SightlineTool and ViewshedTool
updateDesiredTiles() projected only two corner points of the bbox to
compute tile bounds. In polar stereographic projections (e.g. Lunar
South Pole IAU2000:30120), opposite corners of a lat/lon bbox can map
to the same pixel-space point, producing a degenerate or inverted tile
range — resulting in zero tiles fetched and no sightmap/viewshed
overlay rendered.
Fix: sample 9 evenly-spaced points along each of the four bbox edges
(36 points total) and take the pixel-space envelope. This correctly
handles any map projection.
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix: projection-aware azimuth lines and source positioning for polar CRS
- locateSource: skip wrapping for custom (non-wrapping) projections so
distant sources like Sun produce nearly-parallel rays instead of being
folded into the grid as a nearby point source
- AZ hover line: rotate by local north offset derived from the projection
so the bearing line points in the correct geographic direction
- Source azimuth lines: same north-offset correction
- Add _localNorthAngle() helper that computes screen-space north direction
at any lat/lng by projecting a small latitude offset
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix(SightlineTool): skip observer-centric curvature for distant sources
When the ray source (e.g. Sun) is far outside the DEM grid, the
observer-centric curvature correction in curveData() creates a
circular visibility artifact — the terrain bowl centered on the
observer falsely hides all cells beyond ~350 km.
Skip the curvature correction when dataSource is more than one
grid-width outside the data bounds. The shadow-plane propagation
already handles parallel-ray geometry correctly on the flat grid.
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* feat(sightline): add backend sightmap endpoint with per-cell ray-march
- New Python endpoint /api/sightmap computes solar illumination grids
server-side using SPICE for Sun position + DEM ray-marching
- Frontend calls backend instead of fetching tiles + JS shadow-plane algo
- USE_BACKEND_SIGHTMAP const switch to revert to old JS algorithm
- Returns both geographic and projected bounds for polar stereo CRS
- Sightmap overlay uses image-rendering: pixelated for crisp grid cells
- Auto-detects PROJ_DATA path in conda/mamba environments
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* chore: bump version to 5.0.27-20260609 [version bump]
* refactor: consolidate sightmap to backend-only, remove old JS algorithm
- Remove USE_BACKEND_SIGHTMAP flag and all old JS shadow-plane code
- Static path: single sightmap API call (no getbands/ll2aerll)
- Sweep path: batch sightmap API call with times array
- Remove tile-based rendering (makeDataLayer, renderResultToTileData,
atlas shader, _renderFrameCanvases, SightlineTool_Manager import)
- Rewrite renderHeatmapToMap for imageOverlay (no tile layer)
- Rewrite buildSweepAtlas to pre-render frames as dataURL images
- Rewrite sweepShowFrame to swap imageOverlay URL
- Update export functions for backend grid data structure
- Backend sightmap.py: batch mode (compute_sightmap_batch),
extracted _ray_march_grid and _compute_bounds helpers
- Express route: dynamic timeout for batch (N*30s, max 30min)
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* perf: vectorize ray-march with NumPy + coarse Sun az/el interpolation
Replace pure-Python nested loops in _ray_march_grid() with numpy array
operations. All output cells are now processed simultaneously at each
march step via broadcasting and fancy indexing.
Optimizations:
1. Vectorized ray march (#1): ~60x speedup on 259×259 grid (47s → 0.8s)
2. Coarse Sun az/el subgrid (#4): compute SPICE-equivalent az/el on a
10×10 subgrid and bilinearly interpolate to all output cells, reducing
per-cell coordinate transforms from 67K to ~400.
Helper functions added:
- _vectorized_is_nodata: array-based nodata detection
- _pixels_to_geo_batch: batch pixel→geographic via GDAL CoordinateTransformation
- _sun_azel_batch: pure-numpy geodetic Sun az/el (replaces spiceypy.georec per cell)
- _bilinear_interp_2d: pure-numpy 2D bilinear interpolation (no scipy needed)
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix: store raeRaw/raeAllResults so az/el indicator canvases redraw after mount
The SightlineResults.jsx useEffect triggers on el.raeRaw to redraw
indicator canvases. The static callback was calling updateRAEIndicators
directly (before React mounted the canvases) and only setting raeResults
but not raeRaw/raeAllResults, so the useEffect never fired.
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix: playback skydome/az-el blocked by stale atlas guard + pixelated CSS selector
- SightlineElement.jsx: replace ed?.atlas checks with ed?.frameImages
(atlas was the old WebGL texture atlas; refactored code uses frameImages)
- SightlineTool.css: add img.sightmap-pixelated selector (Leaflet applies
className directly to the <img>, not a wrapper div)
- python-environment.yml: add numba>=0.60.0 for JIT optimization
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* perf: Numba JIT + adaptive march + early cutoff + multiprocessing
sightmap.py performance optimizations on the vectorized ray-march:
1. Numba JIT (@numba.njit): compile inner march loop to native code
- 259x259 grid: 0.67s cold / 0.12s warm (was 0.79s numpy-only)
2. Adaptive march step: 4x step when margin > 5°, 2x when > 2°
- Reduces iterations for clearly-illuminated cells
3. Early cutoff: max shadow distance = MAX_TERRAIN_H / tan(el)
- Skips pointless marching when Sun is high
4. Multiprocessing for batch: Pool(ncpu) across timestamps
- 12 frames in 0.62s (52ms/frame) vs sequential
Overall: 47.1s → 0.12s per grid = ~380x speedup (warm cache)
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix: use Pool initializer for multiprocessing (Windows spawn compat)
On Windows/macOS, multiprocessing uses 'spawn' instead of 'fork',
so module-level globals aren't inherited by worker processes.
Fix: pass shared dict via Pool(initializer=_init_pool_worker, initargs=...)
so each worker sets _mp_shared in its own module namespace.
Tested: 72 timestamps in 1.28s (18ms/frame) on 100x100 grid.
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix: five bugs — az offset, playback opacity, progress indicator, mode switch, hover crash
1. sightmap.py: _compute_sun_grid used dem_rows instead of dem_cols for
column pixel clamping → coarse subgrid positions were wrong → az/el
off by ~30-45° on non-square DEMs
2. Playback/sweep overlay now inherits el.opacity as initial value
instead of hardcoded 1.0. Default sweep opacity changed to null;
applySweepOpacity falls back to el.opacity when ed.opacity unset
3. ProgressButton: added indeterminate mode (sliding bar animation)
that activates automatically when loading=true and progress<=0.
Static sightmap generation shows indeterminate; sweep shows %
4. switchElementMode: switching back to static now re-renders the
cached lastData/lastResultGrid or marks changed=true for auto-regen
5. _onCompositeHover: guard against missing topLeftTile (backend
sightmap response doesn't include tile-based fields)
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix: correct sightmap ray direction for polar stereographic projections
_compute_directions had two bugs in the projected CRS branch:
1. Missing grid convergence rotation: geographic azimuth was used
directly in pixel space without rotating by the convergence angle
(longitude for polar stereographic). This caused ~30-45° offset
at the observer's longitude.
2. Inverted dy sign: cos(az) was used for dy when gt[5]<0, but
increasing pixel-y = decreasing northing, so the formula must
negate cos(az) — matching the geographic branch convention.
Also fix _showAzimuthLine/_updateSourceAzimuthLines: the fallback
path (no sweepCenter or indicatorLastDragPoint) now computes
centerLatLng from the map container center so _localNorthAngle
always receives a valid position for the convergence rotation.
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* test: add extensive Playwright tests for sightmap API endpoint
Covers:
- Input validation (missing fields, non-finite numerics, NaN, Infinity)
- Path traversal protection on DEM path
- Single-timestamp sightmap computation (structure, grid values, az/el plausibility)
- Batch multi-timestamp computation (results array, az variation)
- Custom Az/El source (el=0 all shadow, el=90 all visible)
- Error handling (invalid SPICE target, nonexistent DEM)
- Consistency (determinism, different times produce different grids)
- Observer height offset comparison
- maxOutputDim clamping
- Projected bounds for polar stereographic CRS
All DEM-dependent tests gracefully skip when Lunar South Pole
mission is not available. AUTH=local mode returns HTML instead
of JSON — tests detect and skip.
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix(sightline): use observer position for azimuth indicator north offset
The azimuth indicator line was computing _localNorthAngle from the map
center (fallback when sweepCenter is unset), which drifts as the user
pans and may be at the south pole where longitude is ~0. The sightmap
shadows are computed from the actual observer position with correct
per-cell convergence, creating a mismatch.
Fix: store the observer lat/lng as sweepCenter in sweepElData when the
static sightmap completes (same as sweep mode already does). This
ensures _showAzimuthLine, _updateSourceAzimuthLines, the crosshair
placement, and the horizon profile all use the correct observer position
for the north angle calculation.
Also add indicatorLastDragPoint fallback to _updateSourceAzimuthLines
(was missing, unlike _showAzimuthLine which already had it).
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix(sightmap): correct azimuth CW/CCW convention in _sun_azel_batch
The east vector was computed as normal × north (= Up × N = -East = West),
causing atan2(dot(sun, west), dot(sun, north)) to return counter-clockwise
azimuths. This mirrored the result: geographic 240° CW reported as 120°.
Fix: use north × normal (= N × Up = East) per right-hand rule, matching
SPICE's azccw=False (clockwise from north) convention.
Verified: batch az now matches SPICE exactly (226.2493° vs 226.2493°).
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix(sightline): use fixed-width fade gradient for visibility segments
The gradient on segment transitions was 30% of the segment width,
making wide segments have long fades and narrow segments short fades.
Now uses a fixed 2% of the total bar width for all transitions,
producing uniform fade-in and fade-out lengths regardless of segment size.
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* feat(sightline): wire resolution setting to maxOutputDim + gifshot fixes
Resolution dropdown (Low/Med/High/Ultra) now controls the sightmap
grid size sent to the backend:
Static: 100 / 200 / 400 / 800 px
Sweep: 50 / 100 / 200 / 400 px
Also:
- Add willReadFrequently to GIF export canvas contexts (suppresses
Chrome warning about slow getImageData readback)
- Add gifshot progressCallback to update export progress during the
GIF encoding phase (90→100%) instead of jumping at the end
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* refactor(sightline): remove dead SightlineTool_Manager and Layer specific DEMs config
SightlineTool_Manager.js was part of the old JS tile-fetching algorithm
(now replaced by the backend sightmap.py endpoint). Nothing imports it.
Remove:
- SightlineTool_Manager.js
- 'Layer specific DEMs' config section (variables.data with demtileurl,
minZoom, maxNativeZoom, boundingBox) from config.json and all blueprint
mission configs
- vars.data validation in initialize() — replaced with vars.dem check
The Viewshed tool's separate Manager and its layer-level demtileurl
references are unaffected.
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* perf(sightmap): downsample large DEMs at read time via GDAL decimation
High-res DEMs (e.g. 100m vs 4000m) were read fully into memory even when
the output grid was small, causing:
1. Slow computation (full array I/O + march through many more pixels)
2. Windows pickle truncation on multiprocessing (huge arrays exceed
pickle buffer limits when serialized to spawn'd worker processes)
Fix: open_dem() now accepts max_working_dim and uses GDAL's ReadAsArray
with buf_xsize/buf_ysize for server-side bilinear decimation. The
geotransform is adjusted to match the resampled pixel grid. Working dim
is set to max(max_output_dim * 4, 1000) — 4x oversampling for terrain
detail in the ray march, capped at native resolution.
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* perf(sightmap): GDAL overview bands, DEM caching, Numba warmup, reduce working_dim
Three optimizations to cut sightmap generation time on large DEMs:
1. GDAL overview bands: If the DEM has pre-computed overviews (COGs),
read from the overview band directly instead of decimating the full
raster in memory. Falls back to ReadAsArray(buf_xsize/ysize) for
DEMs without overviews.
2. DEM caching: Module-level cache keyed by (path, working_dim) avoids
re-reading the same DEM within a batch run. LRU eviction at 4 entries.
3. Numba JIT warmup: Trigger compilation at module import with a tiny
2x2 dummy array so the ~5-10s first-compilation cost is paid during
startup, not during the actual sightmap computation.
4. Reduce working_dim from 4x to 2x output grid (min 500px instead of
1000px). Halves the march distance and array sizes while maintaining
sufficient terrain detail for shadow boundary accuracy.
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* debug: add timing instrumentation to sightmap.py
Temporary timing logs to stderr at each phase:
- imports, numba_warmup, load_kernels, spice_azel, unload_kernels
- open_dem (with working_dim and actual array size)
- _precompute_grid, _compute_sun_grid, _compute_directions
- _numba_march, total
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* debug: pipe sightmap stderr to Node console for timing visibility
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* debug: include timing data in sightmap JSON response body
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* perf(sightmap): disk-based .npy cache for decimated DEMs
The timing data showed open_dem takes 12.8s (GDAL reading/decimating a
large DEM) while the actual Numba march takes only 0.065s. Since each
sightmap call spawns a new Python process, the in-memory cache is lost.
Fix: after the first GDAL decimate, save the resulting numpy array and
geotransform metadata to .npy/.json files next to the source DEM.
Subsequent process invocations load the pre-decimated array via np.load
(~0.01s). Cache is invalidated if the source DEM file is newer.
Expected improvement: first run ~14s (unchanged), second+ runs ~2s.
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* perf(sightmap): require COG format for large DEMs, remove npy cache
Replace the disk-based .npy cache with a simpler approach: require the
DEM to be a Cloud Optimized GeoTIFF (COG) when decimation is needed.
COGs have internal tiled layout + overview pyramids so GDAL can read at
any target resolution by seeking to the right bytes — no full-file scan.
If the DEM is not a COG and is large enough to need decimation, throw a
clear error with the gdal_translate command to convert it.
Small DEMs that fit within max_working_dim are read directly regardless
of format (no performance issue at small sizes).
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix: pass through Python error messages to frontend on sightmap failure
When sightmap.py exits with code!=0, the Node handler was returning a
generic 'sightmap computation failed' message. Now it tries to parse
the structured JSON error from stdout first, so the actual error
(e.g. 'DEM is not a COG') reaches the frontend.
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* chore: remove timing debug instrumentation from sightmap
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix: sanitize COG error message — no paths or tracebacks in response
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix: remove traceback from JSON error response, log to stderr only
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix: show actual sightmap error message in toast
- calls.api error callback now parses JSON response body from jqXHR
- SightlineTool error handlers display server error message (e.g. COG)
- No traceback or paths in error responses (security)
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix: remove duplicate 'sightmap error' prefix from Python error messages
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* perf(sightmap): temporarily disable multiprocessing for batch mode
Run all timestamps sequentially in the main process so the DEM stays
in memory and avoids pickle serialization overhead per worker.
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* refactor(sightmap): remove multiprocessing and Resolution UI option
- Remove all multiprocessing infrastructure (pool, workers, shared state)
- Batch timestamps run sequentially in the main process
- Remove Resolution dropdown from UI, default to ultra (800px static, 400px sweep)
- Always auto-regenerate on map move (no resolution-gated cutoff)
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* perf: temporarily disable Numba JIT for benchmarking comparison
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* 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>
* 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 …
* chore: bump version to 5.0.22-20260603 [version bump]
* perf(ShadeTool): throttle composite hover, debounce time changes, fix graph play interval leak
- Throttle _onCompositeHover with requestAnimationFrame (was triggering
Zustand store updates + React re-renders on every mousemove)
- Debounce _onTimeChange by 300ms (was firing shade() for all elements
on every TimeControl tick during animation)
- Fix graph play interval leak: use module-level var cleared on close()
- Add window resize listener + pxIsTools store subscription for
responsive graph redraw (ResizeObserver alone was insufficient)
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix(legal): audit attributions.js and add NOTICE file
- Create root NOTICE file per Apache 2.0 conventions
- Fix misattributed leaflet-corridor entry: file is actually
leaflet-distance-markers by Doroszlai Attila & Phil Whitehurst
- Fix incorrect license types:
- DataTables: Custom -> MIT (verified via bundled license.txt)
- Leaflet: Custom -> BSD-2-Clause (verified via file header)
- shapefile: Custom -> BSD-2-Clause (upstream mbostock/shapefile)
- Node.js: Custom -> MIT (verified upstream)
- Marked: Custom -> MIT (verified upstream)
- Update stale version numbers:
- D3: add v7 entry (package.json has ^7.9.0)
- jQuery: 1.11.3 -> 3.5.1 (package.json)
- Turf: 5.1.6 -> 6.5.0 (package.json)
- NippleJS: 0.7.1 -> 0.8.5 (package.json)
- Leaflet: 1.4.0 -> 1.5.1 (vendored file name)
- CodeMirror: 5.19.0 -> 6 (configure/package.json uses @codemirror v6)
- Add missing vendored dependency entries:
- arc.js (BSD-2-Clause, Dane Springmeyer)
- jqColorPicker/tinyColorPicker (MIT, Peter Dematté)
- Dropy (MIT, Tombek/CodePen)
- Leaflet.hotline (ISC, iosphere GmbH)
- Leaflet.pattern (BSD-2-Clause, Tyler Eastman)
- Leaflet.RotatedMarker (MIT, Benjamin Becquet)
- Leaflet.Snap (MIT, Makina Corpus)
- Leaflet.GeometryUtil (BSD-3-Clause, Makina Corpus)
- Leaflet.Path.Drag (MIT, Alexander Milevski)
- Leaflet.TileLayer.GL (Beerware, Iván Sánchez)
- Leaflet.VectorGrid (Beerware, Iván Sánchez)
- Leaflet.ScaleFactor (MIT, Marc Chasse)
- L.Rain (unknown origin/license)
- zlib.js from pdf.js (MIT, Mozilla Foundation)
- FileSaver.js (MIT, Eli Grey)
- georaster-layer-for-leaflet (Apache-2.0, Daniel DuFour)
- js-colormaps (MIT, der_herr_g)
- svelte-range-slider-pips (MPL-2.0, Simon Goellner)
- jQuery Autocomplete (MIT, Tomas Kirda)
- Fabric.js (MIT, bundled in OpenSeadragon/)
- planetcantile (BSD-3-Clause, Andrew Michael Annex)
- Remove phantom entries:
- Highcharts JS: not in src/external/, not in package.json, no usage
- RequireJS: only referenced in a comment in src/normalize.js, not used
- Retain CodeMirror: used in configure/ sub-app (v6 via npm)
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* chore: bump version to 5.0.21-20260603 [version bump]
* fix(ShadeTool): pin horizon/crosshair to sweep center, improve atlas performance
- Horizon profile now uses sweep-time observer center (stored in
sweepElData[id].sweepCenter) instead of current map center
- Azimuth hover line originates from sweep center position
- Crosshair pins to sweep center on pan (tracks via map 'move' event)
- Crosshair now has 2px black border + stronger shadow for visibility
- buildSweepAtlas: reduced chunk size from 16 to 4 frames, added
progress reporting ('Building atlas: XX%'), broken assembleAtlas
into chunked processing (yields to event loop between tiles)
- Toast 'Sweep complete' now shows AFTER atlas is built (was showing
before, giving false sense of completion)
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* perf(ShadeTool): eliminate toDataURL() bottleneck from atlas frame rendering
Root cause: buildSweepAtlas was calling renderResultToTileData() for each
frame, which calls c.toDataURL() per tile (PNG encoding, ~10-20ms each).
With 128 frames × N tiles = thousands of PNG encodes blocking main thread.
Fix: new _renderFrameCanvases() method that only produces canvas objects
(drawImage clone) without any toDataURL() calls. Atlas assembly still calls
toDataURL() once per output tile (unavoidable for GL layer) but that's only
N tiles total instead of N×frames.
Also: crosshair now uses outline (doesn't affect element size) so white
fill is clearly visible with black border.
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix(ShadeTool): accurate progress tracking and trajectory line wrapping
Progress bar now reflects the FULL sweep pipeline:
- 0-50%: shade computation (processChunk)
- 50%: 'Computing heatmap...' (cumulativeVisibility)
- 55-90%: 'Building atlas: X%' (frame rendering)
- 90-100%: 'Building atlas: assembling...' (tile toDataURL)
- Toast only fires at true completion
Previously the bar jumped to 100% after shade computation (fast phase)
then stayed there for 20s during atlas building with no visible progress.
Also: setTimeout(0) yield before cumulativeVisibility so the 'Computing
heatmap...' message actually paints before the synchronous computation.
Horizon chart: break trajectory line at ±180° azimuth boundary to prevent
long wrap-around lines spanning the chart when source crosses South.
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* perf(ShadeTool): rewrite atlas build to eliminate intermediate canvases
Previous approach: created 512+ intermediate canvas objects, performed
1024 drawImage operations, then assembled into atlas. Took ~15-20s.
New approach: renders pixel data directly into atlas canvas via putImageData
at the correct frame offset. Eliminates ALL intermediate canvas allocations
and drawImage calls. Benchmarked at ~2.7s for 128 frames x 8 tiles.
Additional optimizations:
- Reuse a single ImageData/buffer per tile (avoids per-frame allocation)
- Pre-compute color values outside the pixel loop (no object alloc per pixel)
- Use bitwise OR for integer division in hot loop
- Reduce processChunk batch size from 16 to 4 for smoother progress updates
- Fix progress text: show 'Computing shade X/128' instead of misleading '100%'
Also fixes horizon chart rendering order: yellow trajectory renders
BEHIND the brown area chart (rgba(90,62,35,0.8)).
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* feat(ShadeTool): horizon chart polish, TimeUI indicator API, progress rAF fix
- Remove azimuth x-axis labels from horizon chart (cleaner look)
- Add small north arrow indicator at top center of horizon chart
- shadeGraphTimeLabel now shows full ISO time (matching vstSweepFrameLabel)
- Add TimeUI.addIndicator(id, groupId, color, time) / removeIndicator(id, groupId) API
- Renders vertical colored lines on the TimeUI timeline
- Re-renders on timeline zoom/pan/redraw
- CSS: .mmgisTimeUIIndicatorLine styled as absolute-positioned colored lines
- ShadeTool adds red indicator on timeline at current playback time
- Updated on every frame change via sweepShowAllFrames
- Removed on destroy via TimeUI.removeIndicator(null, 'shadetool')
- Switch processChunk and buildSweepAtlas yields from setTimeout(fn,0) to
requestAnimationFrame(fn) so the browser paints between iterations,
making the progress bar update visibly instead of jumping 0->100%
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix: north arrow up, y-axis padding, progress bar lifecycle
- North arrow in horizon chart now points upward (triangle + N label)
- Elevation y-axis label moved slightly for better readability
- Fixed progress bar: onComplete was called before buildSweepAtlas
started, causing 'Done' state to flash before atlas build. Now
onComplete is deferred until atlas finishes in playback mode.
- Added pct updates during tile loading (0-5%) and position API (5-15%)
phases so progress bar moves throughout the entire pipeline.
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix(ShadeTool): throttle sweep progress updates, adjust UI spacing
- Throttle sweepProgressPct Zustand updates to flush at most every 50ms
so React can actually repaint between requestAnimationFrame callbacks.
Previously the store was updating ~200x/sec but the ProgressButton
never re-rendered because React batched all the rapid state changes.
- Elevation axis label: decrease translate x (4px) to give more padding
between the label and the y-axis tick marks
- North arrow: increase gap between triangle and N text (triangle moved
6px higher)
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix(ShadeTool): progress bar now updates during sweep
The ProgressButton in ShadeElement reads el.loadingProgress, but
_flushSweepProgress was only writing to the top-level sweepProgressPct
store field (consumed by the unmounted SweepSection component).
Now _flushSweepProgress also updates the active element's
loadingProgress, so the actually-rendered ProgressButton re-renders
with intermediate percentages during the sweep.
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* feat(ShadeTool): IconTextButton, monotonic progress, single-row time controls
- New design-system IconTextButton (base-ui button with icon + text label)
- Graph buttons now use IconTextButton instead of raw <button> elements
- Progress bar is monotonic: percentage never goes backwards during sweep
- Time controls condensed to single row: [start] → [step|min] → [end]
with start/end read-only (set via TimeUI), only step editable
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* style(ShadeTool): match DrawTool header, UI refinements
- Header now uses shared mmgisToolHeader/mmgisToolTitle pattern (40px, matching DrawTool exactly)
- Tool width increased to 300px
- Time row: arrows removed, start/end are plain text (not inputs)
- vstBinaryLegend + vstTime background: var(--color-a-5)
- Graph time controls: play buttons styled like IconButton md (28px, transparent bg, 18px icons)
- shadeGraphTimeLabel font-size: 14px
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* feat(ShadeTool): div-based multi-source visibility timeline + multi-arc horizon chart
- Replace canvas-based visibility timeline with div-based bars
- Show ALL shade elements with sweep results simultaneously
- Each element gets a labeled row with its own color
- Simplify time labels (no seconds/microseconds, omit year if constant)
- Title changed to 'Visibility Timeline'
- Horizon chart shows trajectory arcs from all linked shade maps
(same center, time, step) with each element's own color
- Brighten dark element colors for visibility on dark backgrounds
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix(ShadeTool): fix horizon/visibility errors + sweep progress tracking
- Add missing 'store' variable in _drawHorizonCanvas (ReferenceError)
- Fix visibility timeline accessing elms[0].results instead of elms[0].ed.results
- Track sweeping element ID in _flushSweepProgress so progress updates
target the correct element, not the (changing) activeElmId
- Reset all elements' loading state when starting a new shadeSweepAll
to prevent stale progress from a cancelled sweep
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix(ShadeTool): fix scrub error, horizon fill, and stuck sweep progress
- Fix _scrubFromVisibilityX: access elms[0].ed.results (not elms[0].results)
- Horizon chart: include source trajectory elevations in auto-fit range,
extend terrain fill to canvas bottom so arcs below horizon are covered
- Reset regenerating/loading state on cancelled sweeps so buttons don't
stay stuck at partial progress when a new sweep replaces an in-flight one
- Clean up all regenerating elements in cancelSweep and shadeSweepElement
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* refactor(ShadeTool): per-element sweep run IDs for simultaneous sweeps
- Replace global _sweepRunId with per-element _sweepRunIds map so
multiple shade maps can sweep simultaneously without cancelling
each other
- _flushSweepProgress now takes elmId as first arg to target the
correct element's loadingProgress
- _highWaterPcts is per-element for monotonic progress tracking
- shadeSweep takes explicit activeElmId parameter instead of reading
store.activeElmId (which changes during concurrent sweeps)
- shadeSweepAll uses separate _sweepAllRunId for its serialization loop
- cancelSweep increments all per-element run IDs to cancel everything
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* feat(ShadeTool): combine horizon + visibility into single panel
- Single 'Charts' toggle button replaces separate Horizon Profile and
Visibility Timeline buttons
- Both charts render in one combined bottom panel (horizon on top,
visibility below, shared time controls at bottom)
- Horizon chart min elevation fixed to exactly 5° below the minimum
terrain horizon point (saves vertical space)
- Trajectory elevations only affect the top bound, not the bottom
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix(ShadeTool): reconnect time controls to combined graph panel
- _scrubToFrame now redraws both horizon canvas and visibility timeline
after updating the store (was only setting store + callback)
- updatePlaybackFrame no longer checks _activeView type — always
redraws both charts since they're combined in one panel
- Fixes play/step/slider not updating charts after panel merge
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* feat(ShadeTool): source section closed by default, +New cycles sources
- Source collapsible section starts closed (was open)
- +New button cycles through non-custom source entities based on the
last element's source (e.g. Sun → Moon → Sun → Moon...)
- Custom source is excluded from the cycle
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix(ShadeTool): visibility timeline uses actual visibilityPct, not horizon re-computation
The visibility timeline was re-computing visibility by comparing source
elevation against the interpolated horizon profile. This disagreed with
the actual shade computation (visibilityPct) because the horizon profile
is a simplified 1D ray-cast while the shade grid is a full terrain shadow
computation.
Now uses visibilityPct > 0 (the actual shade API result) to determine
visible vs occluded segments.
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* feat(ShadeTool): add visibility timeline legend + use center-cell visibility
- Add 'Visibility Timeline' header bar with horizontal legend showing
Visible/Occluded color swatches at the top of the visibility panel
- Store centerVisible (observer grid center cell) in sweep results
instead of using grid-wide visibilityPct > 0 which was too coarse
- Falls back to visibilityPct for backward compatibility with older data
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* feat(ShadeTool): move legend to panel top, invert vis chart colors
- Legend bar at top-left of entire bottom panel (Visible/Terrain/Shaded)
applies to both horizon chart and visibility timeline
- Visibility timeline: shaded segments use element color, visible = white
- Removed vis-specific header/legend (now redundant)
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix(ShadeTool): revert vis colors, remove legend, add occluded label, fix slider alignment
- Reverted vis chart to original colors: element color = visible, gray = occluded
- Removed the panel-level graph legend
- Added 'occluded' suffix text to vis label (lighter weight/opacity)
- Fixed time slider at start/end of vis chart: use playIndex/(frameCount-1)
so slider reaches exact bar edges at first and last frames
- Widened vis label column to accommodate 'occluded' suffix
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix(ShadeTool): vis label text to 'Occultations', full opacity, 12px font
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix(ShadeTool): don't regen static shade when time slider changes during playback
Skip composite/playback elements in _onTimeChange — they should only
scrub through existing sweep frames, not trigger a new shade computation.
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix(ShadeTool): remove horizon canvas right margin, shift elev label 15px right
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix(ShadeTool): invert vis chart colors — colored = occluded, gray = visible
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix(ShadeTool): vis chart uses horizon profile for terrain-aware visibility
Uses _interpolateHorizon(profile, azimuth) to compare source elevation
against the terrain horizon mask — the same data drawn in the horizon
chart. Source is visible only when elevation > terrain at that azimuth.
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* feat(ShadeTool): gradient transitions at vis/occ boundaries in occultation chart
At each visible↔occluded transition, segments now use a CSS
linear-gradient that fades over ~20% of the segment width instead
of a hard color edge.
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix(ShadeTool): vis chart correct on first open + fix double gradient
- Redraw visibility timeline after horizon profile fetch completes
(was rendering with null profile on first open → all occluded)
- Gradient only on trailing edge of each segment (not both sides)
to eliminate the double-gradient artifact at boundaries
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix(ShadeTool): only show results section after sweep completes
Added !el?.regenerating guard to the auto-open effect so the
results/run section stays hidden during the sweep and only opens
once regenerating is false (sweep fully complete).
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix(ShadeTool): center-align vis timeline tick marks with their text labels
Use transform: translateX(-50%) on the tick container and
align-items: center so both the tick line and text are centered
at the same position.
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix(ShadeTool): hide results section during sweep, only show when complete
- Initialize resultsOpen to false for non-static modes
- Add !el.regenerating guard to the render condition so even if
resultsOpen is true, the content is hidden while regenerating
- Auto-open effect still fires when regenerating turns false
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix(ShadeTool): make vis timeline time ticks responsive to label width
Instead of hardcoded labelCol/margin-left values, measure the actual
.shadeVisBar element's left offset at render time. The time labels
container margin and scrub handler now both derive their position
from the real bar bounds, so they stay aligned regardless of label
text length.
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* feat(ShadeTool): add fast-forward button to graph time controls
New fast-forward button (mdi-fast-forward) between play/pause and
step-forward. Toggles 4x playback speed (interval/4, min 50ms).
Active state highlighted with shadeGraphPlayBtnActive class.
Clicking play/pause while fast is active resets to normal speed.
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix(ShadeTool): layer z-ordering after sweeps + vis chart time label timezone
- Re-apply sweepCardOrder z-indices after sweepShowAllFrames,
sweepShowComposite, and showSweepLayers so the first element
stays on top instead of the last-swept one
- Fix _formatSmartTimeLabel using local timezone for month but UTC
for day/hour/min — add timeZone:'UTC' to toLocaleString so
2024-01-01T00:00:00Z shows 'Jan 1' not 'Dec 1'
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix(ShadeTool): charts survive window resize without going fully occluded
Root cause: window resize fires Leaflet moveend → _onPanEnd →
invalidateHorizonCache() nulls _horizonCache. The resize redraw
timeout then finds no profile, so the vis timeline defaults all
frames to occluded and the horizon chart is skipped.
Fix: _scheduleRedraw now calls fetchAndDrawHorizon() which re-uses
the cache when the position hasn't changed or re-fetches when it
has. Also added drawVisibilityTimeline in the cache-hit path of
fetchAndDrawHorizon so both charts always update together.
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* feat(ShadeTool): remove link button, center playback controls, filter charts by center+time
- Removed playbackLinked toggle and all related state (localPlayIndex,
handleToggleLinked, vstLink styles). All elements now share the
global sweepPlayIndex.
- Centered playback controls in the sweep row.
- Charts (horizon + visibility) now only include shade maps whose
sweep center, start time, step, and frame count match the element
whose Charts button was clicked.
- Added info banner at top-left of bottom panel when shade maps are
excluded (shows count and reason).
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* feat(ShadeTool): colored azimuth lines on map while graphs open; fix stale slider on re-sweep
- Added persistent colored dashed azimuth lines on the map for each
shade element while the charts panel is open. Each line uses the
element's color and updates on every frame change (scrub, play,
step). Lines are removed when graphs close or ShadeTool is destroyed.
- Fixed the time slider retaining the old frame count after a new sweep
with fewer steps: updatePlaybackFrame now re-syncs slider.max from
the current sweep results on every call.
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix(ShadeTool): horizon profile skips near-observer DEM pixels and accounts for curvature
- HorizonProfile.py now accepts minSkipRadius (meters) and
planetRadius (meters) parameters. Rays skip DEM pixels within
minSkipRadius of the observer to avoid blocky near-field artifacts.
When planetRadius > 0, applies curvature drop (d²/2R) so distant
terrain dips below the flat-earth projection.
- Frontend sends minSkipRadius=50m and planetRadius from
F_.radiusOfPlanetMajor when vars.curvature config is enabled
(defaults to true).
- Backend route passes the two new params through to the Python script.
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix(ShadeTool): trim seconds from vstTimeReadonly display
Display shows e.g. '2024-01-01T00:00Z' instead of '2024-01-01T00:00:00Z'.
Full timestamp preserved in title tooltip. Stored value unchanged.
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix(ShadeTool): Charts button height 28px, font-size 12px
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix(ShadeTool): vstTimeReadonly styling, hide empty playback section on mode switch
- vstTimeReadonly: font-size 11px, opacity 0.8, first child text-align right
- Close results section on mode change so switching composite→playback
doesn't show an empty run section
- Auto-open now checks mode-specific data (grids for playback, atlas
for composite) instead of just results existence
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix(ShadeTool): prevent empty playback run section after composite→playback switch
Remove shadeMode from auto-open effect deps so that merely switching
modes doesn't re-open the results section with stale data from the
previous mode. The effect now only fires when actual sweep data changes
(results, grids, atlas, regenerating state), ensuring the section
stays closed after a mode switch until a new sweep completes.
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix(ShadeTool): change default icon to mdi-brightness-4
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix(ShadeTool): Charts button color var(--color-a7)
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix(ShadeTool): theme-aware canvases + composite auto-open results
- Az/El indicators, sky dome, horizon chart: fixed dark (#1a1e22)
backgrounds so they remain legible in light theme
- Vis time tick lines/text: use var(--color-a4) instead of hardcoded
white rgba
- Composite sweep auto-open: check el.lastResultGrid (set by composite
path) instead of ed.atlas (only set by playback sweep)
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix(TimeUI): mmgisTimeUITimelineExtent z-index, pointer-events, opacity
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix(ShadeTool): legend hover indicator and colorstop labels always use light text
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix(ShadeTool): lighten az/el canvas backgrounds, theme-aware sky dome labels
- Static az/el: dark fill (#2a3038) + light stroke/axis lines (was white 0.1 fill + black stroke)
- Playback az/el: lightened from #1a1e22 to #2a3038
- Sky dome N/S/E/W labels: read --color-f from CSS so they're dark in light mode
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix(ShadeTool): horizon profile fully theme-responsive
- Background reads --color-a (matches page bg in both themes)
- Grid lines use black in light mode, white in dark mode
- Tick labels, axis title use --color-a4 (muted text)
- North arrow/label uses --color-f (text color)
- Terrain fill lighter/more transparent in light mode
- 0° line adapts to theme
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix(ShadeTool): vis chart visible segments near-white in light mode
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix(ShadeTool): vis chart tick marks now span full time range
Position ticks using i/(N-1) instead of i/N so the last tick
lands at 100%. Always include the final time label. Step size
calculated from (N-1)/(maxLabels-1) to evenly distribute ticks
across the full range.
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix(ShadeTool): vis chart ticks evenly spaced 0%-100%, no drift
Ticks are now placed at evenly-spaced visual positions and each
maps back to the nearest frame index for its label. This avoids
the gradual drift caused by ticks and bar segments using different
coordinate systems (i/(N-1) vs i/N).
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix(ShadeTool): all canvas circle backgrounds to #2c2f30
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix(ShadeTool): canvas circle backgrounds to black
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix(ShadeTool): canvas circle backgrounds to #0f1010
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix(security): add path validation + numeric checks to /gethorizonprofile
- Validate required fields (path, lat, lng)
- Full URL decoding loop to catch encoded traversal
- Restrict paths to /Missions/ (cross-mission ../ allowed)
- Resolved path must stay within Missions directory
- All numeric params validated as finite numbers
- numAzimuths capped at 3600, maxRadius at 100000 (DoS prevention)
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* docs+tests(ShadeTool): E2E tests for horizon profile API, update Shade docs
- E2E tests: input validation, path traversal protection, numeric checks
- Docs: shade modes (static/composite/playback), charts panel (horizon
profile + occultation timeline), sky dome, azimuth lines, time controls
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* refactor(utils): extract shared validateMissionsPath helper
- New validateMissionsPath() handles URL decode loop + /Missions check +
path.resolve guard in one place
- queryTilesetTimesDir and /gethorizonprofile both use the shared helper
- Fix E2E test: replace Infinity test (unrepresentable in JSON) with NaN string
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix(tests): horizon profile E2E tests skip gracefully in AUTH=local mode
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* style(ShadeTool): canvas circle backgrounds to #575d60
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* style(ShadeTool): canvas circle backgrounds to #3a3e40
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix(utils): validateMissionsPath accepts paths without leading slash
Frontend sends 'Missions/...' (no leading slash) via L_.missionPath.
Normalise by prepending '/' before the prefix check.
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix(ShadeTool): pass resolved path to HorizonProfile.py + add tippy tooltips
- gethorizonprofile now passes pathResult.resolved (full filesystem path)
instead of pathResult.decoded (/Missions/...) to the Python script
- Added tippy tooltips: 'Start Time', 'End Time' on vstTimeReadonly spans,
and step size description on the InputWithUnit
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* style(ShadeTool): blue-themed Tooltip for time fields + close button shifted left 6px
- Replaced raw tippy refs with design-system <Tooltip> (blue theme)
- Close button in header shifted left 6px via margin-right: -6px
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix(security): append trailing slash to startsWith check in validateMissionsPath
Prevents /MissionsBackup/... from passing validation. Matches the
pattern already used in scripts/middleware.js (isPathInsideRoot).
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix(HorizonProfile): bounds-check observer pixel before array access
Return flat profile when observer falls outside the DEM read region
instead of crashing with IndexError.
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* feat(ShadeTool): export improvements, color ramps, no-data fix, thicker azimuth lines
1. Export TXT: add bounding box in meters + projection info to grid export
2. Export CSV: row-per-pixel format (entity,time_range/time,lat,lng,percent_visible)
instead of one row per frame — uses heatmap data for both composite and playback
3. Color ramps: add two element-color-based ramps (Fade: transparent→color→transparent,
Edges: color→transparent→color) that use the shademap's user-set color
4. No-data: hide faint red coloring (val===9) — render as fully transparent instead
5. Azimuth lines: increase stroke-width from 2 to 3.5 for better visibility on map
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* chore: bump version to 5.0.23-20260604 [version bump]
* fix(ShadeTool): export TXT bounding box uses CRS project() for projected meters
Use window.mmgisglobal.customCRS.project() to convert SW/NE corners
to proper projected coordinates instead of simple degree-to-meter
conversion. Also includes proj4 string and cell size in projected units.
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix(ShadeTool): color ramps use proper RGBA transparency, 3 stops each
The new ramps (Fade: transparent→color→transparent, Edges:
color→transparent→color) now use 3 RGBA color stops with the 4th
component controlling alpha. evalColor interpolates alpha when present.
Renderer and legend both read the interpolated alpha for these ramps.
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix(ShadeTool): include observer height in horizon cache key
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix(ColorRampPicker): rewrite dropdown to use continuous gradient with RGBA alpha support
- Replace discrete block rendering with continuous linear-gradient matching
the legend's interpolation approach
- Shadow ramp dropdown now matches legend direction (opaque→transparent)
- New alpha-based ramps (_tct, _ctc) properly show transparency over
checkerboard background in both the trigger and popup
- All ramps with hasAlpha flag get a checkerboard underlay
- Standard colormaps (viridis, plasma, etc.) render as smooth gradients
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix(ShadeTool): split composite CSV time range fields
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix(ShadeTool): refresh color-based sweeps after color changes
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* refactor(Sightline): rename ShadeTool → SightlineTool, invert rendering semantics
- Rename entire Shade tool directory to Sightline (git mv)
- Rename all files: ShadeTool.js → SightlineTool.js, etc.
- Rename store: useShadeStore → useSightlineStore
- Rename CSS classes: .shade* → .sightline*, #shadeTool → #sightlineTool
- Rename internal functions: shade() → sightline(), shadeSweep → sightlineSweep
- Rename default color ramp: 'shadow' → 'sightline'
- Update config.json: name 'Shade' → 'Sightline', paths updated
- Update blueprint reference mission config
- Update all test files and fixtures
- Update docs (Shade.md → Sightline.md, SPICE docs)
- Update imports in tools.js, AnalysisTool comment, Globe_ comments
Rendering inversion (done in prior commit, preserved here):
- store.js: invert default 1 → 0 (visible = prominent color)
- Composite heatmap: alpha = alphaFrac (not 1-alphaFrac)
- Vis chart: colored bars = visible (not occluded)
- Color ramp: alpha = t (not 1-t)
- Legend: isSightlineRamp uses alphaFrac directly
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* chore: bump version to 5.0.24-20260606 [version bump]
* fix(SightlineTool): complete rename cleanup, brighter colors, fix Devin Review findings
- Fix tool title 'Shade' → 'Sightline' in panel header
- Rename help file ShadeTool.md → SightlineTool.md, update all content
- Fix default element name 'Shade N' → 'Sightline N'
- Brighter default MULTI_SOURCE_COLORS (amber, coral, blue, green, pink, etc.)
- Fix SweepCard stale 'shadow' colorRamp fallback → 'sightline' (Devin Review)
- Fix ShaderTool_Algorithm import alias → SightlineTool_Algorithm
- Rename internal functions: computeShade → computeSightline,
showShademapLayers → showSightlinemapLayers,
clearAllShadeLayers → clearAllSightlineLayers,
reorderShadeLayers → reorderSightlineLayers
- Fix SightlinePanel calling stale reorderShadeLayers
- Update export labels: 'Shade Map' → 'Sightline Map', 'Shade Grid' → 'Sightline Grid'
- Update progress/toast strings from 'Shade' to 'Sightline'
- Update SPICE docs reference
- Update help file section headers and descriptions
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix(SightlineTool): default sightline color ramp uses element color instead of black
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix(SightlineTool): remove vstBinaryLegend, default opacity 50%
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix(SightlineTool): crosshair redesign — unfilled circle with N/S/E/W tick lines
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* chore: bump version to 5.0.25-20260608 [version bump]
* fix(SightlineTool): full-size pixelated PNG export, mode-aware CSV/TXT, rename to Results
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix(SightlineTool): entity lowercase, ISO UTC times in exports, static TXT header fix, smaller crosshair
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* feat(SightlineTool): playback export as animated GIF with basemap overlay
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix(SightlineTool): header close button margin-right 6px
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix(SightlineTool): GIF export - capture full map container, guard against 0-size canvas
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix(SightlineTool): GIF export - hide UI controls, add timestamp/progress, lower resolution
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix(SightlineTool): GIF export - 720px res, UI progress indicator, remove in-frame progress bar
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix(SightlineTool): GIF progress reaches 100% only after encoding completes
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix(SightlineTool): rewrite CSV exports per mode, remove TXT for playback
- Static: entity,time,lat,lng,visible (single time, binary)
- Playback: entity,time,lat,lng,visible (per-pixel per-frame, each frame's timestamp)
- Composite: entity,start_time,end_time,lat,lng,percent_visible (time range, heatmap)
- Remove TXT grid export option for playback mode
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix(SightlineTool): reset export dropdown to first item on mode change
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* feat(blueprints): add SightlineTool to Lunar South Pole + new Mars reference mission
- Add time section and SightlineTool config to Lunar South Pole mission
- Create new Reference-Mission-Mars with LayersTool + SightlineTool (Sol observers)
- Reference Mission already has SightlineTool on development branch
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* chore: bump version to 5.0.26-20260608 [version bump]
* feat: register Mars variant in reference mission registries
Add Mars entry to REFERENCE_MISSION_VARIANTS in both:
- API/Backend/Utils/missionTemplates.js (backend)
- configure/src/.../NewMissionModal.js (frontend dropdown)
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* Add lunar ref mission dem data
* Add lunar ref mission dem data 2
* fix: sample bbox perimeter for tile bounds in polar projections
The bounding box clamping in SightlineTool and ViewshedTool
updateDesiredTiles() projected only two corner points of the bbox to
compute tile bounds. In polar stereographic projections (e.g. Lunar
South Pole IAU2000:30120), opposite corners of a lat/lon bbox can map
to the same pixel-space point, producing a degenerate or inverted tile
range — resulting in zero tiles fetched and no sightmap/viewshed
overlay rendered.
Fix: sample 9 evenly-spaced points along each of the four bbox edges
(36 points total) and take the pixel-space envelope. This correctly
handles any map projection.
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix: projection-aware azimuth lines and source positioning for polar CRS
- locateSource: skip wrapping for custom (non-wrapping) projections so
distant sources like Sun produce nearly-parallel rays instead of being
folded into the grid as a nearby point source
- AZ hover line: rotate by local north offset derived from the projection
so the bearing line points in the correct geographic direction
- Source azimuth lines: same north-offset correction
- Add _localNorthAngle() helper that computes screen-space north direction
at any lat/lng by projecting a small latitude offset
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix(SightlineTool): skip observer-centric curvature for distant sources
When the ray source (e.g. Sun) is far outside the DEM grid, the
observer-centric curvature correction in curveData() creates a
circular visibility artifact — the terrain bowl centered on the
observer falsely hides all cells beyond ~350 km.
Skip the curvature correction when dataSource is more than one
grid-width outside the data bounds. The shadow-plane propagation
already handles parallel-ray geometry correctly on the flat grid.
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* feat(sightline): add backend sightmap endpoint with per-cell ray-march
- New Python endpoint /api/sightmap computes solar illumination grids
server-side using SPICE for Sun position + DEM ray-marching
- Frontend calls backend instead of fetching tiles + JS shadow-plane algo
- USE_BACKEND_SIGHTMAP const switch to revert to old JS algorithm
- Returns both geographic and projected bounds for polar stereo CRS
- Sightmap overlay uses image-rendering: pixelated for crisp grid cells
- Auto-detects PROJ_DATA path in conda/mamba environments
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* chore: bump version to 5.0.27-20260609 [version bump]
* refactor: consolidate sightmap to backend-only, remove old JS algorithm
- Remove USE_BACKEND_SIGHTMAP flag and all old JS shadow-plane code
- Static path: single sightmap API call (no getbands/ll2aerll)
- Sweep path: batch sightmap API call with times array
- Remove tile-based rendering (makeDataLayer, renderResultToTileData,
atlas shader, _renderFrameCanvases, SightlineTool_Manager import)
- Rewrite renderHeatmapToMap for imageOverlay (no tile layer)
- Rewrite buildSweepAtlas to pre-render frames as dataURL images
- Rewrite sweepShowFrame to swap imageOverlay URL
- Update export functions for backend grid data structure
- Backend sightmap.py: batch mode (compute_sightmap_batch),
extracted _ray_march_grid and _compute_bounds helpers
- Express route: dynamic timeout for batch (N*30s, max 30min)
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* perf: vectorize ray-march with NumPy + coarse Sun az/el interpolation
Replace pure-Python nested loops in _ray_march_grid() with numpy array
operations. All output cells are now processed simultaneously at each
march step via broadcasting and fancy indexing.
Optimizations:
1. Vectorized ray march (#1): ~60x speedup on 259×259 grid (47s → 0.8s)
2. Coarse Sun az/el subgrid (#4): compute SPICE-equivalent az/el on a
10×10 subgrid and bilinearly interpolate to all output cells, reducing
per-cell coordinate transforms from 67K to ~400.
Helper functions added:
- _vectorized_is_nodata: array-based nodata detection
- _pixels_to_geo_batch: batch pixel→geographic via GDAL CoordinateTransformation
- _sun_azel_batch: pure-numpy geodetic Sun az/el (replaces spiceypy.georec per cell)
- _bilinear_interp_2d: pure-numpy 2D bilinear interpolation (no scipy needed)
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix: store raeRaw/raeAllResults so az/el indicator canvases redraw after mount
The SightlineResults.jsx useEffect triggers on el.raeRaw to redraw
indicator canvases. The static callback was calling updateRAEIndicators
directly (before React mounted the canvases) and only setting raeResults
but not raeRaw/raeAllResults, so the useEffect never fired.
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix: playback skydome/az-el blocked by stale atlas guard + pixelated CSS selector
- SightlineElement.jsx: replace ed?.atlas checks with ed?.frameImages
(atlas was the old WebGL texture atlas; refactored code uses frameImages)
- SightlineTool.css: add img.sightmap-pixelated selector (Leaflet applies
className directly to the <img>, not a wrapper div)
- python-environment.yml: add numba>=0.60.0 for JIT optimization
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* perf: Numba JIT + adaptive march + early cutoff + multiprocessing
sightmap.py performance optimizations on the vectorized ray-march:
1. Numba JIT (@numba.njit): compile inner march loop to native code
- 259x259 grid: 0.67s cold / 0.12s warm (was 0.79s numpy-only)
2. Adaptive march step: 4x step when margin > 5°, 2x when > 2°
- Reduces iterations for clearly-illuminated cells
3. Early cutoff: max shadow distance = MAX_TERRAIN_H / tan(el)
- Skips pointless marching when Sun is high
4. Multiprocessing for batch: Pool(ncpu) across timestamps
- 12 frames in 0.62s (52ms/frame) vs sequential
Overall: 47.1s → 0.12s per grid = ~380x speedup (warm cache)
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix: use Pool initializer for multiprocessing (Windows spawn compat)
On Windows/macOS, multiprocessing uses 'spawn' instead of 'fork',
so module-level globals aren't inherited by worker processes.
Fix: pass shared dict via Pool(initializer=_init_pool_worker, initargs=...)
so each worker sets _mp_shared in its own module namespace.
Tested: 72 timestamps in 1.28s (18ms/frame) on 100x100 grid.
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix: five bugs — az offset, playback opacity, progress indicator, mode switch, hover crash
1. sightmap.py: _compute_sun_grid used dem_rows instead of dem_cols for
column pixel clamping → coarse subgrid positions were wrong → az/el
off by ~30-45° on non-square DEMs
2. Playback/sweep overlay now inherits el.opacity as initial value
instead of hardcoded 1.0. Default sweep opacity changed to null;
applySweepOpacity falls back to el.opacity when ed.opacity unset
3. ProgressButton: added indeterminate mode (sliding bar animation)
that activates automatically when loading=true and progress<=0.
Static sightmap generation shows indeterminate; sweep shows %
4. switchElementMode: switching back to static now re-renders the
cached lastData/lastResultGrid or marks changed=true for auto-regen
5. _onCompositeHover: guard against missing topLeftTile (backend
sightmap response doesn't include tile-based fields)
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix: correct sightmap ray direction for polar stereographic projections
_compute_directions had two bugs in the projected CRS branch:
1. Missing grid convergence rotation: geographic azimuth was used
directly in pixel space without rotating by the convergence angle
(longitude for polar stereographic). This caused ~30-45° offset
at the observer's longitude.
2. Inverted dy sign: cos(az) was used for dy when gt[5]<0, but
increasing pixel-y = decreasing northing, so the formula must
negate cos(az) — matching the geographic branch convention.
Also fix _showAzimuthLine/_updateSourceAzimuthLines: the fallback
path (no sweepCenter or indicatorLastDragPoint) now computes
centerLatLng from the map container center so _localNorthAngle
always receives a valid position for the convergence rotation.
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* test: add extensive Playwright tests for sightmap API endpoint
Covers:
- Input validation (missing fields, non-finite numerics, NaN, Infinity)
- Path traversal protection on DEM path
- Single-timestamp sightmap computation (structure, grid values, az/el plausibility)
- Batch multi-timestamp computation (results array, az variation)
- Custom Az/El source (el=0 all shadow, el=90 all visible)
- Error handling (invalid SPICE target, nonexistent DEM)
- Consistency (determinism, different times produce different grids)
- Observer height offset comparison
- maxOutputDim clamping
- Projected bounds for polar stereographic CRS
All DEM-dependent tests gracefully skip when Lunar South Pole
mission is not available. AUTH=local mode returns HTML instead
of JSON — tests detect and skip.
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix(sightline): use observer position for azimuth indicator north offset
The azimuth indicator line was computing _localNorthAngle from the map
center (fallback when sweepCenter is unset), which drifts as the user
pans and may be at the south pole where longitude is ~0. The sightmap
shadows are computed from the actual observer position with correct
per-cell convergence, creating a mismatch.
Fix: store the observer lat/lng as sweepCenter in sweepElData when the
static sightmap completes (same as sweep mode already does). This
ensures _showAzimuthLine, _updateSourceAzimuthLines, the crosshair
placement, and the horizon profile all use the correct observer position
for the north angle calculation.
Also add indicatorLastDragPoint fallback to _updateSourceAzimuthLines
(was missing, unlike _showAzimuthLine which already had it).
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix(sightmap): correct azimuth CW/CCW convention in _sun_azel_batch
The east vector was computed as normal × north (= Up × N = -East = West),
causing atan2(dot(sun, west), dot(sun, north)) to return counter-clockwise
azimuths. This mirrored the result: geographic 240° CW reported as 120°.
Fix: use north × normal (= N × Up = East) per right-hand rule, matching
SPICE's azccw=False (clockwise from north) convention.
Verified: batch az now matches SPICE exactly (226.2493° vs 226.2493°).
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix(sightline): use fixed-width fade gradient for visibility segments
The gradient on segment transitions was 30% of the segment width,
making wide segments have long fades and narrow segments short fades.
Now uses a fixed 2% of the total bar width for all transitions,
producing uniform fade-in and fade-out lengths regardless of segment size.
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* feat(sightline): wire resolution setting to maxOutputDim + gifshot fixes
Resolution dropdown (Low/Med/High/Ultra) now controls the sightmap
grid size sent to the backend:
Static: 100 / 200 / 400 / 800 px
Sweep: 50 / 100 / 200 / 400 px
Also:
- Add willReadFrequently to GIF export canvas contexts (suppresses
Chrome warning about slow getImageData readback)
- Add gifshot progressCallback to update export progress during the
GIF encoding phase (90→100%) instead of jumping at the end
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* refactor(sightline): remove dead SightlineTool_Manager and Layer specific DEMs config
SightlineTool_Manager.js was part of the old JS tile-fetching algorithm
(now replaced by the backend sightmap.py endpoint). Nothing imports it.
Remove:
- SightlineTool_Manager.js
- 'Layer specific DEMs' config section (variables.data with demtileurl,
minZoom, maxNativeZoom, boundingBox) from config.json and all blueprint
mission configs
- vars.data validation in initialize() — replaced with vars.dem check
The Viewshed tool's separate Manager and its layer-level demtileurl
references are unaffected.
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* perf(sightmap): downsample large DEMs at read time via GDAL decimation
High-res DEMs (e.g. 100m vs 4000m) were read fully into memory even when
the output grid was small, causing:
1. Slow computation (full array I/O + march through many more pixels)
2. Windows pickle truncation on multiprocessing (huge arrays exceed
pickle buffer limits when serialized to spawn'd worker processes)
Fix: open_dem() now accepts max_working_dim and uses GDAL's ReadAsArray
with buf_xsize/buf_ysize for server-side bilinear decimation. The
geotransform is adjusted to match the resampled pixel grid. Working dim
is set to max(max_output_dim * 4, 1000) — 4x oversampling for terrain
detail in the ray march, capped at native resolution.
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* perf(sightmap): GDAL overview bands, DEM caching, Numba warmup, reduce working_dim
Three optimizations to cut sightmap generation time on large DEMs:
1. GDAL overview bands: If the DEM has pre-computed overviews (COGs),
read from the overview band directly instead of decimating the full
raster in memory. Falls back to ReadAsArray(buf_xsize/ysize) for
DEMs without overviews.
2. DEM caching: Module-level cache keyed by (path, working_dim) avoids
re-reading the same DEM within a batch run. LRU eviction at 4 entries.
3. Numba JIT warmup: Trigger compilation at module import with a tiny
2x2 dummy array so the ~5-10s first-compilation cost is paid during
startup, not during the actual sightmap computation.
4. Reduce working_dim from 4x to 2x output grid (min 500px instead of
1000px). Halves the march distance and array sizes while maintaining
sufficient terrain detail for shadow boundary accuracy.
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* debug: add timing instrumentation to sightmap.py
Temporary timing logs to stderr at each phase:
- imports, numba_warmup, load_kernels, spice_azel, unload_kernels
- open_dem (with working_dim and actual array size)
- _precompute_grid, _compute_sun_grid, _compute_directions
- _numba_march, total
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* debug: pipe sightmap stderr to Node console for timing visibility
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* debug: include timing data in sightmap JSON response body
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* perf(sightmap): disk-based .npy cache for decimated DEMs
The timing data showed open_dem takes 12.8s (GDAL reading/decimating a
large DEM) while the actual Numba march takes only 0.065s. Since each
sightmap call spawns a new Python process, the in-memory cache is lost.
Fix: after the first GDAL decimate, save the resulting numpy array and
geotransform metadata to .npy/.json files next to the source DEM.
Subsequent process invocations load the pre-decimated array via np.load
(~0.01s). Cache is invalidated if the source DEM file is newer.
Expected improvement: first run ~14s (unchanged), second+ runs ~2s.
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* perf(sightmap): require COG format for large DEMs, remove npy cache
Replace the disk-based .npy cache with a simpler approach: require the
DEM to be a Cloud Optimized GeoTIFF (COG) when decimation is needed.
COGs have internal tiled layout + overview pyramids so GDAL can read at
any target resolution by seeking to the right bytes — no full-file scan.
If the DEM is not a COG and is large enough to need decimation, throw a
clear error with the gdal_translate command to convert it.
Small DEMs that fit within max_working_dim are read directly regardless
of format (no performance issue at small sizes).
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix: pass through Python error messages to frontend on sightmap failure
When sightmap.py exits with code!=0, the Node handler was returning a
generic 'sightmap computation failed' message. Now it tries to parse
the structured JSON error from stdout first, so the actual error
(e.g. 'DEM is not a COG') reaches the frontend.
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* chore: remove timing debug instrumentation from sightmap
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix: sanitize COG error message — no paths or tracebacks in response
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix: remove traceback from JSON error response, log to stderr only
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix: show actual sightmap error message in toast
- calls.api error callback now parses JSON response body from jqXHR
- SightlineTool error handlers display server error message (e.g. COG)
- No traceback or paths in error responses (security)
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix: remove duplicate 'sightmap error' prefix from Python error messages
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* perf(sightmap): temporarily disable multiprocessing for batch mode
Run all timestamps sequentially in the main process so the DEM stays
in memory and avoids pickle serialization overhead per worker.
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* refactor(sightmap): remove multiprocessing and Resolution UI option
- Remove all multiprocessing infrastructure (pool, workers, shared state)
- Batch timestamps run sequentially in the main process
- Remove Resolution dropdown from UI, default to ultra (800px static, 400px sweep)
- Always auto-regenerate on map move (no resolution-gated cutoff)
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* perf: temporarily disable Numba JIT for benchmarking comparison
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* 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>
* 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
- O…
…odule (#998) * fix(ShadeTool): accurate progress tracking and trajectory line wrapping Progress bar now reflects the FULL sweep pipeline: - 0-50%: shade computation (processChunk) - 50%: 'Computing heatmap...' (cumulativeVisibility) - 55-90%: 'Building atlas: X%' (frame rendering) - 90-100%: 'Building atlas: assembling...' (tile toDataURL) - Toast only fires at true completion Previously the bar jumped to 100% after shade computation (fast phase) then stayed there for 20s during atlas building with no visible progress. Also: setTimeout(0) yield before cumulativeVisibility so the 'Computing heatmap...' message actually paints before the synchronous computation. Horizon chart: break trajectory line at ±180° azimuth boundary to prevent long wrap-around lines spanning the chart when source crosses South. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * perf(ShadeTool): rewrite atlas build to eliminate intermediate canvases Previous approach: created 512+ intermediate canvas objects, performed 1024 drawImage operations, then assembled into atlas. Took ~15-20s. New approach: renders pixel data directly into atlas canvas via putImageData at the correct frame offset. Eliminates ALL intermediate canvas allocations and drawImage calls. Benchmarked at ~2.7s for 128 frames x 8 tiles. Additional optimizations: - Reuse a single ImageData/buffer per tile (avoids per-frame allocation) - Pre-compute color values outside the pixel loop (no object alloc per pixel) - Use bitwise OR for integer division in hot loop - Reduce processChunk batch size from 16 to 4 for smoother progress updates - Fix progress text: show 'Computing shade X/128' instead of misleading '100%' Also fixes horizon chart rendering order: yellow trajectory renders BEHIND the brown area chart (rgba(90,62,35,0.8)). Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * feat(ShadeTool): horizon chart polish, TimeUI indicator API, progress rAF fix - Remove azimuth x-axis labels from horizon chart (cleaner look) - Add small north arrow indicator at top center of horizon chart - shadeGraphTimeLabel now shows full ISO time (matching vstSweepFrameLabel) - Add TimeUI.addIndicator(id, groupId, color, time) / removeIndicator(id, groupId) API - Renders vertical colored lines on the TimeUI timeline - Re-renders on timeline zoom/pan/redraw - CSS: .mmgisTimeUIIndicatorLine styled as absolute-positioned colored lines - ShadeTool adds red indicator on timeline at current playback time - Updated on every frame change via sweepShowAllFrames - Removed on destroy via TimeUI.removeIndicator(null, 'shadetool') - Switch processChunk and buildSweepAtlas yields from setTimeout(fn,0) to requestAnimationFrame(fn) so the browser paints between iterations, making the progress bar update visibly instead of jumping 0->100% Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * fix: north arrow up, y-axis padding, progress bar lifecycle - North arrow in horizon chart now points upward (triangle + N label) - Elevation y-axis label moved slightly for better readability - Fixed progress bar: onComplete was called before buildSweepAtlas started, causing 'Done' state to flash before atlas build. Now onComplete is deferred until atlas finishes in playback mode. - Added pct updates during tile loading (0-5%) and position API (5-15%) phases so progress bar moves throughout the entire pipeline. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * fix(ShadeTool): throttle sweep progress updates, adjust UI spacing - Throttle sweepProgressPct Zustand updates to flush at most every 50ms so React can actually repaint between requestAnimationFrame callbacks. Previously the store was updating ~200x/sec but the ProgressButton never re-rendered because React batched all the rapid state changes. - Elevation axis label: decrease translate x (4px) to give more padding between the label and the y-axis tick marks - North arrow: increase gap between triangle and N text (triangle moved 6px higher) Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * fix(ShadeTool): progress bar now updates during sweep The ProgressButton in ShadeElement reads el.loadingProgress, but _flushSweepProgress was only writing to the top-level sweepProgressPct store field (consumed by the unmounted SweepSection component). Now _flushSweepProgress also updates the active element's loadingProgress, so the actually-rendered ProgressButton re-renders with intermediate percentages during the sweep. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * feat(ShadeTool): IconTextButton, monotonic progress, single-row time controls - New design-system IconTextButton (base-ui button with icon + text label) - Graph buttons now use IconTextButton instead of raw <button> elements - Progress bar is monotonic: percentage never goes backwards during sweep - Time controls condensed to single row: [start] → [step|min] → [end] with start/end read-only (set via TimeUI), only step editable Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * style(ShadeTool): match DrawTool header, UI refinements - Header now uses shared mmgisToolHeader/mmgisToolTitle pattern (40px, matching DrawTool exactly) - Tool width increased to 300px - Time row: arrows removed, start/end are plain text (not inputs) - vstBinaryLegend + vstTime background: var(--color-a-5) - Graph time controls: play buttons styled like IconButton md (28px, transparent bg, 18px icons) - shadeGraphTimeLabel font-size: 14px Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * feat(ShadeTool): div-based multi-source visibility timeline + multi-arc horizon chart - Replace canvas-based visibility timeline with div-based bars - Show ALL shade elements with sweep results simultaneously - Each element gets a labeled row with its own color - Simplify time labels (no seconds/microseconds, omit year if constant) - Title changed to 'Visibility Timeline' - Horizon chart shows trajectory arcs from all linked shade maps (same center, time, step) with each element's own color - Brighten dark element colors for visibility on dark backgrounds Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * fix(ShadeTool): fix horizon/visibility errors + sweep progress tracking - Add missing 'store' variable in _drawHorizonCanvas (ReferenceError) - Fix visibility timeline accessing elms[0].results instead of elms[0].ed.results - Track sweeping element ID in _flushSweepProgress so progress updates target the correct element, not the (changing) activeElmId - Reset all elements' loading state when starting a new shadeSweepAll to prevent stale progress from a cancelled sweep Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * fix(ShadeTool): fix scrub error, horizon fill, and stuck sweep progress - Fix _scrubFromVisibilityX: access elms[0].ed.results (not elms[0].results) - Horizon chart: include source trajectory elevations in auto-fit range, extend terrain fill to canvas bottom so arcs below horizon are covered - Reset regenerating/loading state on cancelled sweeps so buttons don't stay stuck at partial progress when a new sweep replaces an in-flight one - Clean up all regenerating elements in cancelSweep and shadeSweepElement Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * refactor(ShadeTool): per-element sweep run IDs for simultaneous sweeps - Replace global _sweepRunId with per-element _sweepRunIds map so multiple shade maps can sweep simultaneously without cancelling each other - _flushSweepProgress now takes elmId as first arg to target the correct element's loadingProgress - _highWaterPcts is per-element for monotonic progress tracking - shadeSweep takes explicit activeElmId parameter instead of reading store.activeElmId (which changes during concurrent sweeps) - shadeSweepAll uses separate _sweepAllRunId for its serialization loop - cancelSweep increments all per-element run IDs to cancel everything Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * feat(ShadeTool): combine horizon + visibility into single panel - Single 'Charts' toggle button replaces separate Horizon Profile and Visibility Timeline buttons - Both charts render in one combined bottom panel (horizon on top, visibility below, shared time controls at bottom) - Horizon chart min elevation fixed to exactly 5° below the minimum terrain horizon point (saves vertical space) - Trajectory elevations only affect the top bound, not the bottom Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * fix(ShadeTool): reconnect time controls to combined graph panel - _scrubToFrame now redraws both horizon canvas and visibility timeline after updating the store (was only setting store + callback) - updatePlaybackFrame no longer checks _activeView type — always redraws both charts since they're combined in one panel - Fixes play/step/slider not updating charts after panel merge Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * feat(ShadeTool): source section closed by default, +New cycles sources - Source collapsible section starts closed (was open) - +New button cycles through non-custom source entities based on the last element's source (e.g. Sun → Moon → Sun → Moon...) - Custom source is excluded from the cycle Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * fix(ShadeTool): visibility timeline uses actual visibilityPct, not horizon re-computation The visibility timeline was re-computing visibility by comparing source elevation against the interpolated horizon profile. This disagreed with the actual shade computation (visibilityPct) because the horizon profile is a simplified 1D ray-cast while the shade grid is a full terrain shadow computation. Now uses visibilityPct > 0 (the actual shade API result) to determine visible vs occluded segments. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * feat(ShadeTool): add visibility timeline legend + use center-cell visibility - Add 'Visibility Timeline' header bar with horizontal legend showing Visible/Occluded color swatches at the top of the visibility panel - Store centerVisible (observer grid center cell) in sweep results instead of using grid-wide visibilityPct > 0 which was too coarse - Falls back to visibilityPct for backward compatibility with older data Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * feat(ShadeTool): move legend to panel top, invert vis chart colors - Legend bar at top-left of entire bottom panel (Visible/Terrain/Shaded) applies to both horizon chart and visibility timeline - Visibility timeline: shaded segments use element color, visible = white - Removed vis-specific header/legend (now redundant) Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * fix(ShadeTool): revert vis colors, remove legend, add occluded label, fix slider alignment - Reverted vis chart to original colors: element color = visible, gray = occluded - Removed the panel-level graph legend - Added 'occluded' suffix text to vis label (lighter weight/opacity) - Fixed time slider at start/end of vis chart: use playIndex/(frameCount-1) so slider reaches exact bar edges at first and last frames - Widened vis label column to accommodate 'occluded' suffix Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * fix(ShadeTool): vis label text to 'Occultations', full opacity, 12px font Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * fix(ShadeTool): don't regen static shade when time slider changes during playback Skip composite/playback elements in _onTimeChange — they should only scrub through existing sweep frames, not trigger a new shade computation. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * fix(ShadeTool): remove horizon canvas right margin, shift elev label 15px right Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * fix(ShadeTool): invert vis chart colors — colored = occluded, gray = visible Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * fix(ShadeTool): vis chart uses horizon profile for terrain-aware visibility Uses _interpolateHorizon(profile, azimuth) to compare source elevation against the terrain horizon mask — the same data drawn in the horizon chart. Source is visible only when elevation > terrain at that azimuth. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * feat(ShadeTool): gradient transitions at vis/occ boundaries in occultation chart At each visible↔occluded transition, segments now use a CSS linear-gradient that fades over ~20% of the segment width instead of a hard color edge. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * fix(ShadeTool): vis chart correct on first open + fix double gradient - Redraw visibility timeline after horizon profile fetch completes (was rendering with null profile on first open → all occluded) - Gradient only on trailing edge of each segment (not both sides) to eliminate the double-gradient artifact at boundaries Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * fix(ShadeTool): only show results section after sweep completes Added !el?.regenerating guard to the auto-open effect so the results/run section stays hidden during the sweep and only opens once regenerating is false (sweep fully complete). Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * fix(ShadeTool): center-align vis timeline tick marks with their text labels Use transform: translateX(-50%) on the tick container and align-items: center so both the tick line and text are centered at the same position. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * fix(ShadeTool): hide results section during sweep, only show when complete - Initialize resultsOpen to false for non-static modes - Add !el.regenerating guard to the render condition so even if resultsOpen is true, the content is hidden while regenerating - Auto-open effect still fires when regenerating turns false Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * fix(ShadeTool): make vis timeline time ticks responsive to label width Instead of hardcoded labelCol/margin-left values, measure the actual .shadeVisBar element's left offset at render time. The time labels container margin and scrub handler now both derive their position from the real bar bounds, so they stay aligned regardless of label text length. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * feat(ShadeTool): add fast-forward button to graph time controls New fast-forward button (mdi-fast-forward) between play/pause and step-forward. Toggles 4x playback speed (interval/4, min 50ms). Active state highlighted with shadeGraphPlayBtnActive class. Clicking play/pause while fast is active resets to normal speed. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * fix(ShadeTool): layer z-ordering after sweeps + vis chart time label timezone - Re-apply sweepCardOrder z-indices after sweepShowAllFrames, sweepShowComposite, and showSweepLayers so the first element stays on top instead of the last-swept one - Fix _formatSmartTimeLabel using local timezone for month but UTC for day/hour/min — add timeZone:'UTC' to toLocaleString so 2024-01-01T00:00:00Z shows 'Jan 1' not 'Dec 1' Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * fix(ShadeTool): charts survive window resize without going fully occluded Root cause: window resize fires Leaflet moveend → _onPanEnd → invalidateHorizonCache() nulls _horizonCache. The resize redraw timeout then finds no profile, so the vis timeline defaults all frames to occluded and the horizon chart is skipped. Fix: _scheduleRedraw now calls fetchAndDrawHorizon() which re-uses the cache when the position hasn't changed or re-fetches when it has. Also added drawVisibilityTimeline in the cache-hit path of fetchAndDrawHorizon so both charts always update together. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * feat(ShadeTool): remove link button, center playback controls, filter charts by center+time - Removed playbackLinked toggle and all related state (localPlayIndex, handleToggleLinked, vstLink styles). All elements now share the global sweepPlayIndex. - Centered playback controls in the sweep row. - Charts (horizon + visibility) now only include shade maps whose sweep center, start time, step, and frame count match the element whose Charts button was clicked. - Added info banner at top-left of bottom panel when shade maps are excluded (shows count and reason). Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * feat(ShadeTool): colored azimuth lines on map while graphs open; fix stale slider on re-sweep - Added persistent colored dashed azimuth lines on the map for each shade element while the charts panel is open. Each line uses the element's color and updates on every frame change (scrub, play, step). Lines are removed when graphs close or ShadeTool is destroyed. - Fixed the time slider retaining the old frame count after a new sweep with fewer steps: updatePlaybackFrame now re-syncs slider.max from the current sweep results on every call. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * fix(ShadeTool): horizon profile skips near-observer DEM pixels and accounts for curvature - HorizonProfile.py now accepts minSkipRadius (meters) and planetRadius (meters) parameters. Rays skip DEM pixels within minSkipRadius of the observer to avoid blocky near-field artifacts. When planetRadius > 0, applies curvature drop (d²/2R) so distant terrain dips below the flat-earth projection. - Frontend sends minSkipRadius=50m and planetRadius from F_.radiusOfPlanetMajor when vars.curvature config is enabled (defaults to true). - Backend route passes the two new params through to the Python script. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * fix(ShadeTool): trim seconds from vstTimeReadonly display Display shows e.g. '2024-01-01T00:00Z' instead of '2024-01-01T00:00:00Z'. Full timestamp preserved in title tooltip. Stored value unchanged. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * fix(ShadeTool): Charts button height 28px, font-size 12px Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * fix(ShadeTool): vstTimeReadonly styling, hide empty playback section on mode switch - vstTimeReadonly: font-size 11px, opacity 0.8, first child text-align right - Close results section on mode change so switching composite→playback doesn't show an empty run section - Auto-open now checks mode-specific data (grids for playback, atlas for composite) instead of just results existence Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * fix(ShadeTool): prevent empty playback run section after composite→playback switch Remove shadeMode from auto-open effect deps so that merely switching modes doesn't re-open the results section with stale data from the previous mode. The effect now only fires when actual sweep data changes (results, grids, atlas, regenerating state), ensuring the section stays closed after a mode switch until a new sweep completes. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * fix(ShadeTool): change default icon to mdi-brightness-4 Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * fix(ShadeTool): Charts button color var(--color-a7) Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * fix(ShadeTool): theme-aware canvases + composite auto-open results - Az/El indicators, sky dome, horizon chart: fixed dark (#1a1e22) backgrounds so they remain legible in light theme - Vis time tick lines/text: use var(--color-a4) instead of hardcoded white rgba - Composite sweep auto-open: check el.lastResultGrid (set by composite path) instead of ed.atlas (only set by playback sweep) Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * fix(TimeUI): mmgisTimeUITimelineExtent z-index, pointer-events, opacity Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * fix(ShadeTool): legend hover indicator and colorstop labels always use light text Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * fix(ShadeTool): lighten az/el canvas backgrounds, theme-aware sky dome labels - Static az/el: dark fill (#2a3038) + light stroke/axis lines (was white 0.1 fill + black stroke) - Playback az/el: lightened from #1a1e22 to #2a3038 - Sky dome N/S/E/W labels: read --color-f from CSS so they're dark in light mode Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * fix(ShadeTool): horizon profile fully theme-responsive - Background reads --color-a (matches page bg in both themes) - Grid lines use black in light mode, white in dark mode - Tick labels, axis title use --color-a4 (muted text) - North arrow/label uses --color-f (text color) - Terrain fill lighter/more transparent in light mode - 0° line adapts to theme Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * fix(ShadeTool): vis chart visible segments near-white in light mode Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * fix(ShadeTool): vis chart tick marks now span full time range Position ticks using i/(N-1) instead of i/N so the last tick lands at 100%. Always include the final time label. Step size calculated from (N-1)/(maxLabels-1) to evenly distribute ticks across the full range. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * fix(ShadeTool): vis chart ticks evenly spaced 0%-100%, no drift Ticks are now placed at evenly-spaced visual positions and each maps back to the nearest frame index for its label. This avoids the gradual drift caused by ticks and bar segments using different coordinate systems (i/(N-1) vs i/N). Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * fix(ShadeTool): all canvas circle backgrounds to #2c2f30 Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * fix(ShadeTool): canvas circle backgrounds to black Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * fix(ShadeTool): canvas circle backgrounds to #0f1010 Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * fix(security): add path validation + numeric checks to /gethorizonprofile - Validate required fields (path, lat, lng) - Full URL decoding loop to catch encoded traversal - Restrict paths to /Missions/ (cross-mission ../ allowed) - Resolved path must stay within Missions directory - All numeric params validated as finite numbers - numAzimuths capped at 3600, maxRadius at 100000 (DoS prevention) Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * docs+tests(ShadeTool): E2E tests for horizon profile API, update Shade docs - E2E tests: input validation, path traversal protection, numeric checks - Docs: shade modes (static/composite/playback), charts panel (horizon profile + occultation timeline), sky dome, azimuth lines, time controls Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * refactor(utils): extract shared validateMissionsPath helper - New validateMissionsPath() handles URL decode loop + /Missions check + path.resolve guard in one place - queryTilesetTimesDir and /gethorizonprofile both use the shared helper - Fix E2E test: replace Infinity test (unrepresentable in JSON) with NaN string Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * fix(tests): horizon profile E2E tests skip gracefully in AUTH=local mode Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * style(ShadeTool): canvas circle backgrounds to #575d60 Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * style(ShadeTool): canvas circle backgrounds to #3a3e40 Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * fix(utils): validateMissionsPath accepts paths without leading slash Frontend sends 'Missions/...' (no leading slash) via L_.missionPath. Normalise by prepending '/' before the prefix check. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * fix(ShadeTool): pass resolved path to HorizonProfile.py + add tippy tooltips - gethorizonprofile now passes pathResult.resolved (full filesystem path) instead of pathResult.decoded (/Missions/...) to the Python script - Added tippy tooltips: 'Start Time', 'End Time' on vstTimeReadonly spans, and step size description on the InputWithUnit Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * style(ShadeTool): blue-themed Tooltip for time fields + close button shifted left 6px - Replaced raw tippy refs with design-system <Tooltip> (blue theme) - Close button in header shifted left 6px via margin-right: -6px Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * fix(security): append trailing slash to startsWith check in validateMissionsPath Prevents /MissionsBackup/... from passing validation. Matches the pattern already used in scripts/middleware.js (isPathInsideRoot). Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * fix(HorizonProfile): bounds-check observer pixel before array access Return flat profile when observer falls outside the DEM read region instead of crashing with IndexError. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * feat(ShadeTool): export improvements, color ramps, no-data fix, thicker azimuth lines 1. Export TXT: add bounding box in meters + projection info to grid export 2. Export CSV: row-per-pixel format (entity,time_range/time,lat,lng,percent_visible) instead of one row per frame — uses heatmap data for both composite and playback 3. Color ramps: add two element-color-based ramps (Fade: transparent→color→transparent, Edges: color→transparent→color) that use the shademap's user-set color 4. No-data: hide faint red coloring (val===9) — render as fully transparent instead 5. Azimuth lines: increase stroke-width from 2 to 3.5 for better visibility on map Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * chore: bump version to 5.0.23-20260604 [version bump] * fix(ShadeTool): export TXT bounding box uses CRS project() for projected meters Use window.mmgisglobal.customCRS.project() to convert SW/NE corners to proper projected coordinates instead of simple degree-to-meter conversion. Also includes proj4 string and cell size in projected units. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * fix(ShadeTool): color ramps use proper RGBA transparency, 3 stops each The new ramps (Fade: transparent→color→transparent, Edges: color→transparent→color) now use 3 RGBA color stops with the 4th component controlling alpha. evalColor interpolates alpha when present. Renderer and legend both read the interpolated alpha for these ramps. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * fix(ShadeTool): include observer height in horizon cache key Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * fix(ColorRampPicker): rewrite dropdown to use continuous gradient with RGBA alpha support - Replace discrete block rendering with continuous linear-gradient matching the legend's interpolation approach - Shadow ramp dropdown now matches legend direction (opaque→transparent) - New alpha-based ramps (_tct, _ctc) properly show transparency over checkerboard background in both the trigger and popup - All ramps with hasAlpha flag get a checkerboard underlay - Standard colormaps (viridis, plasma, etc.) render as smooth gradients Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * fix(ShadeTool): split composite CSV time range fields Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * fix(ShadeTool): refresh color-based sweeps after color changes Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * refactor(Sightline): rename ShadeTool → SightlineTool, invert rendering semantics - Rename entire Shade tool directory to Sightline (git mv) - Rename all files: ShadeTool.js → SightlineTool.js, etc. - Rename store: useShadeStore → useSightlineStore - Rename CSS classes: .shade* → .sightline*, #shadeTool → #sightlineTool - Rename internal functions: shade() → sightline(), shadeSweep → sightlineSweep - Rename default color ramp: 'shadow' → 'sightline' - Update config.json: name 'Shade' → 'Sightline', paths updated - Update blueprint reference mission config - Update all test files and fixtures - Update docs (Shade.md → Sightline.md, SPICE docs) - Update imports in tools.js, AnalysisTool comment, Globe_ comments Rendering inversion (done in prior commit, preserved here): - store.js: invert default 1 → 0 (visible = prominent color) - Composite heatmap: alpha = alphaFrac (not 1-alphaFrac) - Vis chart: colored bars = visible (not occluded) - Color ramp: alpha = t (not 1-t) - Legend: isSightlineRamp uses alphaFrac directly Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * chore: bump version to 5.0.24-20260606 [version bump] * fix(SightlineTool): complete rename cleanup, brighter colors, fix Devin Review findings - Fix tool title 'Shade' → 'Sightline' in panel header - Rename help file ShadeTool.md → SightlineTool.md, update all content - Fix default element name 'Shade N' → 'Sightline N' - Brighter default MULTI_SOURCE_COLORS (amber, coral, blue, green, pink, etc.) - Fix SweepCard stale 'shadow' colorRamp fallback → 'sightline' (Devin Review) - Fix ShaderTool_Algorithm import alias → SightlineTool_Algorithm - Rename internal functions: computeShade → computeSightline, showShademapLayers → showSightlinemapLayers, clearAllShadeLayers → clearAllSightlineLayers, reorderShadeLayers → reorderSightlineLayers - Fix SightlinePanel calling stale reorderShadeLayers - Update export labels: 'Shade Map' → 'Sightline Map', 'Shade Grid' → 'Sightline Grid' - Update progress/toast strings from 'Shade' to 'Sightline' - Update SPICE docs reference - Update help file section headers and descriptions Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * fix(SightlineTool): default sightline color ramp uses element color instead of black Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * fix(SightlineTool): remove vstBinaryLegend, default opacity 50% Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * fix(SightlineTool): crosshair redesign — unfilled circle with N/S/E/W tick lines Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * chore: bump version to 5.0.25-20260608 [version bump] * fix(SightlineTool): full-size pixelated PNG export, mode-aware CSV/TXT, rename to Results Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * fix(SightlineTool): entity lowercase, ISO UTC times in exports, static TXT header fix, smaller crosshair Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * feat(SightlineTool): playback export as animated GIF with basemap overlay Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * fix(SightlineTool): header close button margin-right 6px Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * fix(SightlineTool): GIF export - capture full map container, guard against 0-size canvas Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * fix(SightlineTool): GIF export - hide UI controls, add timestamp/progress, lower resolution Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * fix(SightlineTool): GIF export - 720px res, UI progress indicator, remove in-frame progress bar Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * fix(SightlineTool): GIF progress reaches 100% only after encoding completes Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * fix(SightlineTool): rewrite CSV exports per mode, remove TXT for playback - Static: entity,time,lat,lng,visible (single time, binary) - Playback: entity,time,lat,lng,visible (per-pixel per-frame, each frame's timestamp) - Composite: entity,start_time,end_time,lat,lng,percent_visible (time range, heatmap) - Remove TXT grid export option for playback mode Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * fix(SightlineTool): reset export dropdown to first item on mode change Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * feat(blueprints): add SightlineTool to Lunar South Pole + new Mars reference mission - Add time section and SightlineTool config to Lunar South Pole mission - Create new Reference-Mission-Mars with LayersTool + SightlineTool (Sol observers) - Reference Mission already has SightlineTool on development branch Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * chore: bump version to 5.0.26-20260608 [version bump] * feat: register Mars variant in reference mission registries Add Mars entry to REFERENCE_MISSION_VARIANTS in both: - API/Backend/Utils/missionTemplates.js (backend) - configure/src/.../NewMissionModal.js (frontend dropdown) Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Add lunar ref mission dem data * Add lunar ref mission dem data 2 * fix: sample bbox perimeter for tile bounds in polar projections The bounding box clamping in SightlineTool and ViewshedTool updateDesiredTiles() projected only two corner points of the bbox to compute tile bounds. In polar stereographic projections (e.g. Lunar South Pole IAU2000:30120), opposite corners of a lat/lon bbox can map to the same pixel-space point, producing a degenerate or inverted tile range — resulting in zero tiles fetched and no sightmap/viewshed overlay rendered. Fix: sample 9 evenly-spaced points along each of the four bbox edges (36 points total) and take the pixel-space envelope. This correctly handles any map projection. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * fix: projection-aware azimuth lines and source positioning for polar CRS - locateSource: skip wrapping for custom (non-wrapping) projections so distant sources like Sun produce nearly-parallel rays instead of being folded into the grid as a nearby point source - AZ hover line: rotate by local north offset derived from the projection so the bearing line points in the correct geographic direction - Source azimuth lines: same north-offset correction - Add _localNorthAngle() helper that computes screen-space north direction at any lat/lng by projecting a small latitude offset Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * fix(SightlineTool): skip observer-centric curvature for distant sources When the ray source (e.g. Sun) is far outside the DEM grid, the observer-centric curvature correction in curveData() creates a circular visibility artifact — the terrain bowl centered on the observer falsely hides all cells beyond ~350 km. Skip the curvature correction when dataSource is more than one grid-width outside the data bounds. The shadow-plane propagation already handles parallel-ray geometry correctly on the flat grid. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * feat(sightline): add backend sightmap endpoint with per-cell ray-march - New Python endpoint /api/sightmap computes solar illumination grids server-side using SPICE for Sun position + DEM ray-marching - Frontend calls backend instead of fetching tiles + JS shadow-plane algo - USE_BACKEND_SIGHTMAP const switch to revert to old JS algorithm - Returns both geographic and projected bounds for polar stereo CRS - Sightmap overlay uses image-rendering: pixelated for crisp grid cells - Auto-detects PROJ_DATA path in conda/mamba environments Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * chore: bump version to 5.0.27-20260609 [version bump] * refactor: consolidate sightmap to backend-only, remove old JS algorithm - Remove USE_BACKEND_SIGHTMAP flag and all old JS shadow-plane code - Static path: single sightmap API call (no getbands/ll2aerll) - Sweep path: batch sightmap API call with times array - Remove tile-based rendering (makeDataLayer, renderResultToTileData, atlas shader, _renderFrameCanvases, SightlineTool_Manager import) - Rewrite renderHeatmapToMap for imageOverlay (no tile layer) - Rewrite buildSweepAtlas to pre-render frames as dataURL images - Rewrite sweepShowFrame to swap imageOverlay URL - Update export functions for backend grid data structure - Backend sightmap.py: batch mode (compute_sightmap_batch), extracted _ray_march_grid and _compute_bounds helpers - Express route: dynamic timeout for batch (N*30s, max 30min) Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * perf: vectorize ray-march with NumPy + coarse Sun az/el interpolation Replace pure-Python nested loops in _ray_march_grid() with numpy array operations. All output cells are now processed simultaneously at each march step via broadcasting and fancy indexing. Optimizations: 1. Vectorized ray march (#1): ~60x speedup on 259×259 grid (47s → 0.8s) 2. Coarse Sun az/el subgrid (#4): compute SPICE-equivalent az/el on a 10×10 subgrid and bilinearly interpolate to all output cells, reducing per-cell coordinate transforms from 67K to ~400. Helper functions added: - _vectorized_is_nodata: array-based nodata detection - _pixels_to_geo_batch: batch pixel→geographic via GDAL CoordinateTransformation - _sun_azel_batch: pure-numpy geodetic Sun az/el (replaces spiceypy.georec per cell) - _bilinear_interp_2d: pure-numpy 2D bilinear interpolation (no scipy needed) Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * fix: store raeRaw/raeAllResults so az/el indicator canvases redraw after mount The SightlineResults.jsx useEffect triggers on el.raeRaw to redraw indicator canvases. The static callback was calling updateRAEIndicators directly (before React mounted the canvases) and only setting raeResults but not raeRaw/raeAllResults, so the useEffect never fired. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * fix: playback skydome/az-el blocked by stale atlas guard + pixelated CSS selector - SightlineElement.jsx: replace ed?.atlas checks with ed?.frameImages (atlas was the old WebGL texture atlas; refactored code uses frameImages) - SightlineTool.css: add img.sightmap-pixelated selector (Leaflet applies className directly to the <img>, not a wrapper div) - python-environment.yml: add numba>=0.60.0 for JIT optimization Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * perf: Numba JIT + adaptive march + early cutoff + multiprocessing sightmap.py performance optimizations on the vectorized ray-march: 1. Numba JIT (@numba.njit): compile inner march loop to native code - 259x259 grid: 0.67s cold / 0.12s warm (was 0.79s numpy-only) 2. Adaptive march step: 4x step when margin > 5°, 2x when > 2° - Reduces iterations for clearly-illuminated cells 3. Early cutoff: max shadow distance = MAX_TERRAIN_H / tan(el) - Skips pointless marching when Sun is high 4. Multiprocessing for batch: Pool(ncpu) across timestamps - 12 frames in 0.62s (52ms/frame) vs sequential Overall: 47.1s → 0.12s per grid = ~380x speedup (warm cache) Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * fix: use Pool initializer for multiprocessing (Windows spawn compat) On Windows/macOS, multiprocessing uses 'spawn' instead of 'fork', so module-level globals aren't inherited by worker processes. Fix: pass shared dict via Pool(initializer=_init_pool_worker, initargs=...) so each worker sets _mp_shared in its own module namespace. Tested: 72 timestamps in 1.28s (18ms/frame) on 100x100 grid. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * fix: five bugs — az offset, playback opacity, progress indicator, mode switch, hover crash 1. sightmap.py: _compute_sun_grid used dem_rows instead of dem_cols for column pixel clamping → coarse subgrid positions were wrong → az/el off by ~30-45° on non-square DEMs 2. Playback/sweep overlay now inherits el.opacity as initial value instead of hardcoded 1.0. Default sweep opacity changed to null; applySweepOpacity falls back to el.opacity when ed.opacity unset 3. ProgressButton: added indeterminate mode (sliding bar animation) that activates automatically when loading=true and progress<=0. Static sightmap generation shows indeterminate; sweep shows % 4. switchElementMode: switching back to static now re-renders the cached lastData/lastResultGrid or marks changed=true for auto-regen 5. _onCompositeHover: guard against missing topLeftTile (backend sightmap response doesn't include tile-based fields) Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * fix: correct sightmap ray direction for polar stereographic projections _compute_directions had two bugs in the projected CRS branch: 1. Missing grid convergence rotation: geographic azimuth was used directly in pixel space without rotating by the convergence angle (longitude for polar stereographic). This caused ~30-45° offset at the observer's longitude. 2. Inverted dy sign: cos(az) was used for dy when gt[5]<0, but increasing pixel-y = decreasing northing, so the formula must negate cos(az) — matching the geographic branch convention. Also fix _showAzimuthLine/_updateSourceAzimuthLines: the fallback path (no sweepCenter or indicatorLastDragPoint) now computes centerLatLng from the map container center so _localNorthAngle always receives a valid position for the convergence rotation. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * test: add extensive Playwright tests for sightmap API endpoint Covers: - Input validation (missing fields, non-finite numerics, NaN, Infinity) - Path traversal protection on DEM path - Single-timestamp sightmap computation (structure, grid values, az/el plausibility) - Batch multi-timestamp computation (results array, az variation) - Custom Az/El source (el=0 all shadow, el=90 all visible) - Error handling (invalid SPICE target, nonexistent DEM) - Consistency (determinism, different times produce different grids) - Observer height offset comparison - maxOutputDim clamping - Projected bounds for polar stereographic CRS All DEM-dependent tests gracefully skip when Lunar South Pole mission is not available. AUTH=local mode returns HTML instead of JSON — tests detect and skip. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * fix(sightline): use observer position for azimuth indicator north offset The azimuth indicator line was computing _localNorthAngle from the map center (fallback when sweepCenter is unset), which drifts as the user pans and may be at the south pole where longitude is ~0. The sightmap shadows are computed from the actual observer position with correct per-cell convergence, creating a mismatch. Fix: store the observer lat/lng as sweepCenter in sweepElData when the static sightmap completes (same as sweep mode already does). This ensures _showAzimuthLine, _updateSourceAzimuthLines, the crosshair placement, and the horizon profile all use the correct observer position for the north angle calculation. Also add indicatorLastDragPoint fallback to _updateSourceAzimuthLines (was missing, unlike _showAzimuthLine which already had it). Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * fix(sightmap): correct azimuth CW/CCW convention in _sun_azel_batch The east vector was computed as normal × north (= Up × N = -East = West), causing atan2(dot(sun, west), dot(sun, north)) to return counter-clockwise azimuths. This mirrored the result: geographic 240° CW reported as 120°. Fix: use north × normal (= N × Up = East) per right-hand rule, matching SPICE's azccw=False (clockwise from north) convention. Verified: batch az now matches SPICE exactly (226.2493° vs 226.2493°). Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * fix(sightline): use fixed-width fade gradient for visibility segments The gradient on segment transitions was 30% of the segment width, making wide segments have long fades and narrow segments short fades. Now uses a fixed 2% of the total bar width for all transitions, producing uniform fade-in and fade-out lengths regardless of segment size. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * feat(sightline): wire resolution setting to maxOutputDim + gifshot fixes Resolution dropdown (Low/Med/High/Ultra) now controls the sightmap grid size sent to the backend: Static: 100 / 200 / 400 / 800 px Sweep: 50 / 100 / 200 / 400 px Also: - Add willReadFrequently to GIF export canvas contexts (suppresses Chrome warning about slow getImageData readback) - Add gifshot progressCallback to update export progress during the GIF encoding phase (90→100%) instead of jumping at the end Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * refactor(sightline): remove dead SightlineTool_Manager and Layer specific DEMs config SightlineTool_Manager.js was part of the old JS tile-fetching algorithm (now replaced by the backend sightmap.py endpoint). Nothing imports it. Remove: - SightlineTool_Manager.js - 'Layer specific DEMs' config section (variables.data with demtileurl, minZoom, maxNativeZoom, boundingBox) from config.json and all blueprint mission configs - vars.data validation in initialize() — replaced with vars.dem check The Viewshed tool's separate Manager and its layer-level demtileurl references are unaffected. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * perf(sightmap): downsample large DEMs at read time via GDAL decimation High-res DEMs (e.g. 100m vs 4000m) were read fully into memory even when the output grid was small, causing: 1. Slow computation (full array I/O + march through many more pixels) 2. Windows pickle truncation on multiprocessing (huge arrays exceed pickle buffer limits when serialized to spawn'd worker processes) Fix: open_dem() now accepts max_working_dim and uses GDAL's ReadAsArray with buf_xsize/buf_ysize for server-side bilinear decimation. The geotransform is adjusted to match the resampled pixel grid. Working dim is set to max(max_output_dim * 4, 1000) — 4x oversampling for terrain detail in the ray march, capped at native resolution. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * perf(sightmap): GDAL overview bands, DEM caching, Numba warmup, reduce working_dim Three optimizations to cut sightmap generation time on large DEMs: 1. GDAL overview bands: If the DEM has pre-computed overviews (COGs), read from the overview band directly instead of decimating the full raster in memory. Falls back to ReadAsArray(buf_xsize/ysize) for DEMs without overviews. 2. DEM caching: Module-level cache keyed by (path, working_dim) avoids re-reading the same DEM within a batch run. LRU eviction at 4 entries. 3. Numba JIT warmup: Trigger compilation at module import with a tiny 2x2 dummy array so the ~5-10s first-compilation cost is paid during startup, not during the actual sightmap computation. 4. Reduce working_dim from 4x to 2x output grid (min 500px instead of 1000px). Halves the march distance and array sizes while maintaining sufficient terrain detail for shadow boundary accuracy. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * debug: add timing instrumentation to sightmap.py Temporary timing logs to stderr at each phase: - imports, numba_warmup, load_kernels, spice_azel, unload_kernels - open_dem (with working_dim and actual array size) - _precompute_grid, _compute_sun_grid, _compute_directions - _numba_march, total Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * debug: pipe sightmap stderr to Node console for timing visibility Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * debug: include timing data in sightmap JSON response body Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * perf(sightmap): disk-based .npy cache for decimated DEMs The timing data showed open_dem takes 12.8s (GDAL reading/decimating a large DEM) while the actual Numba march takes only 0.065s. Since each sightmap call spawns a new Python process, the in-memory cache is lost. Fix: after the first GDAL decimate, save the resulting numpy array and geotransform metadata to .npy/.json files next to the source DEM. Subsequent process invocations load the pre-decimated array via np.load (~0.01s). Cache is invalidated if the source DEM file is newer. Expected improvement: first run ~14s (unchanged), second+ runs ~2s. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * perf(sightmap): require COG format for large DEMs, remove npy cache Replace the disk-based .npy cache with a simpler approach: require the DEM to be a Cloud Optimized GeoTIFF (COG) when decimation is needed. COGs have internal tiled layout + overview pyramids so GDAL can read at any target resolution by seeking to the right bytes — no full-file scan. If the DEM is not a COG and is large enough to need decimation, throw a clear error with the gdal_translate command to convert it. Small DEMs that fit within max_working_dim are read directly regardless of format (no performance issue at small sizes). Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * fix: pass through Python error messages to frontend on sightmap failure When sightmap.py exits with code!=0, the Node handler was returning a generic 'sightmap computation failed' message. Now it tries to parse the structured JSON error from stdout first, so the actual error (e.g. 'DEM is not a COG') reaches the frontend. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * chore: remove timing debug instrumentation from sightmap Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * fix: sanitize COG error message — no paths or tracebacks in response Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * fix: remove traceback from JSON error response, log to stderr only Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * fix: show actual sightmap error message in toast - calls.api error callback now parses JSON response body from jqXHR - SightlineTool error handlers display server error message (e.g. COG) - No traceback or paths in error responses (security) Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * fix: remove duplicate 'sightmap error' prefix from Python error messages Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * perf(sightmap): temporarily disable multiprocessing for batch mode Run all timestamps sequentially in the main process so the DEM stays in memory and avoids pickle serialization overhead per worker. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * refactor(sightmap): remove multiprocessing and Resolution UI option - Remove all multiprocessing infrastructure (pool, workers, shared state) - Batch timestamps run sequentially in the main process - Remove Resolution dropdown from UI, default to ultra (800px static, 400px sweep) - Always auto-regenerate on map move (no resolution-gated cutoff) Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * perf: temporarily disable Numba JIT for benchmarking comparison Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * 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_sc…
* fix(SightlineTool): default sightline color ramp uses element color instead of black
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix(SightlineTool): remove vstBinaryLegend, default opacity 50%
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix(SightlineTool): crosshair redesign — unfilled circle with N/S/E/W tick lines
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* chore: bump version to 5.0.25-20260608 [version bump]
* fix(SightlineTool): full-size pixelated PNG export, mode-aware CSV/TXT, rename to Results
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix(SightlineTool): entity lowercase, ISO UTC times in exports, static TXT header fix, smaller crosshair
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* feat(SightlineTool): playback export as animated GIF with basemap overlay
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix(SightlineTool): header close button margin-right 6px
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix(SightlineTool): GIF export - capture full map container, guard against 0-size canvas
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix(SightlineTool): GIF export - hide UI controls, add timestamp/progress, lower resolution
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix(SightlineTool): GIF export - 720px res, UI progress indicator, remove in-frame progress bar
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix(SightlineTool): GIF progress reaches 100% only after encoding completes
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix(SightlineTool): rewrite CSV exports per mode, remove TXT for playback
- Static: entity,time,lat,lng,visible (single time, binary)
- Playback: entity,time,lat,lng,visible (per-pixel per-frame, each frame's timestamp)
- Composite: entity,start_time,end_time,lat,lng,percent_visible (time range, heatmap)
- Remove TXT grid export option for playback mode
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix(SightlineTool): reset export dropdown to first item on mode change
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* feat(blueprints): add SightlineTool to Lunar South Pole + new Mars reference mission
- Add time section and SightlineTool config to Lunar South Pole mission
- Create new Reference-Mission-Mars with LayersTool + SightlineTool (Sol observers)
- Reference Mission already has SightlineTool on development branch
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* chore: bump version to 5.0.26-20260608 [version bump]
* feat: register Mars variant in reference mission registries
Add Mars entry to REFERENCE_MISSION_VARIANTS in both:
- API/Backend/Utils/missionTemplates.js (backend)
- configure/src/.../NewMissionModal.js (frontend dropdown)
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* Add lunar ref mission dem data
* Add lunar ref mission dem data 2
* fix: sample bbox perimeter for tile bounds in polar projections
The bounding box clamping in SightlineTool and ViewshedTool
updateDesiredTiles() projected only two corner points of the bbox to
compute tile bounds. In polar stereographic projections (e.g. Lunar
South Pole IAU2000:30120), opposite corners of a lat/lon bbox can map
to the same pixel-space point, producing a degenerate or inverted tile
range — resulting in zero tiles fetched and no sightmap/viewshed
overlay rendered.
Fix: sample 9 evenly-spaced points along each of the four bbox edges
(36 points total) and take the pixel-space envelope. This correctly
handles any map projection.
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix: projection-aware azimuth lines and source positioning for polar CRS
- locateSource: skip wrapping for custom (non-wrapping) projections so
distant sources like Sun produce nearly-parallel rays instead of being
folded into the grid as a nearby point source
- AZ hover line: rotate by local north offset derived from the projection
so the bearing line points in the correct geographic direction
- Source azimuth lines: same north-offset correction
- Add _localNorthAngle() helper that computes screen-space north direction
at any lat/lng by projecting a small latitude offset
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix(SightlineTool): skip observer-centric curvature for distant sources
When the ray source (e.g. Sun) is far outside the DEM grid, the
observer-centric curvature correction in curveData() creates a
circular visibility artifact — the terrain bowl centered on the
observer falsely hides all cells beyond ~350 km.
Skip the curvature correction when dataSource is more than one
grid-width outside the data bounds. The shadow-plane propagation
already handles parallel-ray geometry correctly on the flat grid.
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* feat(sightline): add backend sightmap endpoint with per-cell ray-march
- New Python endpoint /api/sightmap computes solar illumination grids
server-side using SPICE for Sun position + DEM ray-marching
- Frontend calls backend instead of fetching tiles + JS shadow-plane algo
- USE_BACKEND_SIGHTMAP const switch to revert to old JS algorithm
- Returns both geographic and projected bounds for polar stereo CRS
- Sightmap overlay uses image-rendering: pixelated for crisp grid cells
- Auto-detects PROJ_DATA path in conda/mamba environments
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* chore: bump version to 5.0.27-20260609 [version bump]
* refactor: consolidate sightmap to backend-only, remove old JS algorithm
- Remove USE_BACKEND_SIGHTMAP flag and all old JS shadow-plane code
- Static path: single sightmap API call (no getbands/ll2aerll)
- Sweep path: batch sightmap API call with times array
- Remove tile-based rendering (makeDataLayer, renderResultToTileData,
atlas shader, _renderFrameCanvases, SightlineTool_Manager import)
- Rewrite renderHeatmapToMap for imageOverlay (no tile layer)
- Rewrite buildSweepAtlas to pre-render frames as dataURL images
- Rewrite sweepShowFrame to swap imageOverlay URL
- Update export functions for backend grid data structure
- Backend sightmap.py: batch mode (compute_sightmap_batch),
extracted _ray_march_grid and _compute_bounds helpers
- Express route: dynamic timeout for batch (N*30s, max 30min)
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* perf: vectorize ray-march with NumPy + coarse Sun az/el interpolation
Replace pure-Python nested loops in _ray_march_grid() with numpy array
operations. All output cells are now processed simultaneously at each
march step via broadcasting and fancy indexing.
Optimizations:
1. Vectorized ray march (#1): ~60x speedup on 259×259 grid (47s → 0.8s)
2. Coarse Sun az/el subgrid (#4): compute SPICE-equivalent az/el on a
10×10 subgrid and bilinearly interpolate to all output cells, reducing
per-cell coordinate transforms from 67K to ~400.
Helper functions added:
- _vectorized_is_nodata: array-based nodata detection
- _pixels_to_geo_batch: batch pixel→geographic via GDAL CoordinateTransformation
- _sun_azel_batch: pure-numpy geodetic Sun az/el (replaces spiceypy.georec per cell)
- _bilinear_interp_2d: pure-numpy 2D bilinear interpolation (no scipy needed)
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix: store raeRaw/raeAllResults so az/el indicator canvases redraw after mount
The SightlineResults.jsx useEffect triggers on el.raeRaw to redraw
indicator canvases. The static callback was calling updateRAEIndicators
directly (before React mounted the canvases) and only setting raeResults
but not raeRaw/raeAllResults, so the useEffect never fired.
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix: playback skydome/az-el blocked by stale atlas guard + pixelated CSS selector
- SightlineElement.jsx: replace ed?.atlas checks with ed?.frameImages
(atlas was the old WebGL texture atlas; refactored code uses frameImages)
- SightlineTool.css: add img.sightmap-pixelated selector (Leaflet applies
className directly to the <img>, not a wrapper div)
- python-environment.yml: add numba>=0.60.0 for JIT optimization
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* perf: Numba JIT + adaptive march + early cutoff + multiprocessing
sightmap.py performance optimizations on the vectorized ray-march:
1. Numba JIT (@numba.njit): compile inner march loop to native code
- 259x259 grid: 0.67s cold / 0.12s warm (was 0.79s numpy-only)
2. Adaptive march step: 4x step when margin > 5°, 2x when > 2°
- Reduces iterations for clearly-illuminated cells
3. Early cutoff: max shadow distance = MAX_TERRAIN_H / tan(el)
- Skips pointless marching when Sun is high
4. Multiprocessing for batch: Pool(ncpu) across timestamps
- 12 frames in 0.62s (52ms/frame) vs sequential
Overall: 47.1s → 0.12s per grid = ~380x speedup (warm cache)
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix: use Pool initializer for multiprocessing (Windows spawn compat)
On Windows/macOS, multiprocessing uses 'spawn' instead of 'fork',
so module-level globals aren't inherited by worker processes.
Fix: pass shared dict via Pool(initializer=_init_pool_worker, initargs=...)
so each worker sets _mp_shared in its own module namespace.
Tested: 72 timestamps in 1.28s (18ms/frame) on 100x100 grid.
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix: five bugs — az offset, playback opacity, progress indicator, mode switch, hover crash
1. sightmap.py: _compute_sun_grid used dem_rows instead of dem_cols for
column pixel clamping → coarse subgrid positions were wrong → az/el
off by ~30-45° on non-square DEMs
2. Playback/sweep overlay now inherits el.opacity as initial value
instead of hardcoded 1.0. Default sweep opacity changed to null;
applySweepOpacity falls back to el.opacity when ed.opacity unset
3. ProgressButton: added indeterminate mode (sliding bar animation)
that activates automatically when loading=true and progress<=0.
Static sightmap generation shows indeterminate; sweep shows %
4. switchElementMode: switching back to static now re-renders the
cached lastData/lastResultGrid or marks changed=true for auto-regen
5. _onCompositeHover: guard against missing topLeftTile (backend
sightmap response doesn't include tile-based fields)
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix: correct sightmap ray direction for polar stereographic projections
_compute_directions had two bugs in the projected CRS branch:
1. Missing grid convergence rotation: geographic azimuth was used
directly in pixel space without rotating by the convergence angle
(longitude for polar stereographic). This caused ~30-45° offset
at the observer's longitude.
2. Inverted dy sign: cos(az) was used for dy when gt[5]<0, but
increasing pixel-y = decreasing northing, so the formula must
negate cos(az) — matching the geographic branch convention.
Also fix _showAzimuthLine/_updateSourceAzimuthLines: the fallback
path (no sweepCenter or indicatorLastDragPoint) now computes
centerLatLng from the map container center so _localNorthAngle
always receives a valid position for the convergence rotation.
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* test: add extensive Playwright tests for sightmap API endpoint
Covers:
- Input validation (missing fields, non-finite numerics, NaN, Infinity)
- Path traversal protection on DEM path
- Single-timestamp sightmap computation (structure, grid values, az/el plausibility)
- Batch multi-timestamp computation (results array, az variation)
- Custom Az/El source (el=0 all shadow, el=90 all visible)
- Error handling (invalid SPICE target, nonexistent DEM)
- Consistency (determinism, different times produce different grids)
- Observer height offset comparison
- maxOutputDim clamping
- Projected bounds for polar stereographic CRS
All DEM-dependent tests gracefully skip when Lunar South Pole
mission is not available. AUTH=local mode returns HTML instead
of JSON — tests detect and skip.
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix(sightline): use observer position for azimuth indicator north offset
The azimuth indicator line was computing _localNorthAngle from the map
center (fallback when sweepCenter is unset), which drifts as the user
pans and may be at the south pole where longitude is ~0. The sightmap
shadows are computed from the actual observer position with correct
per-cell convergence, creating a mismatch.
Fix: store the observer lat/lng as sweepCenter in sweepElData when the
static sightmap completes (same as sweep mode already does). This
ensures _showAzimuthLine, _updateSourceAzimuthLines, the crosshair
placement, and the horizon profile all use the correct observer position
for the north angle calculation.
Also add indicatorLastDragPoint fallback to _updateSourceAzimuthLines
(was missing, unlike _showAzimuthLine which already had it).
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix(sightmap): correct azimuth CW/CCW convention in _sun_azel_batch
The east vector was computed as normal × north (= Up × N = -East = West),
causing atan2(dot(sun, west), dot(sun, north)) to return counter-clockwise
azimuths. This mirrored the result: geographic 240° CW reported as 120°.
Fix: use north × normal (= N × Up = East) per right-hand rule, matching
SPICE's azccw=False (clockwise from north) convention.
Verified: batch az now matches SPICE exactly (226.2493° vs 226.2493°).
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix(sightline): use fixed-width fade gradient for visibility segments
The gradient on segment transitions was 30% of the segment width,
making wide segments have long fades and narrow segments short fades.
Now uses a fixed 2% of the total bar width for all transitions,
producing uniform fade-in and fade-out lengths regardless of segment size.
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* feat(sightline): wire resolution setting to maxOutputDim + gifshot fixes
Resolution dropdown (Low/Med/High/Ultra) now controls the sightmap
grid size sent to the backend:
Static: 100 / 200 / 400 / 800 px
Sweep: 50 / 100 / 200 / 400 px
Also:
- Add willReadFrequently to GIF export canvas contexts (suppresses
Chrome warning about slow getImageData readback)
- Add gifshot progressCallback to update export progress during the
GIF encoding phase (90→100%) instead of jumping at the end
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* refactor(sightline): remove dead SightlineTool_Manager and Layer specific DEMs config
SightlineTool_Manager.js was part of the old JS tile-fetching algorithm
(now replaced by the backend sightmap.py endpoint). Nothing imports it.
Remove:
- SightlineTool_Manager.js
- 'Layer specific DEMs' config section (variables.data with demtileurl,
minZoom, maxNativeZoom, boundingBox) from config.json and all blueprint
mission configs
- vars.data validation in initialize() — replaced with vars.dem check
The Viewshed tool's separate Manager and its layer-level demtileurl
references are unaffected.
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* perf(sightmap): downsample large DEMs at read time via GDAL decimation
High-res DEMs (e.g. 100m vs 4000m) were read fully into memory even when
the output grid was small, causing:
1. Slow computation (full array I/O + march through many more pixels)
2. Windows pickle truncation on multiprocessing (huge arrays exceed
pickle buffer limits when serialized to spawn'd worker processes)
Fix: open_dem() now accepts max_working_dim and uses GDAL's ReadAsArray
with buf_xsize/buf_ysize for server-side bilinear decimation. The
geotransform is adjusted to match the resampled pixel grid. Working dim
is set to max(max_output_dim * 4, 1000) — 4x oversampling for terrain
detail in the ray march, capped at native resolution.
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* perf(sightmap): GDAL overview bands, DEM caching, Numba warmup, reduce working_dim
Three optimizations to cut sightmap generation time on large DEMs:
1. GDAL overview bands: If the DEM has pre-computed overviews (COGs),
read from the overview band directly instead of decimating the full
raster in memory. Falls back to ReadAsArray(buf_xsize/ysize) for
DEMs without overviews.
2. DEM caching: Module-level cache keyed by (path, working_dim) avoids
re-reading the same DEM within a batch run. LRU eviction at 4 entries.
3. Numba JIT warmup: Trigger compilation at module import with a tiny
2x2 dummy array so the ~5-10s first-compilation cost is paid during
startup, not during the actual sightmap computation.
4. Reduce working_dim from 4x to 2x output grid (min 500px instead of
1000px). Halves the march distance and array sizes while maintaining
sufficient terrain detail for shadow boundary accuracy.
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* debug: add timing instrumentation to sightmap.py
Temporary timing logs to stderr at each phase:
- imports, numba_warmup, load_kernels, spice_azel, unload_kernels
- open_dem (with working_dim and actual array size)
- _precompute_grid, _compute_sun_grid, _compute_directions
- _numba_march, total
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* debug: pipe sightmap stderr to Node console for timing visibility
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* debug: include timing data in sightmap JSON response body
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* perf(sightmap): disk-based .npy cache for decimated DEMs
The timing data showed open_dem takes 12.8s (GDAL reading/decimating a
large DEM) while the actual Numba march takes only 0.065s. Since each
sightmap call spawns a new Python process, the in-memory cache is lost.
Fix: after the first GDAL decimate, save the resulting numpy array and
geotransform metadata to .npy/.json files next to the source DEM.
Subsequent process invocations load the pre-decimated array via np.load
(~0.01s). Cache is invalidated if the source DEM file is newer.
Expected improvement: first run ~14s (unchanged), second+ runs ~2s.
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* perf(sightmap): require COG format for large DEMs, remove npy cache
Replace the disk-based .npy cache with a simpler approach: require the
DEM to be a Cloud Optimized GeoTIFF (COG) when decimation is needed.
COGs have internal tiled layout + overview pyramids so GDAL can read at
any target resolution by seeking to the right bytes — no full-file scan.
If the DEM is not a COG and is large enough to need decimation, throw a
clear error with the gdal_translate command to convert it.
Small DEMs that fit within max_working_dim are read directly regardless
of format (no performance issue at small sizes).
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix: pass through Python error messages to frontend on sightmap failure
When sightmap.py exits with code!=0, the Node handler was returning a
generic 'sightmap computation failed' message. Now it tries to parse
the structured JSON error from stdout first, so the actual error
(e.g. 'DEM is not a COG') reaches the frontend.
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* chore: remove timing debug instrumentation from sightmap
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix: sanitize COG error message — no paths or tracebacks in response
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix: remove traceback from JSON error response, log to stderr only
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix: show actual sightmap error message in toast
- calls.api error callback now parses JSON response body from jqXHR
- SightlineTool error handlers display server error message (e.g. COG)
- No traceback or paths in error responses (security)
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix: remove duplicate 'sightmap error' prefix from Python error messages
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* perf(sightmap): temporarily disable multiprocessing for batch mode
Run all timestamps sequentially in the main process so the DEM stays
in memory and avoids pickle serialization overhead per worker.
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* refactor(sightmap): remove multiprocessing and Resolution UI option
- Remove all multiprocessing infrastructure (pool, workers, shared state)
- Batch timestamps run sequentially in the main process
- Remove Resolution dropdown from UI, default to ultra (800px static, 400px sweep)
- Always auto-regenerate on map move (no resolution-gated cutoff)
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* perf: temporarily disable Numba JIT for benchmarking comparison
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* 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 gi…
postgis:18 stores data in a version specific subdir and expects the volume at /var/lib/postgresql, not /var/lib/postgresql/data. On a fresh install the old path makes the db container crash-loop, so mmgis never starts (container mmgis-db-1 is unhealthy). Left over from the postgres16 to18 upgrade (#935). Tested with down -v then up -d on a clean volume.
…t, and Config clone (#1001) * feat(blueprints): add SightlineTool to Lunar South Pole + new Mars reference mission - Add time section and SightlineTool config to Lunar South Pole mission - Create new Reference-Mission-Mars with LayersTool + SightlineTool (Sol observers) - Reference Mission already has SightlineTool on development branch Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * chore: bump version to 5.0.26-20260608 [version bump] * feat: register Mars variant in reference mission registries Add Mars entry to REFERENCE_MISSION_VARIANTS in both: - API/Backend/Utils/missionTemplates.js (backend) - configure/src/.../NewMissionModal.js (frontend dropdown) Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Add lunar ref mission dem data * Add lunar ref mission dem data 2 * fix: sample bbox perimeter for tile bounds in polar projections The bounding box clamping in SightlineTool and ViewshedTool updateDesiredTiles() projected only two corner points of the bbox to compute tile bounds. In polar stereographic projections (e.g. Lunar South Pole IAU2000:30120), opposite corners of a lat/lon bbox can map to the same pixel-space point, producing a degenerate or inverted tile range — resulting in zero tiles fetched and no sightmap/viewshed overlay rendered. Fix: sample 9 evenly-spaced points along each of the four bbox edges (36 points total) and take the pixel-space envelope. This correctly handles any map projection. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * fix: projection-aware azimuth lines and source positioning for polar CRS - locateSource: skip wrapping for custom (non-wrapping) projections so distant sources like Sun produce nearly-parallel rays instead of being folded into the grid as a nearby point source - AZ hover line: rotate by local north offset derived from the projection so the bearing line points in the correct geographic direction - Source azimuth lines: same north-offset correction - Add _localNorthAngle() helper that computes screen-space north direction at any lat/lng by projecting a small latitude offset Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * fix(SightlineTool): skip observer-centric curvature for distant sources When the ray source (e.g. Sun) is far outside the DEM grid, the observer-centric curvature correction in curveData() creates a circular visibility artifact — the terrain bowl centered on the observer falsely hides all cells beyond ~350 km. Skip the curvature correction when dataSource is more than one grid-width outside the data bounds. The shadow-plane propagation already handles parallel-ray geometry correctly on the flat grid. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * feat(sightline): add backend sightmap endpoint with per-cell ray-march - New Python endpoint /api/sightmap computes solar illumination grids server-side using SPICE for Sun position + DEM ray-marching - Frontend calls backend instead of fetching tiles + JS shadow-plane algo - USE_BACKEND_SIGHTMAP const switch to revert to old JS algorithm - Returns both geographic and projected bounds for polar stereo CRS - Sightmap overlay uses image-rendering: pixelated for crisp grid cells - Auto-detects PROJ_DATA path in conda/mamba environments Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * chore: bump version to 5.0.27-20260609 [version bump] * refactor: consolidate sightmap to backend-only, remove old JS algorithm - Remove USE_BACKEND_SIGHTMAP flag and all old JS shadow-plane code - Static path: single sightmap API call (no getbands/ll2aerll) - Sweep path: batch sightmap API call with times array - Remove tile-based rendering (makeDataLayer, renderResultToTileData, atlas shader, _renderFrameCanvases, SightlineTool_Manager import) - Rewrite renderHeatmapToMap for imageOverlay (no tile layer) - Rewrite buildSweepAtlas to pre-render frames as dataURL images - Rewrite sweepShowFrame to swap imageOverlay URL - Update export functions for backend grid data structure - Backend sightmap.py: batch mode (compute_sightmap_batch), extracted _ray_march_grid and _compute_bounds helpers - Express route: dynamic timeout for batch (N*30s, max 30min) Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * perf: vectorize ray-march with NumPy + coarse Sun az/el interpolation Replace pure-Python nested loops in _ray_march_grid() with numpy array operations. All output cells are now processed simultaneously at each march step via broadcasting and fancy indexing. Optimizations: 1. Vectorized ray march (#1): ~60x speedup on 259×259 grid (47s → 0.8s) 2. Coarse Sun az/el subgrid (#4): compute SPICE-equivalent az/el on a 10×10 subgrid and bilinearly interpolate to all output cells, reducing per-cell coordinate transforms from 67K to ~400. Helper functions added: - _vectorized_is_nodata: array-based nodata detection - _pixels_to_geo_batch: batch pixel→geographic via GDAL CoordinateTransformation - _sun_azel_batch: pure-numpy geodetic Sun az/el (replaces spiceypy.georec per cell) - _bilinear_interp_2d: pure-numpy 2D bilinear interpolation (no scipy needed) Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * fix: store raeRaw/raeAllResults so az/el indicator canvases redraw after mount The SightlineResults.jsx useEffect triggers on el.raeRaw to redraw indicator canvases. The static callback was calling updateRAEIndicators directly (before React mounted the canvases) and only setting raeResults but not raeRaw/raeAllResults, so the useEffect never fired. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * fix: playback skydome/az-el blocked by stale atlas guard + pixelated CSS selector - SightlineElement.jsx: replace ed?.atlas checks with ed?.frameImages (atlas was the old WebGL texture atlas; refactored code uses frameImages) - SightlineTool.css: add img.sightmap-pixelated selector (Leaflet applies className directly to the <img>, not a wrapper div) - python-environment.yml: add numba>=0.60.0 for JIT optimization Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * perf: Numba JIT + adaptive march + early cutoff + multiprocessing sightmap.py performance optimizations on the vectorized ray-march: 1. Numba JIT (@numba.njit): compile inner march loop to native code - 259x259 grid: 0.67s cold / 0.12s warm (was 0.79s numpy-only) 2. Adaptive march step: 4x step when margin > 5°, 2x when > 2° - Reduces iterations for clearly-illuminated cells 3. Early cutoff: max shadow distance = MAX_TERRAIN_H / tan(el) - Skips pointless marching when Sun is high 4. Multiprocessing for batch: Pool(ncpu) across timestamps - 12 frames in 0.62s (52ms/frame) vs sequential Overall: 47.1s → 0.12s per grid = ~380x speedup (warm cache) Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * fix: use Pool initializer for multiprocessing (Windows spawn compat) On Windows/macOS, multiprocessing uses 'spawn' instead of 'fork', so module-level globals aren't inherited by worker processes. Fix: pass shared dict via Pool(initializer=_init_pool_worker, initargs=...) so each worker sets _mp_shared in its own module namespace. Tested: 72 timestamps in 1.28s (18ms/frame) on 100x100 grid. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * fix: five bugs — az offset, playback opacity, progress indicator, mode switch, hover crash 1. sightmap.py: _compute_sun_grid used dem_rows instead of dem_cols for column pixel clamping → coarse subgrid positions were wrong → az/el off by ~30-45° on non-square DEMs 2. Playback/sweep overlay now inherits el.opacity as initial value instead of hardcoded 1.0. Default sweep opacity changed to null; applySweepOpacity falls back to el.opacity when ed.opacity unset 3. ProgressButton: added indeterminate mode (sliding bar animation) that activates automatically when loading=true and progress<=0. Static sightmap generation shows indeterminate; sweep shows % 4. switchElementMode: switching back to static now re-renders the cached lastData/lastResultGrid or marks changed=true for auto-regen 5. _onCompositeHover: guard against missing topLeftTile (backend sightmap response doesn't include tile-based fields) Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * fix: correct sightmap ray direction for polar stereographic projections _compute_directions had two bugs in the projected CRS branch: 1. Missing grid convergence rotation: geographic azimuth was used directly in pixel space without rotating by the convergence angle (longitude for polar stereographic). This caused ~30-45° offset at the observer's longitude. 2. Inverted dy sign: cos(az) was used for dy when gt[5]<0, but increasing pixel-y = decreasing northing, so the formula must negate cos(az) — matching the geographic branch convention. Also fix _showAzimuthLine/_updateSourceAzimuthLines: the fallback path (no sweepCenter or indicatorLastDragPoint) now computes centerLatLng from the map container center so _localNorthAngle always receives a valid position for the convergence rotation. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * test: add extensive Playwright tests for sightmap API endpoint Covers: - Input validation (missing fields, non-finite numerics, NaN, Infinity) - Path traversal protection on DEM path - Single-timestamp sightmap computation (structure, grid values, az/el plausibility) - Batch multi-timestamp computation (results array, az variation) - Custom Az/El source (el=0 all shadow, el=90 all visible) - Error handling (invalid SPICE target, nonexistent DEM) - Consistency (determinism, different times produce different grids) - Observer height offset comparison - maxOutputDim clamping - Projected bounds for polar stereographic CRS All DEM-dependent tests gracefully skip when Lunar South Pole mission is not available. AUTH=local mode returns HTML instead of JSON — tests detect and skip. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * fix(sightline): use observer position for azimuth indicator north offset The azimuth indicator line was computing _localNorthAngle from the map center (fallback when sweepCenter is unset), which drifts as the user pans and may be at the south pole where longitude is ~0. The sightmap shadows are computed from the actual observer position with correct per-cell convergence, creating a mismatch. Fix: store the observer lat/lng as sweepCenter in sweepElData when the static sightmap completes (same as sweep mode already does). This ensures _showAzimuthLine, _updateSourceAzimuthLines, the crosshair placement, and the horizon profile all use the correct observer position for the north angle calculation. Also add indicatorLastDragPoint fallback to _updateSourceAzimuthLines (was missing, unlike _showAzimuthLine which already had it). Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * fix(sightmap): correct azimuth CW/CCW convention in _sun_azel_batch The east vector was computed as normal × north (= Up × N = -East = West), causing atan2(dot(sun, west), dot(sun, north)) to return counter-clockwise azimuths. This mirrored the result: geographic 240° CW reported as 120°. Fix: use north × normal (= N × Up = East) per right-hand rule, matching SPICE's azccw=False (clockwise from north) convention. Verified: batch az now matches SPICE exactly (226.2493° vs 226.2493°). Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * fix(sightline): use fixed-width fade gradient for visibility segments The gradient on segment transitions was 30% of the segment width, making wide segments have long fades and narrow segments short fades. Now uses a fixed 2% of the total bar width for all transitions, producing uniform fade-in and fade-out lengths regardless of segment size. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * feat(sightline): wire resolution setting to maxOutputDim + gifshot fixes Resolution dropdown (Low/Med/High/Ultra) now controls the sightmap grid size sent to the backend: Static: 100 / 200 / 400 / 800 px Sweep: 50 / 100 / 200 / 400 px Also: - Add willReadFrequently to GIF export canvas contexts (suppresses Chrome warning about slow getImageData readback) - Add gifshot progressCallback to update export progress during the GIF encoding phase (90→100%) instead of jumping at the end Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * refactor(sightline): remove dead SightlineTool_Manager and Layer specific DEMs config SightlineTool_Manager.js was part of the old JS tile-fetching algorithm (now replaced by the backend sightmap.py endpoint). Nothing imports it. Remove: - SightlineTool_Manager.js - 'Layer specific DEMs' config section (variables.data with demtileurl, minZoom, maxNativeZoom, boundingBox) from config.json and all blueprint mission configs - vars.data validation in initialize() — replaced with vars.dem check The Viewshed tool's separate Manager and its layer-level demtileurl references are unaffected. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * perf(sightmap): downsample large DEMs at read time via GDAL decimation High-res DEMs (e.g. 100m vs 4000m) were read fully into memory even when the output grid was small, causing: 1. Slow computation (full array I/O + march through many more pixels) 2. Windows pickle truncation on multiprocessing (huge arrays exceed pickle buffer limits when serialized to spawn'd worker processes) Fix: open_dem() now accepts max_working_dim and uses GDAL's ReadAsArray with buf_xsize/buf_ysize for server-side bilinear decimation. The geotransform is adjusted to match the resampled pixel grid. Working dim is set to max(max_output_dim * 4, 1000) — 4x oversampling for terrain detail in the ray march, capped at native resolution. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * perf(sightmap): GDAL overview bands, DEM caching, Numba warmup, reduce working_dim Three optimizations to cut sightmap generation time on large DEMs: 1. GDAL overview bands: If the DEM has pre-computed overviews (COGs), read from the overview band directly instead of decimating the full raster in memory. Falls back to ReadAsArray(buf_xsize/ysize) for DEMs without overviews. 2. DEM caching: Module-level cache keyed by (path, working_dim) avoids re-reading the same DEM within a batch run. LRU eviction at 4 entries. 3. Numba JIT warmup: Trigger compilation at module import with a tiny 2x2 dummy array so the ~5-10s first-compilation cost is paid during startup, not during the actual sightmap computation. 4. Reduce working_dim from 4x to 2x output grid (min 500px instead of 1000px). Halves the march distance and array sizes while maintaining sufficient terrain detail for shadow boundary accuracy. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * debug: add timing instrumentation to sightmap.py Temporary timing logs to stderr at each phase: - imports, numba_warmup, load_kernels, spice_azel, unload_kernels - open_dem (with working_dim and actual array size) - _precompute_grid, _compute_sun_grid, _compute_directions - _numba_march, total Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * debug: pipe sightmap stderr to Node console for timing visibility Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * debug: include timing data in sightmap JSON response body Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * perf(sightmap): disk-based .npy cache for decimated DEMs The timing data showed open_dem takes 12.8s (GDAL reading/decimating a large DEM) while the actual Numba march takes only 0.065s. Since each sightmap call spawns a new Python process, the in-memory cache is lost. Fix: after the first GDAL decimate, save the resulting numpy array and geotransform metadata to .npy/.json files next to the source DEM. Subsequent process invocations load the pre-decimated array via np.load (~0.01s). Cache is invalidated if the source DEM file is newer. Expected improvement: first run ~14s (unchanged), second+ runs ~2s. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * perf(sightmap): require COG format for large DEMs, remove npy cache Replace the disk-based .npy cache with a simpler approach: require the DEM to be a Cloud Optimized GeoTIFF (COG) when decimation is needed. COGs have internal tiled layout + overview pyramids so GDAL can read at any target resolution by seeking to the right bytes — no full-file scan. If the DEM is not a COG and is large enough to need decimation, throw a clear error with the gdal_translate command to convert it. Small DEMs that fit within max_working_dim are read directly regardless of format (no performance issue at small sizes). Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * fix: pass through Python error messages to frontend on sightmap failure When sightmap.py exits with code!=0, the Node handler was returning a generic 'sightmap computation failed' message. Now it tries to parse the structured JSON error from stdout first, so the actual error (e.g. 'DEM is not a COG') reaches the frontend. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * chore: remove timing debug instrumentation from sightmap Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * fix: sanitize COG error message — no paths or tracebacks in response Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * fix: remove traceback from JSON error response, log to stderr only Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * fix: show actual sightmap error message in toast - calls.api error callback now parses JSON response body from jqXHR - SightlineTool error handlers display server error message (e.g. COG) - No traceback or paths in error responses (security) Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * fix: remove duplicate 'sightmap error' prefix from Python error messages Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * perf(sightmap): temporarily disable multiprocessing for batch mode Run all timestamps sequentially in the main process so the DEM stays in memory and avoids pickle serialization overhead per worker. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * refactor(sightmap): remove multiprocessing and Resolution UI option - Remove all multiprocessing infrastructure (pool, workers, shared state) - Batch timestamps run sequentially in the main process - Remove Resolution dropdown from UI, default to ultra (800px static, 400px sweep) - Always auto-regenerate on map move (no resolution-gated cutoff) Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * perf: temporarily disable Numba JIT for benchmarking comparison Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * 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…
…aming, relative paths (#1002) * fix: five bugs — az offset, playback opacity, progress indicator, mode switch, hover crash 1. sightmap.py: _compute_sun_grid used dem_rows instead of dem_cols for column pixel clamping → coarse subgrid positions were wrong → az/el off by ~30-45° on non-square DEMs 2. Playback/sweep overlay now inherits el.opacity as initial value instead of hardcoded 1.0. Default sweep opacity changed to null; applySweepOpacity falls back to el.opacity when ed.opacity unset 3. ProgressButton: added indeterminate mode (sliding bar animation) that activates automatically when loading=true and progress<=0. Static sightmap generation shows indeterminate; sweep shows % 4. switchElementMode: switching back to static now re-renders the cached lastData/lastResultGrid or marks changed=true for auto-regen 5. _onCompositeHover: guard against missing topLeftTile (backend sightmap response doesn't include tile-based fields) Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * fix: correct sightmap ray direction for polar stereographic projections _compute_directions had two bugs in the projected CRS branch: 1. Missing grid convergence rotation: geographic azimuth was used directly in pixel space without rotating by the convergence angle (longitude for polar stereographic). This caused ~30-45° offset at the observer's longitude. 2. Inverted dy sign: cos(az) was used for dy when gt[5]<0, but increasing pixel-y = decreasing northing, so the formula must negate cos(az) — matching the geographic branch convention. Also fix _showAzimuthLine/_updateSourceAzimuthLines: the fallback path (no sweepCenter or indicatorLastDragPoint) now computes centerLatLng from the map container center so _localNorthAngle always receives a valid position for the convergence rotation. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * test: add extensive Playwright tests for sightmap API endpoint Covers: - Input validation (missing fields, non-finite numerics, NaN, Infinity) - Path traversal protection on DEM path - Single-timestamp sightmap computation (structure, grid values, az/el plausibility) - Batch multi-timestamp computation (results array, az variation) - Custom Az/El source (el=0 all shadow, el=90 all visible) - Error handling (invalid SPICE target, nonexistent DEM) - Consistency (determinism, different times produce different grids) - Observer height offset comparison - maxOutputDim clamping - Projected bounds for polar stereographic CRS All DEM-dependent tests gracefully skip when Lunar South Pole mission is not available. AUTH=local mode returns HTML instead of JSON — tests detect and skip. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * fix(sightline): use observer position for azimuth indicator north offset The azimuth indicator line was computing _localNorthAngle from the map center (fallback when sweepCenter is unset), which drifts as the user pans and may be at the south pole where longitude is ~0. The sightmap shadows are computed from the actual observer position with correct per-cell convergence, creating a mismatch. Fix: store the observer lat/lng as sweepCenter in sweepElData when the static sightmap completes (same as sweep mode already does). This ensures _showAzimuthLine, _updateSourceAzimuthLines, the crosshair placement, and the horizon profile all use the correct observer position for the north angle calculation. Also add indicatorLastDragPoint fallback to _updateSourceAzimuthLines (was missing, unlike _showAzimuthLine which already had it). Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * fix(sightmap): correct azimuth CW/CCW convention in _sun_azel_batch The east vector was computed as normal × north (= Up × N = -East = West), causing atan2(dot(sun, west), dot(sun, north)) to return counter-clockwise azimuths. This mirrored the result: geographic 240° CW reported as 120°. Fix: use north × normal (= N × Up = East) per right-hand rule, matching SPICE's azccw=False (clockwise from north) convention. Verified: batch az now matches SPICE exactly (226.2493° vs 226.2493°). Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * fix(sightline): use fixed-width fade gradient for visibility segments The gradient on segment transitions was 30% of the segment width, making wide segments have long fades and narrow segments short fades. Now uses a fixed 2% of the total bar width for all transitions, producing uniform fade-in and fade-out lengths regardless of segment size. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * feat(sightline): wire resolution setting to maxOutputDim + gifshot fixes Resolution dropdown (Low/Med/High/Ultra) now controls the sightmap grid size sent to the backend: Static: 100 / 200 / 400 / 800 px Sweep: 50 / 100 / 200 / 400 px Also: - Add willReadFrequently to GIF export canvas contexts (suppresses Chrome warning about slow getImageData readback) - Add gifshot progressCallback to update export progress during the GIF encoding phase (90→100%) instead of jumping at the end Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * refactor(sightline): remove dead SightlineTool_Manager and Layer specific DEMs config SightlineTool_Manager.js was part of the old JS tile-fetching algorithm (now replaced by the backend sightmap.py endpoint). Nothing imports it. Remove: - SightlineTool_Manager.js - 'Layer specific DEMs' config section (variables.data with demtileurl, minZoom, maxNativeZoom, boundingBox) from config.json and all blueprint mission configs - vars.data validation in initialize() — replaced with vars.dem check The Viewshed tool's separate Manager and its layer-level demtileurl references are unaffected. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * perf(sightmap): downsample large DEMs at read time via GDAL decimation High-res DEMs (e.g. 100m vs 4000m) were read fully into memory even when the output grid was small, causing: 1. Slow computation (full array I/O + march through many more pixels) 2. Windows pickle truncation on multiprocessing (huge arrays exceed pickle buffer limits when serialized to spawn'd worker processes) Fix: open_dem() now accepts max_working_dim and uses GDAL's ReadAsArray with buf_xsize/buf_ysize for server-side bilinear decimation. The geotransform is adjusted to match the resampled pixel grid. Working dim is set to max(max_output_dim * 4, 1000) — 4x oversampling for terrain detail in the ray march, capped at native resolution. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * perf(sightmap): GDAL overview bands, DEM caching, Numba warmup, reduce working_dim Three optimizations to cut sightmap generation time on large DEMs: 1. GDAL overview bands: If the DEM has pre-computed overviews (COGs), read from the overview band directly instead of decimating the full raster in memory. Falls back to ReadAsArray(buf_xsize/ysize) for DEMs without overviews. 2. DEM caching: Module-level cache keyed by (path, working_dim) avoids re-reading the same DEM within a batch run. LRU eviction at 4 entries. 3. Numba JIT warmup: Trigger compilation at module import with a tiny 2x2 dummy array so the ~5-10s first-compilation cost is paid during startup, not during the actual sightmap computation. 4. Reduce working_dim from 4x to 2x output grid (min 500px instead of 1000px). Halves the march distance and array sizes while maintaining sufficient terrain detail for shadow boundary accuracy. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * debug: add timing instrumentation to sightmap.py Temporary timing logs to stderr at each phase: - imports, numba_warmup, load_kernels, spice_azel, unload_kernels - open_dem (with working_dim and actual array size) - _precompute_grid, _compute_sun_grid, _compute_directions - _numba_march, total Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * debug: pipe sightmap stderr to Node console for timing visibility Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * debug: include timing data in sightmap JSON response body Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * perf(sightmap): disk-based .npy cache for decimated DEMs The timing data showed open_dem takes 12.8s (GDAL reading/decimating a large DEM) while the actual Numba march takes only 0.065s. Since each sightmap call spawns a new Python process, the in-memory cache is lost. Fix: after the first GDAL decimate, save the resulting numpy array and geotransform metadata to .npy/.json files next to the source DEM. Subsequent process invocations load the pre-decimated array via np.load (~0.01s). Cache is invalidated if the source DEM file is newer. Expected improvement: first run ~14s (unchanged), second+ runs ~2s. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * perf(sightmap): require COG format for large DEMs, remove npy cache Replace the disk-based .npy cache with a simpler approach: require the DEM to be a Cloud Optimized GeoTIFF (COG) when decimation is needed. COGs have internal tiled layout + overview pyramids so GDAL can read at any target resolution by seeking to the right bytes — no full-file scan. If the DEM is not a COG and is large enough to need decimation, throw a clear error with the gdal_translate command to convert it. Small DEMs that fit within max_working_dim are read directly regardless of format (no performance issue at small sizes). Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * fix: pass through Python error messages to frontend on sightmap failure When sightmap.py exits with code!=0, the Node handler was returning a generic 'sightmap computation failed' message. Now it tries to parse the structured JSON error from stdout first, so the actual error (e.g. 'DEM is not a COG') reaches the frontend. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * chore: remove timing debug instrumentation from sightmap Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * fix: sanitize COG error message — no paths or tracebacks in response Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * fix: remove traceback from JSON error response, log to stderr only Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * fix: show actual sightmap error message in toast - calls.api error callback now parses JSON response body from jqXHR - SightlineTool error handlers display server error message (e.g. COG) - No traceback or paths in error responses (security) Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * fix: remove duplicate 'sightmap error' prefix from Python error messages Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * perf(sightmap): temporarily disable multiprocessing for batch mode Run all timestamps sequentially in the main process so the DEM stays in memory and avoids pickle serialization overhead per worker. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * refactor(sightmap): remove multiprocessing and Resolution UI option - Remove all multiprocessing infrastructure (pool, workers, shared state) - Batch timestamps run sequentially in the main process - Remove Resolution dropdown from UI, default to ultra (800px static, 400px sweep) - Always auto-regenerate on map move (no resolution-gated cutoff) Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * perf: temporarily disable Numba JIT for benchmarking comparison Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * 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.…
* fix(sightmap): correct azimuth CW/CCW convention in _sun_azel_batch
The east vector was computed as normal × north (= Up × N = -East = West),
causing atan2(dot(sun, west), dot(sun, north)) to return counter-clockwise
azimuths. This mirrored the result: geographic 240° CW reported as 120°.
Fix: use north × normal (= N × Up = East) per right-hand rule, matching
SPICE's azccw=False (clockwise from north) convention.
Verified: batch az now matches SPICE exactly (226.2493° vs 226.2493°).
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix(sightline): use fixed-width fade gradient for visibility segments
The gradient on segment transitions was 30% of the segment width,
making wide segments have long fades and narrow segments short fades.
Now uses a fixed 2% of the total bar width for all transitions,
producing uniform fade-in and fade-out lengths regardless of segment size.
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* feat(sightline): wire resolution setting to maxOutputDim + gifshot fixes
Resolution dropdown (Low/Med/High/Ultra) now controls the sightmap
grid size sent to the backend:
Static: 100 / 200 / 400 / 800 px
Sweep: 50 / 100 / 200 / 400 px
Also:
- Add willReadFrequently to GIF export canvas contexts (suppresses
Chrome warning about slow getImageData readback)
- Add gifshot progressCallback to update export progress during the
GIF encoding phase (90→100%) instead of jumping at the end
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* refactor(sightline): remove dead SightlineTool_Manager and Layer specific DEMs config
SightlineTool_Manager.js was part of the old JS tile-fetching algorithm
(now replaced by the backend sightmap.py endpoint). Nothing imports it.
Remove:
- SightlineTool_Manager.js
- 'Layer specific DEMs' config section (variables.data with demtileurl,
minZoom, maxNativeZoom, boundingBox) from config.json and all blueprint
mission configs
- vars.data validation in initialize() — replaced with vars.dem check
The Viewshed tool's separate Manager and its layer-level demtileurl
references are unaffected.
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* perf(sightmap): downsample large DEMs at read time via GDAL decimation
High-res DEMs (e.g. 100m vs 4000m) were read fully into memory even when
the output grid was small, causing:
1. Slow computation (full array I/O + march through many more pixels)
2. Windows pickle truncation on multiprocessing (huge arrays exceed
pickle buffer limits when serialized to spawn'd worker processes)
Fix: open_dem() now accepts max_working_dim and uses GDAL's ReadAsArray
with buf_xsize/buf_ysize for server-side bilinear decimation. The
geotransform is adjusted to match the resampled pixel grid. Working dim
is set to max(max_output_dim * 4, 1000) — 4x oversampling for terrain
detail in the ray march, capped at native resolution.
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* perf(sightmap): GDAL overview bands, DEM caching, Numba warmup, reduce working_dim
Three optimizations to cut sightmap generation time on large DEMs:
1. GDAL overview bands: If the DEM has pre-computed overviews (COGs),
read from the overview band directly instead of decimating the full
raster in memory. Falls back to ReadAsArray(buf_xsize/ysize) for
DEMs without overviews.
2. DEM caching: Module-level cache keyed by (path, working_dim) avoids
re-reading the same DEM within a batch run. LRU eviction at 4 entries.
3. Numba JIT warmup: Trigger compilation at module import with a tiny
2x2 dummy array so the ~5-10s first-compilation cost is paid during
startup, not during the actual sightmap computation.
4. Reduce working_dim from 4x to 2x output grid (min 500px instead of
1000px). Halves the march distance and array sizes while maintaining
sufficient terrain detail for shadow boundary accuracy.
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* debug: add timing instrumentation to sightmap.py
Temporary timing logs to stderr at each phase:
- imports, numba_warmup, load_kernels, spice_azel, unload_kernels
- open_dem (with working_dim and actual array size)
- _precompute_grid, _compute_sun_grid, _compute_directions
- _numba_march, total
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* debug: pipe sightmap stderr to Node console for timing visibility
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* debug: include timing data in sightmap JSON response body
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* perf(sightmap): disk-based .npy cache for decimated DEMs
The timing data showed open_dem takes 12.8s (GDAL reading/decimating a
large DEM) while the actual Numba march takes only 0.065s. Since each
sightmap call spawns a new Python process, the in-memory cache is lost.
Fix: after the first GDAL decimate, save the resulting numpy array and
geotransform metadata to .npy/.json files next to the source DEM.
Subsequent process invocations load the pre-decimated array via np.load
(~0.01s). Cache is invalidated if the source DEM file is newer.
Expected improvement: first run ~14s (unchanged), second+ runs ~2s.
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* perf(sightmap): require COG format for large DEMs, remove npy cache
Replace the disk-based .npy cache with a simpler approach: require the
DEM to be a Cloud Optimized GeoTIFF (COG) when decimation is needed.
COGs have internal tiled layout + overview pyramids so GDAL can read at
any target resolution by seeking to the right bytes — no full-file scan.
If the DEM is not a COG and is large enough to need decimation, throw a
clear error with the gdal_translate command to convert it.
Small DEMs that fit within max_working_dim are read directly regardless
of format (no performance issue at small sizes).
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix: pass through Python error messages to frontend on sightmap failure
When sightmap.py exits with code!=0, the Node handler was returning a
generic 'sightmap computation failed' message. Now it tries to parse
the structured JSON error from stdout first, so the actual error
(e.g. 'DEM is not a COG') reaches the frontend.
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* chore: remove timing debug instrumentation from sightmap
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix: sanitize COG error message — no paths or tracebacks in response
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix: remove traceback from JSON error response, log to stderr only
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix: show actual sightmap error message in toast
- calls.api error callback now parses JSON response body from jqXHR
- SightlineTool error handlers display server error message (e.g. COG)
- No traceback or paths in error responses (security)
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix: remove duplicate 'sightmap error' prefix from Python error messages
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* perf(sightmap): temporarily disable multiprocessing for batch mode
Run all timestamps sequentially in the main process so the DEM stays
in memory and avoids pickle serialization overhead per worker.
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* refactor(sightmap): remove multiprocessing and Resolution UI option
- Remove all multiprocessing infrastructure (pool, workers, shared state)
- Batch timestamps run sequentially in the main process
- Remove Resolution dropdown from UI, default to ultra (800px static, 400px sweep)
- Always auto-regenerate on map move (no resolution-gated cutoff)
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* perf: temporarily disable Numba JIT for benchmarking comparison
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* 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.m…
…inment (#1004) * fix: pass through Python error messages to frontend on sightmap failure When sightmap.py exits with code!=0, the Node handler was returning a generic 'sightmap computation failed' message. Now it tries to parse the structured JSON error from stdout first, so the actual error (e.g. 'DEM is not a COG') reaches the frontend. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * chore: remove timing debug instrumentation from sightmap Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * fix: sanitize COG error message — no paths or tracebacks in response Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * fix: remove traceback from JSON error response, log to stderr only Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * fix: show actual sightmap error message in toast - calls.api error callback now parses JSON response body from jqXHR - SightlineTool error handlers display server error message (e.g. COG) - No traceback or paths in error responses (security) Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * fix: remove duplicate 'sightmap error' prefix from Python error messages Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * perf(sightmap): temporarily disable multiprocessing for batch mode Run all timestamps sequentially in the main process so the DEM stays in memory and avoids pickle serialization overhead per worker. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * refactor(sightmap): remove multiprocessing and Resolution UI option - Remove all multiprocessing infrastructure (pool, workers, shared state) - Batch timestamps run sequentially in the main process - Remove Resolution dropdown from UI, default to ultra (800px static, 400px sweep) - Always auto-regenerate on map move (no resolution-gated cutoff) Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * perf: temporarily disable Numba JIT for benchmarking comparison Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * 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 ch…
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Sync fork's
developmentbranch with upstreamNASA-AMMOS/development.Notable upstream changes included:
Test plan