Skip to content

Migrate map from Leaflet to MapLibre GL JS with PMTiles#138

Merged
maorcc merged 18 commits intomainfrom
maor/maplibre-migration
Apr 11, 2026
Merged

Migrate map from Leaflet to MapLibre GL JS with PMTiles#138
maorcc merged 18 commits intomainfrom
maor/maplibre-migration

Conversation

@maorcc
Copy link
Copy Markdown
Owner

@maorcc maorcc commented Apr 7, 2026

Summary

  • Replace Leaflet + OSM raster tiles with MapLibre GL JS using self-hosted PMTiles (Middle East extract on Cloudflare R2) and a warm Protomaps basemap flavor with Hebrew labels
  • Alert polygons now use a GeoJSON source with `alerts-fill` / `alerts-hit` / `alerts-stroke` layers; polygon state drives data properties (`fillColor`, `fillOpacity`, etc.) instead of Leaflet `setStyle`
  • Leaflet popups replaced by a slide-in location panel (`#location-panel`) — bottom-sheet on mobile, sidebar on desktop
  • Country borders split into solid (non-disputed) and dashed (disputed) layers; road widths narrowed to 40%
  • `prediction-mode.js` migrated off Leaflet overlays (`L.polyline`, `L.polygon`, `map.removeLayer`) to a MapLibre GeoJSON source with three layers (`prediction-band`, `prediction-line`, `prediction-arrow`)
  • `ellipse-mode.js` fully migrated: replaced all Leaflet layers/markers with `maplibregl.Marker` and four dedicated GeoJSON sources (`ellipse-overlays`, `ellipse-visual`, `ellipse-editing`, `algc-overlay`); replaced `map.distance()` with haversine, `map.options.crs` with Web Mercator math, `L.DomEvent` with native listeners
  • `ensureOrefPoints` consolidated from duplicate extension-local copies into `AppState` as a shared singleton
  • Leaflet CDN removed entirely

Summary by CodeRabbit

  • New Features

    • Redesigned unified location panel (bottom-sheet on mobile / sidebar on desktop) with merged per-location event lists and synchronized scroll↔timeline seeking
    • Map rendering migrated to a modern vector stack with faster basemap tiles and more responsive overlays (ellipses, predictions, markers)
  • Documentation

    • Expanded architecture and setup guides, including basemap/tiles replacement and when to consult specific docs
  • Chores

    • Added screenshot/media paths to ignore list to reduce repo noise

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 7, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: fcade465-ea39-43d6-a0e0-1e5b98d96692

📥 Commits

Reviewing files that changed from the base of the PR and between 068166e and 9d76d47.

📒 Files selected for processing (3)
  • CLAUDE.md
  • docs/architecture.md
  • web/index.html
🚧 Files skipped from review as they are similar to previous changes (3)
  • docs/architecture.md
  • CLAUDE.md
  • web/index.html

📝 Walkthrough

Walkthrough

Replaces Leaflet with MapLibre GL JS and a self-hosted PMTiles basemap; switches runtime Voronoi/polygon computation to precomputed GeoJSON (locations_polygons.json); migrates ellipse and prediction overlays from Leaflet vectors to MapLibre GeoJSON sources; introduces a unified location panel and updates AppState APIs.

Changes

Cohort / File(s) Summary
Gitignore Configuration
\.gitignore
Added ignore patterns for media under docs/screenshots/ (exclude common image/video extensions).
Documentation
CLAUDE.md, docs/architecture.md, docs/ellipse-alg-C.md, docs/ellipse-feature.md
Updated docs to describe MapLibre + PMTiles basemap, precomputed locations_polygons.json, feature-state styling, MapLibre-based ellipse rendering, Web Mercator math, and procedures for PMTiles generation/upload.
Map & UI Shell
web/index.html
Replaced Leaflet with MapLibre GL JS, load precomputed polygon GeoJSON into a FeatureCollection (alerts-source), use feature-state styling, replace popups with a unified location panel UI, add panel/timeline synchronization, and extend window.AppState API (featureMap, ensureOrefPoints, openLocationPanel).
Ellipse Mode
web/ellipse-mode.js
Migrated from Leaflet vector layers to MapLibre GeoJSON sources/markers; replaced getLocationPolygons with getFeatureMap; swapped Leaflet CRS/distance calls for inline Web Mercator math and haversineMeters(); delegated oref points caching to AppState.
Prediction Mode
web/prediction-mode.js
Converted prediction overlays from Leaflet layers to a MapLibre GeoJSON prediction-source with layers; switched geometry inputs from Leaflet polygon objects to GeoJSON feature coordinates; moved to featureMap-driven clustering and delegated ensureOrefPoints.

Sequence Diagram

sequenceDiagram
    actor User
    participant Browser
    participant AppState
    participant MapLibre as MapLibre GL JS
    participant GeoJSON as GeoJSON Source

    User->>Browser: Open app
    Browser->>AppState: ensureOrefPoints()
    AppState->>Browser: load locations_polygons.json (featureMap)
    Browser->>GeoJSON: setData(FeatureCollection)
    GeoJSON->>MapLibre: add alerts-source + layers
    MapLibre->>User: render polygons

    User->>MapLibre: click polygon
    MapLibre->>Browser: click event -> selectedLocation
    Browser->>Browser: open unified panel
    Browser->>GeoJSON: update event list / setFeatureState()
    GeoJSON->>MapLibre: layer styling updated
    MapLibre->>User: refreshed polygon styling
Loading

Estimated Code Review Effort

🎯 4 (Complex) | ⏱️ ~80 minutes

Possibly Related Issues

Possibly Related PRs

Poem

🐰
I hopped from Leaflet to MapLibre's light,
Polygons pre-baked, PMTiles in sight—
GeoJSON trails and panels that slide,
Features with state, no runtime to bide.
Hop, click, view — the map feels right!

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 6.35% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The pull request title accurately summarizes the main change: migrating the map visualization stack from Leaflet to MapLibre GL JS with PMTiles, which aligns with the substantial refactoring across web components, documentation updates, and data pipeline changes.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch maor/maplibre-migration

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@maorcc maorcc force-pushed the maor/maplibre-migration branch from 101a646 to 102cf42 Compare April 7, 2026 10:51
maorcc added 10 commits April 8, 2026 21:54
Replace Leaflet + OSM raster tiles with MapLibre GL JS using self-hosted
PMTiles (Middle East extract on R2) and a warm Protomaps basemap flavor.
Alert polygons are now a GeoJSON source with fill/stroke/hit layers.
Popups replaced by a slide-in location panel (bottom-sheet on mobile,
sidebar on desktop). Prediction mode migrated off Leaflet overlays to
a MapLibre GeoJSON source. ensureOrefPoints consolidated into AppState.
- Replace Leaflet layers/markers with maplibregl.Marker and GeoJSON sources
- Add 4 GeoJSON sources (ellipse-overlays, ellipse-visual, ellipse-editing, algc-overlay) via setupLayers()
- Replace map.distance() calls with haversineMeters() helper
- Replace CRS project/unproject with Web Mercator math
- Replace L.DomEvent.* with native event listeners
- Replace map interaction handlers (dragging/scrollWheelZoom/...) with MapLibre equivalents
- Replace getLocationPolygons with getFeatureMap (GeoJSON feature objects)
- Replace polygonRings() to work with GeoJSON coordinates instead of Leaflet getLatLngs()
- Add featureBbox() for bbox intersection check replacing getBounds().intersects()
- Delegate ensureOrefPoints to window.AppState
- Register dblclick handlers for cluster overlay (→ algC) and algC overlay (→ clear) via setupEventHandlers()
- Add openLocationPanel to AppState for pin click callbacks
- Update architecture.md: replace Leaflet/Voronoi/d3-delaunay references with MapLibre GL JS + PMTiles + pre-computed GeoJSON polygons
- Update CLAUDE.md project description
- Update ellipse-feature.md: locationPolygons → featureMap (GeoJSON), Web Mercator projection note
- Update ellipse-alg-C.md: remove Leaflet geometry reference
- Extend maxBounds south to lat 11.0 (was 24.0) to cover Yemen
Road narrowing, border filters, and sub-regional boundary visibility
are now applied to the Protomaps layer list before MapLibre is
initialized, instead of patching post-load with setPaintProperty.
Add future basemap style tweaks to customizeBasemapLayers().
@maorcc maorcc force-pushed the maor/maplibre-migration branch from 95a39a5 to 9f143cd Compare April 9, 2026 19:38
maorcc added 7 commits April 10, 2026 00:44
- Raise panel z-index above menu buttons (1000 → 1010)
- Highlight selected polygon when panel opens
- Make location panel non-modal so map stays pannable
- Fix star toggle closing panel (stopPropagation on click)
- Fix reconstructStateAt ordering for panel highlight cleanup
- Add maxTileCacheZoomLevels and fadeDuration for snappier rendering
- Reduce playback gap skip delay (1000ms → 300ms)
During timeline playback, reconstructStateAt() was calling
source.setData() with all ~1,400 features at ~60fps. MapLibre's web
worker coalesced updates, causing intermittent missing polygon colors.

setFeatureState() is synchronous and bypasses the web worker, fixing
the rendering lag. All polygon style mutations now go through
setFeatureState/removeFeatureState instead of mutating GeoJSON
properties. updateMapLibreSource() is now only called once during
initial polygon load.
Display all location polygon outlines in light gray (#aaaaaa) at 25%
opacity, giving spatial context even when no alerts are active.
Switch from rate-limited pub-*.r2.dev to tiles.oref-map.org.
Polling was starting before buildPolygons() populated nameToId,
so setLocationState() silently dropped all alerts until the next
history poll 10s later. Now polling starts only after polygons are
built via a callback from buildPolygons().
- Three states: closed (trigger button), slim (timeline controls), full
  (event list + timeline). Desktop: right sidebar. Mobile: bottom sheet.
- Event list shows chronological alerts for selected location from R2
  day-history, with current-time indicator at ~75% scroll position.
- Bidirectional sync: slider scrolls event list; scrolling event list
  seeks timeline (with _scrollSyncLock to prevent loops).
- Desktop slim/full share same right-edge position with animated height
  transition (slim→full only; full→slim is instant to avoid confusion).
- Escape key: Full→Slim→Closed. Arrow keys: prev/next event.
- Clicking selected polygon deselects and collapses to slim.
- Blue outline + floating label on selected polygon for clear map link.
- Selecting a location no longer changes the viewed time (scroll lock
  held through CSS transition; all renderEventList() calls guarded).
- Selected polygon now correctly reflects map state when seeking
  (reconstructStateAt no longer restores stale saved highlight state).
- Mobile slim panel spans full width at bottom, consistent with full.
@maorcc maorcc marked this pull request as ready for review April 11, 2026 11:22
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 7

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
web/prediction-mode.js (1)

221-390: ⚠️ Potential issue | 🟡 Minor

Handle ensureOrefPoints() failures by clearing the overlay.

A rejected load currently leaves the previous prediction geometry in place and surfaces an unhandled promise. On a transient oref_points.json failure, users keep seeing stale launch vectors instead of the overlay disappearing.

🛡️ Suggested fix
-      });
+      }).catch(function(err) {
+        console.error('Failed to update prediction overlay:', err);
+        clearPredictionLines();
+      });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/prediction-mode.js` around lines 221 - 390, The A.ensureOrefPoints()
promise chain lacks rejection handling, leaving stale prediction geometry on
failure; catch rejections from A.ensureOrefPoints() and in the catch handler
call clearPredictionLines(), set predictionFeatures to an empty array (or
equivalent cleared state) and call updatePredictionSource() to remove the
overlay and avoid an unhandled promise rejection; locate the
A.ensureOrefPoints() call and add the .catch handler that references
clearPredictionLines(), predictionFeatures, and updatePredictionSource().
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@docs/architecture.md`:
- Around line 130-149: The markdownlint errors come from an unlabeled fenced
code block containing the PMTiles URL and missing blank lines around the
"Current file coverage" table; update the fenced block that contains
"https://pub-0cb002f302e94002b76aa0bc30eb8763.r2.dev/middle-east.pmtiles" to
include a language (e.g., text) and ensure there is a blank line before the
"**Current file coverage:**" heading and another blank line after the table so
the table is properly separated from surrounding text.

In `@web/ellipse-mode.js`:
- Around line 1847-1852: When initEllipse() can run before the MapLibre 'load'
event, setEnabled(ellipseEnabled) calls controller.sync(true) too early and
flushOverlaySource() no-ops because the overlay sources (ellipse-overlays,
ellipse-visual, ellipse-editing, algc-overlay) are not yet created; fix by
re-syncing after deferred layer initialization: in the map.once('load', ...)
handler that calls setupLayers() and setupEventHandlers(), also call
controller.sync(true) (or call setEnabled(ellipseEnabled) again) to force
flushOverlaySource() once the sources exist so overlays populate correctly;
locate initEllipse, setEnabled, controller.sync, flushOverlaySource, and the
map.once('load'...) block to implement this re-sync.

In `@web/index.html`:
- Around line 863-894: Replace the mouse-only interactive elements (`#up-handle`,
`#up-close-row`, `#up-fav-star` and each child within `#up-event-list`) with real
<button> elements or give them proper button semantics (role="button" and
tabindex="0") and add keyboard handlers that trigger the same actions on Enter
and Space; ensure the favorite star exposes aria-pressed or aria-checked to
reflect state and provide an accessible label, and make the close control and
timeline controls focusable with appropriate aria-label/title attributes and
visible focus styles so all interactions work with keyboard and screen readers.
- Around line 2383-2386: The popup creation currently injects unescaped external
data by concatenating name into setHTML (see _panelLabelPopup and use of
maplibregl.Popup.setHTML), risking XSS; change to build DOM nodes and assign
textContent (or use Popup.setDOMContent) instead of setHTML — e.g., create a
container element, append a bold element with its textContent set to name (and
similarly for label in other occurrences noted at 2444-2465 and 4200-4225), then
pass that node to setDOMContent/addTo(map) so no raw HTML from the API is
injected.
- Around line 2368-2403: _removePanelHighlight() currently blindly reapplies the
old _panelHighlightSaved snapshot captured in _applyPanelHighlight(), which can
overwrite newer styling updated by setLocationState(), removeLocation(),
fadeGreenMarkers(), or timeline rebuilds; instead, stop restoring that stale
snapshot—either maintain highlight-specific feature state (e.g.,
setFeatureState(..., { highlight: true }) and only remove that key) or recompute
the feature's current base style from the authoritative source (locationStates)
and call map.setFeatureState(...) with the recomputed base properties before
removing the highlight; update _applyPanelHighlight, _removePanelHighlight, and
any code that reads/writes _panelHighlightSaved to use the new highlight state
key or recomputed base style, and ensure nameToId and map.setFeatureState calls
target only the highlight-specific properties so newer alert/all-clear state is
never overwritten.
- Around line 3912-3920: The loop that computes evIdx compares
_locationEventList[ei].ts to newTime but buildLocationEventList() populates
alertDate (not ts), so evIdx never advances; update the comparison in the loop
to use _locationEventList[ei].alertDate (and coerce it to the same time type as
newTime if needed) so evIdx changes correctly and the throttle (checking evIdx
!== _currentEventIdx before calling syncScrollToTime) works as intended;
references: selectedLocation, _locationEventList, buildLocationEventList, evIdx,
_currentEventIdx, syncScrollToTime, newTime.

In `@web/prediction-mode.js`:
- Around line 449-453: The replay logic currently drops buffered
predictionFeatures when setupLayers() later creates the source because
updatePredictionSource() is ignored if the source is missing; modify
setupLayers() (the code that creates the prediction GeoJSON source) to, after
creating the source, replay any buffered predictionFeatures/populate
predictionFeatures and call updatePredictionSource() so previously buffered
geometry from sync()/updatePredictionLines() is flushed; also add error handling
to the async chain started at ensureOrefPoints() (the promise beginning at line
~221) by attaching a .catch() that logs the error and clears or resets any stale
prediction geometry/state to avoid unhandled rejections and stale overlays.

---

Outside diff comments:
In `@web/prediction-mode.js`:
- Around line 221-390: The A.ensureOrefPoints() promise chain lacks rejection
handling, leaving stale prediction geometry on failure; catch rejections from
A.ensureOrefPoints() and in the catch handler call clearPredictionLines(), set
predictionFeatures to an empty array (or equivalent cleared state) and call
updatePredictionSource() to remove the overlay and avoid an unhandled promise
rejection; locate the A.ensureOrefPoints() call and add the .catch handler that
references clearPredictionLines(), predictionFeatures, and
updatePredictionSource().
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: eb21a058-0621-4da1-9c31-f7a026b25edf

📥 Commits

Reviewing files that changed from the base of the PR and between c4872e7 and 068166e.

📒 Files selected for processing (8)
  • .gitignore
  • CLAUDE.md
  • docs/architecture.md
  • docs/ellipse-alg-C.md
  • docs/ellipse-feature.md
  • web/ellipse-mode.js
  • web/index.html
  • web/prediction-mode.js

Comment on lines +1847 to +1852
} else {
map.once('load', function() {
setupLayers();
setupEventHandlers();
});
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

wc -l web/ellipse-mode.js

Repository: maorcc/oref-map

Length of output: 83


🏁 Script executed:

sed -n '1840,1860p' web/ellipse-mode.js

Repository: maorcc/oref-map

Length of output: 561


🏁 Script executed:

sed -n '2080,2100p' web/ellipse-mode.js

Repository: maorcc/oref-map

Length of output: 729


🏁 Script executed:

grep -n "setupLayers" web/ellipse-mode.js | head -20

Repository: maorcc/oref-map

Length of output: 146


🏁 Script executed:

grep -n "function setupLayers" web/ellipse-mode.js

Repository: maorcc/oref-map

Length of output: 92


🏁 Script executed:

sed -n '1719,1810p' web/ellipse-mode.js

Repository: maorcc/oref-map

Length of output: 2968


🏁 Script executed:

grep -n "function setEnabled" web/ellipse-mode.js

Repository: maorcc/oref-map

Length of output: 149


🏁 Script executed:

grep -n "\.sync\(" web/ellipse-mode.js

Repository: maorcc/oref-map

Length of output: 82


🏁 Script executed:

sed -n '2050,2080p' web/ellipse-mode.js

Repository: maorcc/oref-map

Length of output: 1116


🏁 Script executed:

grep -n "controller\.sync" web/ellipse-mode.js

Repository: maorcc/oref-map

Length of output: 93


🏁 Script executed:

grep -n "\.sync" web/ellipse-mode.js | head -20

Repository: maorcc/oref-map

Length of output: 93


🏁 Script executed:

grep -n "controller\.setEnabled" web/ellipse-mode.js

Repository: maorcc/oref-map

Length of output: 166


🏁 Script executed:

grep -n "controller =" web/ellipse-mode.js

Repository: maorcc/oref-map

Length of output: 104


🏁 Script executed:

grep -n "var controller" web/ellipse-mode.js

Repository: maorcc/oref-map

Length of output: 104


🏁 Script executed:

sed -n '1600,1680p' web/ellipse-mode.js

Repository: maorcc/oref-map

Length of output: 3220


🏁 Script executed:

grep -n "function createController" web/ellipse-mode.js

Repository: maorcc/oref-map

Length of output: 93


🏁 Script executed:

grep -n "createController\s*=" web/ellipse-mode.js

Repository: maorcc/oref-map

Length of output: 41


🏁 Script executed:

grep -n "createController" web/ellipse-mode.js

Repository: maorcc/oref-map

Length of output: 233


🏁 Script executed:

sed -n '14,200p' web/ellipse-mode.js

Repository: maorcc/oref-map

Length of output: 7297


🏁 Script executed:

sed -n '1678,1720p' web/ellipse-mode.js

Repository: maorcc/oref-map

Length of output: 1502


🏁 Script executed:

grep -n "function drawEllipseOverlays" web/ellipse-mode.js

Repository: maorcc/oref-map

Length of output: 119


🏁 Script executed:

sed -n '1500,1530p' web/ellipse-mode.js

Repository: maorcc/oref-map

Length of output: 1179


🏁 Script executed:

sed -n '927,1000p' web/ellipse-mode.js

Repository: maorcc/oref-map

Length of output: 2988


🏁 Script executed:

sed -n '1840,1870p' web/ellipse-mode.js

Repository: maorcc/oref-map

Length of output: 931


🏁 Script executed:

sed -n '919,922p' web/ellipse-mode.js

Repository: maorcc/oref-map

Length of output: 122


🏁 Script executed:

sed -n '2085,2095p' web/ellipse-mode.js

Repository: maorcc/oref-map

Length of output: 325


🏁 Script executed:

sed -n '100,110p' web/ellipse-mode.js

Repository: maorcc/oref-map

Length of output: 427


Re-sync ellipse state after deferred layer initialization.

When oref-ellipse-mode is already enabled and initEllipse() runs before MapLibre's load event, setEnabled(ellipseEnabled) at line 2088 calls controller.sync(true) before the sources ellipse-overlays, ellipse-visual, ellipse-editing, and algc-overlay are created. Since the sources don't exist yet, flushOverlaySource() silently no-ops, leaving overlays blank until another app event triggers a resync.

🔁 Suggested fix
    if (map.loaded()) {
      setupLayers();
      setupEventHandlers();
    } else {
      map.once('load', function() {
        setupLayers();
        setupEventHandlers();
+       if (controller.isEnabled()) controller.sync(true);
      });
    }

Also applies to: 2088-2088

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/ellipse-mode.js` around lines 1847 - 1852, When initEllipse() can run
before the MapLibre 'load' event, setEnabled(ellipseEnabled) calls
controller.sync(true) too early and flushOverlaySource() no-ops because the
overlay sources (ellipse-overlays, ellipse-visual, ellipse-editing,
algc-overlay) are not yet created; fix by re-syncing after deferred layer
initialization: in the map.once('load', ...) handler that calls setupLayers()
and setupEventHandlers(), also call controller.sync(true) (or call
setEnabled(ellipseEnabled) again) to force flushOverlaySource() once the sources
exist so overlays populate correctly; locate initEllipse, setEnabled,
controller.sync, flushOverlaySource, and the map.once('load'...) block to
implement this re-sync.

Comment on lines +863 to 894
<div id="unified-panel">
<div id="up-handle"></div>
<div id="up-location">
<div id="up-location-header">
<button id="up-location-close" aria-label="סגור">&times;</button>
<b id="up-location-name"></b>
<span id="up-fav-star"></span>
</div>
<div id="up-event-list"></div>
</div>
<div id="up-timeline">
<div id="timeline-slider-wrap">
<div id="timeline-ticks"></div>
<input type="range" id="timeline-slider" min="0" max="999" value="999" step="1">
</div>
<div id="timeline-date-row">
<span id="timeline-label"></span>
<div id="timeline-date-nav">
<button id="tl-date-prev" title="יום קודם">&#9666;</button>
<span id="tl-date-label"></span>
<button id="tl-date-next" title="יום הבא">&#9656;</button>
</div>
</div>
<div id="timeline-transport">
<button id="tl-prev" title="אירוע קודם">⏮</button>
<button id="tl-play" title="נגן/עצור">▶</button>
<button id="tl-next" title="אירוע הבא">⏭</button>
</div>
<div id="up-close-row">
<span class="btn-icon"><svg width="18" height="18" viewBox="0 0 100 100" style="vertical-align:middle"><path d="M48,8 L26,26 L48,44 L48,34 C62,34 74,46 74,60 C74,74 62,86 48,86 C38,86 30,80 26,72 L18,76 C24,88 35,96 48,96 C68,96 84,80 84,60 C84,40 68,24 48,24 L48,8Z" fill="#A1C2FA" stroke="#333" stroke-width="3" stroke-linejoin="round"/><circle cx="48" cy="60" r="26" fill="white" stroke="#333" stroke-width="3"/><g stroke="#333" stroke-width="4" stroke-linecap="round"><line x1="48" y1="60" x2="38" y2="52"/><line x1="48" y1="60" x2="32" y2="68"/></g><circle cx="48" cy="60" r="3.5" fill="#A1C2FA" stroke="#333" stroke-width="2"/><g stroke="#333" stroke-width="2.5" stroke-linecap="round"><line x1="48" y1="39" x2="48" y2="43"/><line x1="69" y1="60" x2="65" y2="60"/><line x1="48" y1="81" x2="48" y2="77"/><line x1="27" y1="60" x2="31" y2="60"/></g></svg></span><span class="btn-text">התרעות קודמות</span><span class="tl-close">✕</span>
</div>
</div>
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

The new panel controls are mouse-only right now.

#up-handle, #up-close-row, the favorite star, and each event row are rendered as plain div/span elements and only receive click handlers, so they are not tabbable or keyboard-operable. These need real buttons, or at least button semantics plus Enter/Space handlers, before shipping.

Also applies to: 2460-2463, 3999-4047

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/index.html` around lines 863 - 894, Replace the mouse-only interactive
elements (`#up-handle`, `#up-close-row`, `#up-fav-star` and each child within
`#up-event-list`) with real <button> elements or give them proper button semantics
(role="button" and tabindex="0") and add keyboard handlers that trigger the same
actions on Enter and Space; ensure the favorite star exposes aria-pressed or
aria-checked to reflect state and provide an accessible label, and make the
close control and timeline controls focusable with appropriate aria-label/title
attributes and visible focus styles so all interactions work with keyboard and
screen readers.

Comment on lines +449 to +453
if (map.loaded()) {
setupLayers();
} else {
map.once('load', setupLayers);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, let's check the file structure and see the key sections mentioned
fd -t f prediction-mode.js

Repository: maorcc/oref-map

Length of output: 81


🏁 Script executed:

# Read the relevant sections of the file
cat -n web/prediction-mode.js | sed -n '220,230p;420,480p'

Repository: maorcc/oref-map

Length of output: 2851


🏁 Script executed:

# Get a broader view of the function definitions
cat -n web/prediction-mode.js | sed -n '1,50p'

Repository: maorcc/oref-map

Length of output: 2029


🏁 Script executed:

# Search for sync function definition
rg -n "function sync\(" web/prediction-mode.js -A 20

Repository: maorcc/oref-map

Length of output: 706


🏁 Script executed:

# Search for updatePredictionSource function definition
rg -n "function updatePredictionSource\(" web/prediction-mode.js -A 15

Repository: maorcc/oref-map

Length of output: 782


🏁 Script executed:

# Check the end of the ensureOrefPoints promise chain to see if it has .catch()
cat -n web/prediction-mode.js | sed -n '220,390p'

Repository: maorcc/oref-map

Length of output: 8797


🏁 Script executed:

# Find schedulePredictionUpdate function
rg -n "function schedulePredictionUpdate\(" web/prediction-mode.js -A 15

Repository: maorcc/oref-map

Length of output: 534


🏁 Script executed:

# Also check what updatePredictionLines does since it's likely called by schedulePredictionUpdate
rg -n "function updatePredictionLines\(" web/prediction-mode.js -A 5

Repository: maorcc/oref-map

Length of output: 200


🏁 Script executed:

# Check if there's a .catch() on the ensureOrefPoints promise
cat -n web/prediction-mode.js | sed -n '215,395p' | grep -A 5 "\.catch\|\.finally"

Repository: maorcc/oref-map

Length of output: 41


🏁 Script executed:

# Let's see the full context around line 390 to confirm what happens after updatePredictionSource
cat -n web/prediction-mode.js | sed -n '385,405p'

Repository: maorcc/oref-map

Length of output: 672


Replay buffered prediction state after the source is created, and add error handling to the async prediction chain.

If oref-predict is enabled and this module initializes before MapLibre fires load, the call to sync() at line 470 can trigger updatePredictionLines() immediately. This async function populates predictionFeatures and calls updatePredictionSource() at line 389, but the source doesn't exist yet, so the update is silently ignored (line 161-162 checks if (src)). When setupLayers() later creates the source with an empty collection, the buffered features are never flushed, leaving the overlay blank until the next app:stateChanged event.

Additionally, the promise chain starting at line 221 has no .catch() handler, leaving unhandled rejections and stale prediction geometry if ensureOrefPoints() fails.

Suggested fix
     function setupLayers() {
+      if (map.getSource('prediction-source')) return;
       map.addSource('prediction-source', {
         type: 'geojson',
         data: { type: 'FeatureCollection', features: [] }
       });
       map.addLayer({
         id: 'prediction-band',
         type: 'fill',
         source: 'prediction-source',
         filter: ['==', ['get', 'kind'], 'band'],
         paint: { 'fill-color': '#ff4444', 'fill-opacity': 0.1 }
       });
       map.addLayer({
         id: 'prediction-line',
         type: 'line',
         source: 'prediction-source',
         filter: ['==', ['get', 'kind'], 'line'],
         paint: { 'line-color': '#ff4444', 'line-width': 2.5, 'line-opacity': 0.7, 'line-dasharray': [10, 8] }
       });
       map.addLayer({
         id: 'prediction-arrow',
         type: 'line',
         source: 'prediction-source',
         filter: ['==', ['get', 'kind'], 'arrow'],
         paint: { 'line-color': '#ff4444', 'line-width': 2.5, 'line-opacity': 0.7 }
       });
+
+      updatePredictionSource();
+      if (enabled) schedulePredictionUpdate();
     }

Also add error handling to line 221:

-      A.ensureOrefPoints().then(function(orefPts) {
+      A.ensureOrefPoints().then(function(orefPts) {
         // ... existing code ...
         predictionFeatures = newFeatures;
         updatePredictionSource();
-      });
+      }).catch(function(err) {
+        clearPredictionLines();
+        console.error('Prediction update failed:', err);
+      });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/prediction-mode.js` around lines 449 - 453, The replay logic currently
drops buffered predictionFeatures when setupLayers() later creates the source
because updatePredictionSource() is ignored if the source is missing; modify
setupLayers() (the code that creates the prediction GeoJSON source) to, after
creating the source, replay any buffered predictionFeatures/populate
predictionFeatures and call updatePredictionSource() so previously buffered
geometry from sync()/updatePredictionLines() is flushed; also add error handling
to the async chain started at ensureOrefPoints() (the promise beginning at line
~221) by attaching a .catch() that logs the error and clears or resets any stale
prediction geometry/state to avoid unhandled rejections and stale overlays.

- Fix slider bands missing when opening panel via location click directly
  (was calling openSlimPanel bypassing rebuildTimeline; now calls openTimelineFn)
- Fix map label popup leaking on each slider drag
  (applyPanelHighlight now removes existing popup before creating a new one)
- Fix stale highlight restore in _removePanelHighlight: recompute style from
  locationStates instead of saved snapshot (snapshot was stale after seekTo)
- Fix .ts → .alertDate in playback scroll-sync comparison
- Fix XSS: replace setHTML/innerHTML with DOM element creation in renderEventList
  and _applyPanelHighlight
- Panel background #f7f7f5 + env(safe-area-inset-bottom) to distinguish from
  Android nav bar
- RTL header layout: star next to name, X on the left
- docs/architecture.md: add language tags to bare code fences, blank line before table
@maorcc maorcc self-assigned this Apr 11, 2026
@maorcc maorcc merged commit a4189df into main Apr 11, 2026
1 check passed
@maorcc maorcc deleted the maor/maplibre-migration branch April 11, 2026 12:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant