-
Notifications
You must be signed in to change notification settings - Fork 0
Make Crime map points draggable #45
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,59 @@ | ||
| export const MARKER_DRAG_SETTLE_MS = 350; | ||
|
|
||
| function readMarkerPosition(marker) { | ||
| const position = marker.getLngLat?.(); | ||
| const lng = Number(position?.lng); | ||
| const lat = Number(position?.lat); | ||
| return Number.isFinite(lng) && Number.isFinite(lat) ? { lng, lat } : null; | ||
| } | ||
|
|
||
| export function wireSettledMarkerDrag(marker, { | ||
| scheduler = globalThis, | ||
| settleMs = MARKER_DRAG_SETTLE_MS, | ||
| isActive = () => true, | ||
| onDragStart = () => {}, | ||
| onMove = () => {}, | ||
| onSettled = () => {}, | ||
| } = {}) { | ||
| let disposed = false; | ||
| let settleTimer = null; | ||
|
|
||
| const cancelSettle = () => { | ||
| if (settleTimer == null) return; | ||
| scheduler.clearTimeout(settleTimer); | ||
| settleTimer = null; | ||
| }; | ||
| const handleDragStart = () => { | ||
| cancelSettle(); | ||
| if (!disposed && isActive()) onDragStart(); | ||
| }; | ||
| const handleDrag = () => { | ||
| cancelSettle(); | ||
| if (disposed || !isActive()) return; | ||
| const position = readMarkerPosition(marker); | ||
| if (position) onMove(position); | ||
| }; | ||
| const handleDragEnd = () => { | ||
| cancelSettle(); | ||
| if (disposed || !isActive()) return; | ||
| settleTimer = scheduler.setTimeout(() => { | ||
| settleTimer = null; | ||
| if (disposed || !isActive()) return; | ||
| const position = readMarkerPosition(marker); | ||
| if (position) onSettled(position); | ||
| }, settleMs); | ||
| }; | ||
|
|
||
| marker.on('dragstart', handleDragStart); | ||
| marker.on('drag', handleDrag); | ||
| marker.on('dragend', handleDragEnd); | ||
|
|
||
| return () => { | ||
| if (disposed) return; | ||
| disposed = true; | ||
| cancelSettle(); | ||
| marker.off('dragstart', handleDragStart); | ||
| marker.off('drag', handleDrag); | ||
| marker.off('dragend', handleDragEnd); | ||
| }; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -27,6 +27,9 @@ import { | |
| geometryBounds, | ||
| } from '../map/camera_fit.js'; | ||
| import { describeCrimeDataScope } from '../ui/data_scope.js'; | ||
| import { wireSettledMarkerDrag } from './draggable_marker.js'; | ||
|
|
||
| export { wireSettledMarkerDrag }; | ||
|
|
||
| const CRIME_LAYER_IDS = [ | ||
| 'districts-fill', | ||
|
|
@@ -154,6 +157,8 @@ export async function initCrimeMode(map, { | |
| const mapReady = waitForMapReady(map); | ||
| let markerA = null; | ||
| let markerB = null; | ||
| let disposeMarkerADrag = null; | ||
| let disposeMarkerBDrag = null; | ||
| let tractClickWired = false; | ||
| let districtClickWired = false; | ||
| let active = true; | ||
|
|
@@ -403,26 +408,74 @@ export async function initCrimeMode(map, { | |
| } | ||
|
|
||
| function syncComparisonOverlays({ centerLonLat, centerBLonLat, radiusM, queryMode }) { | ||
| if (queryMode !== 'buffer') { | ||
| removeBufferOverlay(map); | ||
| const removeMarkerA = () => { | ||
| disposeMarkerADrag?.(); | ||
| disposeMarkerADrag = null; | ||
| markerA?.remove(); | ||
| markerB?.remove(); | ||
| markerA = null; | ||
| }; | ||
| const removeMarkerB = () => { | ||
| disposeMarkerBDrag?.(); | ||
| disposeMarkerBDrag = null; | ||
| markerB?.remove(); | ||
| markerB = null; | ||
| }; | ||
| const updateDraggedPoint = (target, { lng, lat }) => { | ||
| store.setComparisonPoint(target, lng, lat, t('crime.mapPoint', { target })); | ||
| const center = target === 'B' ? store.centerBLonLat : store.centerLonLat; | ||
| if (target === 'B') upsertBufferB(map, { centerLonLat: center, radiusM: store.radius }); | ||
| else upsertBufferA(map, { centerLonLat: center, radiusM: store.radius }); | ||
| }; | ||
| const wireMarkerDrag = (marker, target) => wireSettledMarkerDrag(marker, { | ||
| isActive: () => active && isActive() && store.queryMode === 'buffer', | ||
| onDragStart: () => { | ||
| refreshOwner.cancel(); | ||
| publishCurrentSelection(undefined, { origin: 'map' }); | ||
| }, | ||
| onMove: (position) => { | ||
| updateDraggedPoint(target, position); | ||
| publishCurrentSelection(undefined, { origin: 'drag' }); | ||
| }, | ||
| onSettled: (position) => { | ||
| updateDraggedPoint(target, position); | ||
|
Comment on lines
+439
to
+440
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
If Point B is cleared shortly after being dropped, the panel sets Useful? React with 👍 / 👎. |
||
| publishCurrentSelection(undefined, { origin: 'map' }); | ||
| onPointChange(target); | ||
| void requestRefresh(); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When dragging begins while a coordinator-owned refresh is in flight—including during initial Crime loading— Useful? React with 👍 / 👎. |
||
| }, | ||
| }); | ||
|
|
||
| if (queryMode !== 'buffer') { | ||
| removeBufferOverlay(map); | ||
| removeMarkerA(); | ||
| removeMarkerB(); | ||
| return; | ||
| } | ||
| if (centerLonLat) { | ||
| markerA ||= createMapMarker({ color: '#c86b00', className: 'analysis-marker analysis-marker--a' }); | ||
| if (!markerA) { | ||
| markerA = createMapMarker({ | ||
| color: '#c86b00', | ||
| className: 'analysis-marker analysis-marker--a', | ||
| draggable: true, | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
For keyboard-only users, enabling MapLibre's pointer dragging does not provide any way to focus and move the marker: the added wiring listens only for drag events, while Useful? React with 👍 / 👎. |
||
| }); | ||
| disposeMarkerADrag = wireMarkerDrag(markerA, 'A'); | ||
| } | ||
| markerA.setLngLat(centerLonLat).addTo(map); | ||
| localizeMapMarker(markerA); | ||
| upsertBufferA(map, { centerLonLat, radiusM }); | ||
| } | ||
| } else removeMarkerA(); | ||
| if (centerBLonLat) { | ||
| markerB ||= createMapMarker({ color: '#0a6c74', className: 'analysis-marker analysis-marker--b' }); | ||
| if (!markerB) { | ||
| markerB = createMapMarker({ | ||
| color: '#0a6c74', | ||
| className: 'analysis-marker analysis-marker--b', | ||
| draggable: true, | ||
| }); | ||
| disposeMarkerBDrag = wireMarkerDrag(markerB, 'B'); | ||
| } | ||
| markerB.setLngLat(centerBLonLat).addTo(map); | ||
| localizeMapMarker(markerB); | ||
| upsertBufferB(map, { centerLonLat: centerBLonLat, radiusM }); | ||
| } | ||
| } else removeMarkerB(); | ||
| } | ||
|
|
||
| async function ensureTractOutline({ signal, isCurrent, onSourceResolved }) { | ||
|
|
@@ -544,6 +597,10 @@ export async function initCrimeMode(map, { | |
| } else if (!active) { | ||
| lastCameraSelectionKey = null; | ||
| pointsController.clear(); | ||
| disposeMarkerADrag?.(); | ||
| disposeMarkerBDrag?.(); | ||
| disposeMarkerADrag = null; | ||
| disposeMarkerBDrag = null; | ||
| markerA?.remove(); | ||
| markerB?.remove(); | ||
| markerA = null; | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If the user starts another adjustment during the 450 ms
fitBoundsWithPanelanimation launched by the previous settled drag, this cancellation only invalidates the refresh and does not stop the map animation or release the points controller's programmatic-move ownership. The map can therefore continue translating beneath a held pointer, moving the marker away from the intended drop position beforedragend; stop the active camera transition on drag start or avoid initiating a fit for settled marker drags.Useful? React with 👍 / 👎.