AOI: move the analyze/cancel popup onto the core popup service - #309
Closed
CarsonDavis wants to merge 29 commits into
Closed
AOI: move the analyze/cancel popup onto the core popup service#309CarsonDavis wants to merge 29 commits into
CarsonDavis wants to merge 29 commits into
Conversation
CarsonDavis
force-pushed
the
feature/299-aoi-popup-migration
branch
from
August 11, 2026 22:17
b5e1c36 to
e59635c
Compare
CarsonDavis
force-pushed
the
feature/298-map-popup-service
branch
from
August 14, 2026 17:14
9c72e45 to
cb13772
Compare
CarsonDavis
force-pushed
the
feature/299-aoi-popup-migration
branch
6 times, most recently
from
August 19, 2026 16:15
a0216b2 to
cc4ed63
Compare
Collaborator
Author
|
Follow-up carried over from #303's body (trimmed out of it during review): Overlay retirement — |
AOI now asks core for its popup with one serializable request and listens for the button events on the bus, so the plugin carries no popup DOM, no popup CSS and no callback across the core boundary. The Cancel button's event and the popup's dismissEvent are both plugin:aoi:drawingCancelled, so core's broadcast replaces the plugin's own emit and the X and a click away clear the selection like Cancel does. A single _pendingPopup cancel replaces the untracked moveend one-shot and its fallback timer, cancelled on a superseding selection and on destroy so a closed tool can never leave a popup behind. It is armed before the camera is read rather than after: reading the view is asynchronous, and a teardown or a superseding selection during that hop has to drop this popup instead of letting it open later. Every path that opens the popup first checks that it is still the current pending show, so a moveend, the fallback timer and a rejected fitBounds arbitrate to exactly one popup. The tooltip's stylesheet goes with its component: the shared .aoi-tool / .aoi-tooltip rules keep only the tool, and core's popup carries its own CSS.
Covers the map:showPopup request shape, the deferred show, the hide-before-show ordering, label escaping, the analyze and cancel hand-offs, and teardown while a show is still pending. Also pins the three pending-show defects the migration fixes: two rapid selections leaving two live one-shots, a superseded selection's rejected fitBounds opening its own popup, and a pending show surviving destroy.
`map:showPopup` now answers its own request with how the popup closed, so AOI no longer names events for core to broadcast. The two popup event constants and their subscriptions are gone; `_showPopup` awaits the request and branches on the result: a primary press hands the feature to the analysis consumers, a secondary press and a dismissal (the X or a click on the map) clear the selection, and `closed` — a retract or a replacement AOI itself caused — leaves the selection alone. A rejected request, which is how core reports a malformed request or a mount failure, is warned about instead of surfacing as an unhandled rejection. Cancelling a pending show also joins `_clearSelection`, so a cleared selection cannot leave a popup queued behind the camera. The spec drives the popup slot the way core does — one popup at a time, answered on its own promise — and covers each outcome, the rejection path, and the teardown and supersession cases it already pinned.
Take the up-front retract in `_applySelection` off the list of things that look deletable: the comment now says outright that it is load-bearing. A reader who knows `map:showPopup` replaces the open popup for itself can otherwise conclude the extra `map:hidePopup` is redundant, and it is not — this selection's popup waits for the camera to settle, so without the retract the previous popup is still the open one when core's deferred `map:click` dismissal runs, and it answers as a dismissal that clears the selection just made. Drop the `_cancelPendingPopup()` guard from `_clearSelection`. It is dead: `_showPopup` only ever runs with `_pendingPopup` already null (from `settled`, which cancels first, or from the branch that never arms one), `_clearSelection` has a single call site on the popup's outcome, and a selection that arms a new pending popup retracts the open one first, which answers it `closed` and never reaches the clear. Its spec case went with it — it exercised a state no path produces. Split the outcome and rejection handlers into a two-arg `then`, so a throw out of an `analysisAOIReady` consumer is no longer logged as a failure to show the popup. Also list the one-shot `map:moveend` subscription in the header contract and drop two fake-api accessors the spec never reads.
The selection path now reads `map:getBounds` before it decides whether to move the camera, so the fake api answers that request and the flush helper runs the chain of microtasks it adds instead of a single tick. Without both, every case here asserts against a selection that has not reached its popup yet. Three cases follow from that read. A selection already inside the view opens its popup immediately, with no fitBounds and nothing subscribed to moveend. A rejected fitBounds opens the popup once, anchored against the view read a moment earlier, and the fallback timer that follows adds none. And a torn down or superseded selection is dropped mid-read, before it ever subscribes. The teardown case now covers the whole contract rather than the popup alone: closing the tool, switching away from it and collapsing its panel all reach the plugin through `destroy()`, which has to leave no selection, no highlight and no popup behind.
`map:hidePopup` empties core's single popup slot whoever filled it, so AOI retracting unconditionally would close another plugin's popup on teardown or on the next selection. Track the popup AOI has on screen — the request that goes unanswered for exactly as long as it is up — and ask for a hide only while that is AOI's own. The slot is released however the request answers, including the 'closed' another plugin's popup produces. A camera step that threw also left `_pendingPopup` pointing at a token nothing could settle, which claimed a popup was on its way when none was. Release it from the failure path, and only while it is still that chain's, so a selection that superseded it keeps its own. The comment on the deferred show promised more than the code does: the one-shot settles on the next `map:moveend`, whichever movement produces it. Say so, and record why it is subscribed before the fit is requested — `mmgisAPI.request` runs its provider synchronously and a fit with no transition emits `moveend` from inside that call.
AOI tracked whether the popup on screen was its own so it could stay quiet otherwise. Core answers that now — it retracts a popup only for the caller that opened it — so the tracking is gone, and teardown and the up-front retract in a new selection just ask. Popup requests go through AOI's own bus handle, which stamps them with AOI's id; that stamp is the whole reason core can tell. The handle's stand-in gained a `request` to match, so a tool made without a bus still degrades to no-ops rather than crashing the first time it reaches for one. The up-front retract keeps its job of beating the deferred click-away, and is race-free by construction now: it can only ever close AOI's own popup.
The analyze/cancel card is a title over its two buttons, so the label travels in the request's `title` field and no html is sent at all. Core renders a title as text and names the card by it, which is what the tooltip this replaced announced.
CarsonDavis
force-pushed
the
feature/299-aoi-popup-migration
branch
from
August 26, 2026 19:37
d97bd31 to
b550739
Compare
…up-migration # Conflicts: # src/essence/Tools/AOI/AOITool.js
A `fitBounds` rejection cannot be cancelled once it is in flight, so it is the one route into the settle path that outlives the selection that started it. Left to run it disarms the current selection's pending show and opens the previous selection's card. The spec holds each fit open so that rejection can be fired after a second selection has taken over. The mocked handles stamp their caller the way the real one does, in the options object, rather than in the argument beside it that the bus now refuses.
Fold the payload permutations into the main request test, drop the camera-deferral scenarios aoiSelectionCamera.spec.js already covers, and keep one pin per ownership outcome.
7 tasks
A drawing session now retracts the previous selection's card and clears the selection with it: the card held the session's Escape and Enter and offered to analyze the area being replaced, and it holds the selection's only controls, so leaving the selection behind would strand it. Every request AOI makes goes through its plugin handle, so core sees who asked; on/off stay on the global bus. A LineString or MultiLineString now gets a vertex-mean centroid, so a drawn line - a supported drawShapes value - gets its Analyze/Cancel card. selectionTooltipAnchor is renamed selectionPopupAnchor after the tooltip it no longer anchors.
The fake bus now runs a provider inside `request`, as core does, so the spec that pins subscribing to `map:moveend` before asking for the fit fails when the two are swapped. New specs cover the armed disarm - a teardown or a superseding selection after the camera was read - and the selection a drawing session takes away. The rejected-show spec restores a working `map:showPopup` before the second selection, so it can tell the tool is still usable. Reworded the teardown comment: closing or unloading the tool reaches destroy(), hiding it does not.
Picking a draw shape retracted the card and dropped the selection with it, before a single vertex existed, so Escaping back out left nothing to restore. The selection is now held across the arming, given up on the first committed vertex, and its card put back when the session ends without one. The shape `map:drawstart` carries also reaches the panel state, so a mid-session shape switch no longer drops back to the shape picker while a session is live.
Both now read a selection through one vertex-parts helper, so a line or a point gets a camera step and the off-screen anchor fallback a polygon gets instead of skipping straight to an unguarded show. The camera spec's second moveend also carries a payload unrelated to the centroid it asserts, so a handler forwarding that payload as the anchor is caught there.
A drawing session that is backed out of now re-anchors the selection's card against the current view, so a centroid off-screen falls back to the view centre instead of stranding the selection's only Analyze and Cancel. The shape-switch flag goes with it: both adapters restart a session without a cancel of their own, so every cancel that arrives is the user backing out.
Collaborator
Author
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.
The AOI plugin no longer draws its own analyze/cancel card — it asks core for one and core answers with which button was pressed.
Closes #299
The analyze/cancel card is core's popup now
Previously, you picked an area — searched a state, clicked a boundary in inspect mode, drew a shape, uploaded a file — and a small card appeared above it with the area's name, an Analyze area button and a Cancel button. That card was the plugin's own React component, mounted into a core overlay node through a callback core handed out, and its stylesheet lived in the plugin too.
That arrangement can't survive a real plugin boundary. Handing a plugin a DOM node to mount into means the plugin has to ship a card, its markup and its styling, and every fix to any of it lands in one plugin instead of everywhere.
The fix: AOI sends plain data over the bus — a lat/lng, a title and two button labels — and core draws the card, owns its position and lifecycle, and reports back which button was pressed. Same wording, same buttons, same anchor above the area, plus a close X and Escape handling that the old card never had. Analyze area still emits the same
plugin:aoi:analysisAOIReadyevent with the same feature; core's answer only says which button was pressed, so the plugin attaches the feature itself. The old tooltip component and its styles are deleted, and the contract AOI advertises drops a now-unused cancelled event and a dead subscription to tool switches.Demo mission: yes, searching any state brings the card up, and it still drives the stats and chart panels.
Closing the card now abandons the selection
Previously, an area you had picked stayed picked until you pressed Analyze area or Cancel. There was no X on the card, Escape did nothing, and clicking somewhere else on the map left the card sitting where it was.
Core reports all three of those — the X, Escape, a click anywhere else on the map — as the same thing: the card was dismissed. The issue asked for the X and the map click but left open what a dismissal should do to the area you had picked.
The fix: a dismissal is treated exactly like Cancel. The area and its highlight are dropped and the same cleared event fires as before. This is the one change a user would notice without drawing anything, and it is the reviewer's call to make.
Demo mission: yes — search a state, then click bare map, and the highlight goes away with the card.
Starting a drawing puts the card away, and backing out brings it back
Previously, with an area selected, clicking Rectangle left the old card up for the whole drawing session, offering to analyze the area you were in the middle of replacing. And because a card is a dialog that takes keyboard focus, it swallowed the Escape and Enter that the drawing session itself needs.
Leaving it up stopped being an option once the card belongs to core: a click on the map during a drawing comes back as a dismissal, which would now wipe the selection out from under you.
The fix: the card is retracted when a drawing starts and the old selection is parked rather than dropped. The first vertex you commit is where it actually goes away. Back out before that vertex — Escape, or a session that ends with no shape — and the same card returns, re-anchored to the middle of the current view so it lands on screen even if you panned. The panel now shows whichever shape the engine reports it started, rather than the button that was pressed, so the two cannot disagree.
Demo mission: yes — search a state, click Rectangle (card goes, highlight stays), press Escape before the first corner (card comes back).
No stray cards, and nobody else's card gets taken down
Previously, selecting a far-away area and closing the tool while the camera was still flying produced a card anyway, with buttons wired to a tool that no longer existed. AOI waits for the camera to settle before asking for a card, and the one-shot camera listener and fallback timer doing that waiting were untracked, so nothing could call them off or stop them both firing. Each deferred show is now a tracked slot that teardown, a newer selection, or a failed camera move all disarm, and only one of them can win it.
Previously, with a card already open, clicking another boundary in inspect mode cleared the selection you had just made — that click reached core a beat later as a dismissal of the still-open card. AOI now retracts its own card before it starts the camera move, so there is nothing left for the click to dismiss.
Core has one popup slot, so a blanket hide on teardown would also take down a card another plugin had put there. Every AOI request travels through the plugin's own bus handle, which stamps it with AOI's id, and core honours a hide request only from the card's owner.
Demo mission: yes for the first two — search a far-off state and close the AOI panel immediately, then click boundary after boundary in inspect mode. No for the third; the demo has no second popup consumer.
A drawn line or point can be a selection
Previously, on a mission whose draw shapes include lines or points, you could draw a line, finish it, and get nothing at all: no card, so the line could neither be analyzed nor cleared. The centroid helper only understood points and polygons and the bounds helper only polygons, so a line fell through both.
The fix: both helpers now walk whatever vertices the geometry has, so a line or point gets the same card, anchored at the mean of its vertices. One consequence to know about: a line wider than the current view now moves the camera, where before it never did.
Demo mission: no. The demo leaves the draw shapes at the polygon, rectangle and circle defaults, and search, boundary clicks and uploads all produce polygons.
Decisions to review
plugin:aoi:drawingCancelledis gone from the published contract. Cancel and dismiss both funnel into the cleared event, though core's answer still tells them apart, so the emit could have been kept on the cancel branch. Nothing in the repo listened; an out-of-tree listener loses it silently.