ERA-13723: Blue start/end patrol pins with patrol-name labels - #1666
ERA-13723: Blue start/end patrol pins with patrol-name labels#1666tiffanynwong wants to merge 1 commit into
Conversation
Replace the per-patrol-type icon at patrol track endpoints with two shared blue map pins (play glyph = start, square = end) and label each with the patrol name (title, else patrol type display name, else "Patrol Start" / "Patrol End"). The name renders to the right of the pin as black text with a white halo, no background chip. Also make the start/stop symbol layer attach reliably when the patrol data is already loaded at mount (wait for the source via a sourcedata event before mounting the memoized symbol-layer child). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Updates patrol track endpoint rendering to use shared blue start/end pin assets and to label those pins with the patrol name, while also addressing a Mapbox source/layer ordering issue that could prevent the start/stop symbol layer from attaching.
Changes:
- Replace per-patrol-type endpoint icons with shared start/end pin SVG assets, loaded as Mapbox images.
- Update start/end marker labels to prefer patrol title, then patrol type display name, with generic fallbacks (preserving “(Est)”).
- Add a “wait for source” gate before mounting the memoized symbol-layer child to improve layer-attach reliability.
Reviewed changes
Copilot reviewed 2 out of 4 changed files in this pull request and generated 3 comments.
| File | Description |
|---|---|
| src/utils/patrols.js | Switches start/end point feature image to new pin assets and updates label selection logic. |
| src/PatrolStartStopLayer/layer.js | Adjusts symbol styling/layout for right-of-pin labels and adds source-ready gating before mounting the symbol layer. |
| src/common/images/icons/patrol-start-pin.svg | New blue start pin SVG asset (play glyph). |
| src/common/images/icons/patrol-end-pin.svg | New blue end pin SVG asset (square glyph). |
| const { data: { patrolTypes } } = store.getState(); | ||
| const matchingPatrolType = patrolType | ||
| ? (patrolTypes || []).find(({ value, id }) => value === patrolType || id === patrolType) | ||
| : null; | ||
| const patrolName = patrol.title || matchingPatrolType?.display || null; |
| const patrolName = patrol.title || matchingPatrolType?.display || null; | ||
| const startLabel = patrolName || 'Patrol Start'; | ||
| const endLabel = patrolName || 'Patrol End'; | ||
|
|
| const [isSourceReady, setIsSourceReady] = useState(false); | ||
| useEffect(() => { | ||
| if (!map) return undefined; | ||
|
|
||
| const handleSourceData = () => { | ||
| if (map.getSource(sourceId)) { | ||
| setIsSourceReady(true); | ||
| map.off('sourcedata', handleSourceData); | ||
| } | ||
| }; | ||
| map.on('sourcedata', handleSourceData); | ||
|
|
||
| return () => map.off('sourcedata', handleSourceData); | ||
| }, [map, sourceId]); |
There was a problem hiding this comment.
Thanks Tiffany 😄 Nice PR, sorry it took me that much to review it. I have some feedback for it, but most of it can be delegated to Claude. Just ask it to read my comments from the PR and fix them.
Three high level comments:
1- Tests are broken after these changes 🥲 You can ask Claude to see what's going on and fix them. Then you can make sure they pass with yarn test.
2- This branch is a bit old. Let's make sure to update develop and merge it here (you can ask Claude to do all this). And there are a couple conflicts we need to fix.
3- We are missing unit tests for the changes.
The rest are trivial comments, a couple from me, a bunch from a local Claude review. Most seem trivial to fix!
Edit: btw, I also see a couple items from Copilot that we should work on.
| 'line-cap': 'round', | ||
| }; | ||
|
|
||
| // The patrol name renders on the icon layer (to the right of the pin) as black |
There was a problem hiding this comment.
I see too much commentary. We've asked the agents to avoid unnecessary blocks of comments when doing changes, but I think this PR was sent before I updated the AGENTS.md 😄
| isPatrolDone) { | ||
| patrol_points.end_location = cloneDeep(patrol_points.start_location); | ||
| patrol_points.end_location.properties.title = 'Patrol End (Est)'; | ||
| patrol_points.end_location.properties.image = patrolEndPin; |
There was a problem hiding this comment.
Claude:
The new end-pin assignment in the done-patrol clone branch is unconditionally dead code — the cloned end_location is always deleted three lines later, so this case never gets the square end glyph the PR is about.
| if (patrol_points.start_location.properties.title !== patrol_points.end_location.properties.title) { | ||
| patrol_points.start_location.properties.title += ` & ${patrol_points.end_location.properties.title}`; | ||
| } | ||
| delete patrol_points.end_location; |
There was a problem hiding this comment.
Claude:
When start and end coincide, delete patrol_points.end_location keeps patrolStartPin, so the new end glyph is silently dropped and — with the new equality guard — the label no longer says the patrol ended there either.
| // Label the start/end markers with the patrol's name (title, else patrol type | ||
| // display name), falling back to the generic "Patrol Start"/"Patrol End". | ||
| const patrolType = firstLeg?.patrol_type; | ||
| const { data: { patrolTypes } } = store.getState(); |
There was a problem hiding this comment.
Claude:
patrolTypes is read via store.getState() from inside a reselect compute function, so it is not a memoization input and stale generic labels are never recomputed.
| point(patrol_points.start_location.geometry.coordinates), | ||
| )) { | ||
| patrol_points.start_location.properties.title += ` & ${patrol_points.end_location.properties.title}`; | ||
| if (patrol_points.start_location.properties.title !== patrol_points.end_location.properties.title) { |
There was a problem hiding this comment.
Claude:
The new title !== title guard compares titles after the ' (Est)' suffix is appended, so a coincident start/end renders the patrol name twice in one label.
|
|
||
| // Label the start/end markers with the patrol's name (title, else patrol type | ||
| // display name), falling back to the generic "Patrol Start"/"Patrol End". | ||
| const patrolType = firstLeg?.patrol_type; |
There was a problem hiding this comment.
Claude:
The 8-line patrol-name derivation re-implements displayTitleForPatrol from the same file but drops its leader-name fallback, so the map pin and the track legend show different names for the same patrol.
|
|
||
| if (start_location) { | ||
| patrol_points.start_location = makePatrolPointFromFeature('Patrol Start', [start_location.longitude, start_location.latitude], icon_id, stroke, start_time); | ||
| patrol_points.start_location = makePatrolPointFromFeature(startLabel, [start_location.longitude, start_location.latitude], patrolStartPin, stroke, start_time); |
There was a problem hiding this comment.
Claude:
Replacing the per-patrol-type sprite with one shared blue pin, plus dropping the per-patrol label color, leaves every patrol's endpoints visually identical and desynchronizes the map markers from the track legend.
| const iconLayout = { | ||
| 'icon-anchor': 'bottom', | ||
| 'icon-size': PIN_ICON_SIZE, | ||
| 'text-field': '{title}', |
There was a problem hiding this comment.
Claude:
Moving 'text-field': '{title}' onto the icon layer bypasses mapUserLayoutConfigByLayerId, so Settings → Map → Map Markers → Patrols no longer hides patrol start/end names.
| // the memoized child never re-renders to retry. Wait for the source to exist | ||
| // (signalled by a sourcedata event) before mounting the child so its layer | ||
| // reliably attaches. | ||
| const [isSourceReady, setIsSourceReady] = useState(false); |
There was a problem hiding this comment.
Claude:
isSourceReady is a one-way latch fed by a global sourcedata event with no synchronous check and no re-arm, so it both delays and can permanently prevent the symbol layer from attaching.
| .filter(Boolean) | ||
| .forEach(({ properties: { image } = {} }) => { | ||
| if (image && !map.hasImage(calcImgIdFromUrlForMapImages(image))) { | ||
| addMapImage({ src: image }); |
There was a problem hiding this comment.
Claude:
Two static app-bundled pins are registered from a per-patrol effect keyed on a churning patrolData, with an un-awaited addMapImage and no in-flight guard, instead of once at map init.
Summary
Updates the patrol start and end map markers:
#0056C7) with a white border.(Est)suffix for estimated points is preserved).New assets
src/common/images/icons/patrol-start-pin.svgsrc/common/images/icons/patrol-end-pin.svgNotes for reviewers
ClustersLayer/utils.js(out of scope): removes a pre-existing duplicateimportfrom../constantsthat was preventing the app from compiling on this branch. Included because it blocks the build; happy to split it out if preferred.sourcedataevent) before mounting the symbol child.Testing
yarn test—src/utils/patrols.test.jspasses.yarn lint— clean on changed files.Follow-ups / open questions
🤖 Generated with Claude Code
