Skip to content

Commit 2086a1f

Browse files
committed
RG-T117 PR#42 fixes
1 parent aaf43ff commit 2086a1f

5 files changed

Lines changed: 117 additions & 17 deletions

File tree

src/app/(app)/index.tsx

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,14 +21,14 @@ import { useMapGeolocationUpdates } from '@/hooks/use-map-geolocation-updates';
2121
import { useMapSignalRUpdates } from '@/hooks/use-map-signalr-updates';
2222
import { Env } from '@/lib/env';
2323
import { logger } from '@/lib/logging';
24+
import { useDepartmentMapCenter } from '@/lib/map-center';
2425
import { type MapMakerInfoData } from '@/models/v4/mapping/getMapDataAndMarkersData';
2526
import { locationService } from '@/services/location';
2627
import { useCoreStore } from '@/stores/app/core-store';
2728
import { useLocationStore } from '@/stores/app/location-store';
2829
import { useMapsStore } from '@/stores/maps/store';
2930
import { useToastStore } from '@/stores/toast/store';
3031
import { useWeatherAlertsStore } from '@/stores/weather-alerts/store';
31-
import { getDepartmentMapCenter } from '@/lib/map-center';
3232

3333
Mapbox.setAccessToken(Env.IC_MAPBOX_PUBKEY);
3434

@@ -61,6 +61,9 @@ function MapContent() {
6161
const locationLongitude = useLocationStore((state) => state.longitude);
6262
const locationHeading = useLocationStore((state) => state.heading);
6363
const isMapLocked = useLocationStore((state) => state.isMapLocked);
64+
// Reactive: department config can land after this screen mounts.
65+
const departmentCenter = useDepartmentMapCenter();
66+
const appliedDepartmentCenterRef = useRef<string | null>(null);
6467

6568
// Weather alert banner state
6669
const weatherAlerts = useWeatherAlertsStore((state) => state.alerts);
@@ -109,14 +112,36 @@ function MapContent() {
109112
};
110113
}
111114

112-
// Fallback: default US center when location hasn't arrived yet
115+
// Fallback: the department's configured center when location hasn't arrived yet
113116
return {
114-
centerCoordinate: [getDepartmentMapCenter().longitude, getDepartmentMapCenter().latitude] as [number, number],
117+
centerCoordinate: [departmentCenter.longitude, departmentCenter.latitude] as [number, number],
115118
zoomLevel: 4,
116119
heading: 0,
117120
pitch: 0,
118121
};
119-
}, [locationLatitude, locationLongitude, isMapLocked]);
122+
}, [locationLatitude, locationLongitude, isMapLocked, departmentCenter.latitude, departmentCenter.longitude]);
123+
124+
// defaultSettings only applies at mount, and department config can land after it. Move the camera
125+
// to the department center when it changes, unless a device fix or the user got there first.
126+
useEffect(() => {
127+
if (!isMapReady || hasUserMovedMap || (locationLatitude != null && locationLongitude != null)) {
128+
return;
129+
}
130+
131+
const center = `${departmentCenter.longitude},${departmentCenter.latitude}`;
132+
if (appliedDepartmentCenterRef.current === center) {
133+
return;
134+
}
135+
appliedDepartmentCenterRef.current = center;
136+
137+
cameraRef.current?.setCamera({
138+
centerCoordinate: [departmentCenter.longitude, departmentCenter.latitude],
139+
zoomLevel: 4,
140+
heading: 0,
141+
pitch: 0,
142+
animationDuration: 500,
143+
});
144+
}, [isMapReady, hasUserMovedMap, locationLatitude, locationLongitude, departmentCenter.latitude, departmentCenter.longitude]);
120145

121146
// Fetch map layers (department-level, no unit context needed)
122147
useEffect(() => {

src/app/command-map/[callId].tsx

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22,16 +22,14 @@ import { useCommandMapOverlay } from '@/hooks/use-command-map-overlay';
2222
import { useMapGeolocationUpdates } from '@/hooks/use-map-geolocation-updates';
2323
import { useMapSignalRUpdates } from '@/hooks/use-map-signalr-updates';
2424
import { logger } from '@/lib/logging';
25+
import { useDepartmentMapCenter } from '@/lib/map-center';
2526
import { IncidentMapAnnotationType } from '@/models/v4/incidentCommand/incidentCommandEnums';
2627
import { type MapMakerInfoData } from '@/models/v4/mapping/getMapDataAndMarkersData';
2728
import { useCommandStore } from '@/stores/command/store';
2829
import { useToastStore } from '@/stores/toast/store';
29-
import { getDepartmentMapCenter } from '@/lib/map-center';
3030

3131
type DrawMode = 'none' | 'line' | 'area' | 'text';
3232

33-
const DEFAULT_CENTER: [number, number] = [getDepartmentMapCenter().longitude, getDepartmentMapCenter().latitude];
34-
3533
/**
3634
* Fullscreen editable incident tactical map: pan/zoom freely, save the incident's framing (Save View),
3735
* draw lines/areas, drop text labels, and remove markup — every change syncs to the board's map card
@@ -50,6 +48,8 @@ export default function CommandMapScreen() {
5048
const saveMapAnnotationEntry = useCommandStore((state) => state.saveMapAnnotationEntry);
5149
const deleteMapAnnotationEntry = useCommandStore((state) => state.deleteMapAnnotationEntry);
5250
const showToast = useToastStore((state) => state.showToast);
51+
// Reactive: department config can land after this screen mounts.
52+
const departmentCenter = useDepartmentMapCenter();
5353

5454
const boardState = boards[callId];
5555
const command = boardState?.board?.Command ?? null;
@@ -72,6 +72,9 @@ export default function CommandMapScreen() {
7272
// Tracks what the camera was auto-centered on so higher-priority targets
7373
// arriving later (saved view > ICP > first pin) can still recenter.
7474
const autoCenterPriorityRef = useRef<0 | 1 | 2>(0);
75+
// The department center at mount is already baked into initialCamera, so only a later change
76+
// (config landing after mount) needs an imperative move. Null until the first effect run.
77+
const appliedDepartmentCenterRef = useRef<string | null>(null);
7578

7679
useEffect(() => {
7780
let cancelled = false;
@@ -116,7 +119,7 @@ export default function CommandMapScreen() {
116119
if (first) {
117120
return { centerCoordinate: [first.Longitude, first.Latitude] as [number, number], zoomLevel: 13 };
118121
}
119-
return { centerCoordinate: DEFAULT_CENTER, zoomLevel: 4 };
122+
return { centerCoordinate: [departmentCenter.longitude, departmentCenter.latitude] as [number, number], zoomLevel: 4 };
120123
// incidentPins intentionally omitted: pins arrive async and are applied via the recenter effect below
121124
// eslint-disable-next-line react-hooks/exhaustive-deps
122125
}, [
@@ -128,6 +131,8 @@ export default function CommandMapScreen() {
128131
namedMap?.CenterLatitude,
129132
namedMap?.CenterLongitude,
130133
namedMap?.ZoomLevel,
134+
departmentCenter.latitude,
135+
departmentCenter.longitude,
131136
]);
132137

133138
// initialCamera is only applied at mount (defaultSettings). Pins and the ICP
@@ -152,8 +157,21 @@ export default function CommandMapScreen() {
152157
if (first && autoCenterPriorityRef.current < 1) {
153158
autoCenterPriorityRef.current = 1;
154159
cameraRef.current?.setCamera({ centerCoordinate: [first.Longitude, first.Latitude], zoomLevel: 13, animationDuration: 500 });
160+
return;
161+
}
162+
163+
// Nothing incident-specific to frame: stay on the department center, which changes when config
164+
// lands after mount. The priority stays 0 so an ICP or a pin arriving later still wins.
165+
if (autoCenterPriorityRef.current === 0) {
166+
const center = `${departmentCenter.longitude},${departmentCenter.latitude}`;
167+
const applied = appliedDepartmentCenterRef.current;
168+
appliedDepartmentCenterRef.current = center;
169+
// The first run only records what defaultSettings already applied — no camera move.
170+
if (applied !== null && applied !== center) {
171+
cameraRef.current?.setCamera({ centerCoordinate: [departmentCenter.longitude, departmentCenter.latitude], zoomLevel: 4, animationDuration: 500 });
172+
}
155173
}
156-
}, [incidentPins, namedMap, command]);
174+
}, [incidentPins, namedMap, command, departmentCenter.latitude, departmentCenter.longitude]);
157175

158176
const handleCameraChanged = useCallback((state: { properties?: { center?: number[]; zoom?: number } }) => {
159177
const center = state?.properties?.center;

src/components/maps/full-screen-location-picker.tsx

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import { Box } from '@/components/ui/box';
1010
import { Button, ButtonText } from '@/components/ui/button';
1111
import { Text } from '@/components/ui/text';
1212
import { Env } from '@/lib/env';
13-
import { getDepartmentMapCenter } from '@/lib/map-center';
13+
import { useDepartmentMapCenter } from '@/lib/map-center';
1414

1515
// Ensure Mapbox access token is set before using any Mapbox components
1616
Mapbox.setAccessToken(Env.IC_MAPBOX_PUBKEY);
@@ -35,11 +35,12 @@ const FullScreenLocationPicker: React.FC<FullScreenLocationPickerProps> = ({ ini
3535
const insets = useSafeAreaInsets();
3636
const mapRef = useRef<React.ElementRef<typeof Mapbox.MapView>>(null);
3737
const cameraRef = useRef<any>(null); // Using any due to imperative handle
38+
const departmentCenter = useDepartmentMapCenter();
3839
// Always start with a location - either initial, or default
3940
const [currentLocation, setCurrentLocation] = useState<{
4041
latitude: number;
4142
longitude: number;
42-
}>(initialLocation || getDepartmentMapCenter());
43+
}>(initialLocation || { latitude: departmentCenter.latitude, longitude: departmentCenter.longitude });
4344
const [isLocating, setIsLocating] = useState(false);
4445
const [isReverseGeocoding, setIsReverseGeocoding] = useState(false);
4546
const [address, setAddress] = useState<string | undefined>(undefined);
@@ -152,6 +153,15 @@ const FullScreenLocationPicker: React.FC<FullScreenLocationPickerProps> = ({ ini
152153
};
153154
}, [initialLocation, getUserLocation, reverseGeocode]);
154155

156+
// Department config can land after mount. Follow it only while the placeholder is still on
157+
// screen — an initial location or anything the user/device picked outranks it.
158+
useEffect(() => {
159+
if (initialLocation || hasUserLocation) {
160+
return;
161+
}
162+
setCurrentLocation({ latitude: departmentCenter.latitude, longitude: departmentCenter.longitude });
163+
}, [initialLocation, hasUserLocation, departmentCenter.latitude, departmentCenter.longitude]);
164+
155165
const handleMapPress = (event: GeoJSON.Feature) => {
156166
if (event.geometry.type !== 'GeometryCollection' && 'coordinates' in event.geometry) {
157167
const coords = event.geometry.coordinates as number[];
@@ -177,7 +187,14 @@ const FullScreenLocationPicker: React.FC<FullScreenLocationPickerProps> = ({ ini
177187
return (
178188
<Box style={styles.container}>
179189
<Mapbox.MapView ref={mapRef} style={styles.map} logoEnabled={false} attributionEnabled={true} compassEnabled={true} zoomEnabled={true} rotateEnabled={true} onPress={handleMapPress}>
180-
<Mapbox.Camera ref={cameraRef} zoomLevel={hasUserLocation ? 15 : 4} centerCoordinate={[currentLocation.longitude, currentLocation.latitude]} animationMode="flyTo" animationDuration={1000} />
190+
{/* Without a real location the camera sits on the department center, so it frames it at the department's configured zoom rather than a hardcoded one. */}
191+
<Mapbox.Camera
192+
ref={cameraRef}
193+
zoomLevel={hasUserLocation ? 15 : departmentCenter.zoomLevel}
194+
centerCoordinate={[currentLocation.longitude, currentLocation.latitude]}
195+
animationMode="flyTo"
196+
animationDuration={1000}
197+
/>
181198
{/* Marker for the selected location */}
182199
<Mapbox.PointAnnotation id="selectedLocation" coordinate={[currentLocation.longitude, currentLocation.latitude]} title="Selected Location">
183200
<Box className="items-center justify-center">

src/components/maps/location-picker.tsx

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import { Box } from '@/components/ui/box';
99
import { Button, ButtonText } from '@/components/ui/button';
1010
import { Text } from '@/components/ui/text';
1111
import { Env } from '@/lib/env';
12-
import { getDepartmentMapCenter } from '@/lib/map-center';
12+
import { useDepartmentMapCenter } from '@/lib/map-center';
1313

1414
// Ensure Mapbox access token is set before using any Mapbox components
1515
if (!Env.IC_MAPBOX_PUBKEY) {
@@ -38,11 +38,12 @@ const LocationPicker: React.FC<LocationPickerProps> = ({ initialLocation, onLoca
3838
const mapRef = useRef<React.ElementRef<typeof Mapbox.MapView>>(null);
3939
const cameraRef = useRef<any>(null); // Using any due to imperative handle
4040
const isMountedRef = useRef(true);
41+
const departmentCenter = useDepartmentMapCenter();
4142
// Always start with a location - either initial, or default
4243
const [currentLocation, setCurrentLocation] = useState<{
4344
latitude: number;
4445
longitude: number;
45-
}>(initialLocation || getDepartmentMapCenter());
46+
}>(initialLocation || { latitude: departmentCenter.latitude, longitude: departmentCenter.longitude });
4647
const [isLocating, setIsLocating] = useState(false);
4748
const [hasUserLocation, setHasUserLocation] = useState(!!initialLocation);
4849

@@ -123,6 +124,15 @@ const LocationPicker: React.FC<LocationPickerProps> = ({ initialLocation, onLoca
123124
};
124125
}, [initialLocation, getUserLocation]);
125126

127+
// Department config can land after mount. Follow it only while the placeholder is still on
128+
// screen — an initial location or anything the user/device picked outranks it.
129+
useEffect(() => {
130+
if (initialLocation || hasUserLocation) {
131+
return;
132+
}
133+
setCurrentLocation({ latitude: departmentCenter.latitude, longitude: departmentCenter.longitude });
134+
}, [initialLocation, hasUserLocation, departmentCenter.latitude, departmentCenter.longitude]);
135+
126136
const handleMapPress = (event: GeoJSON.Feature) => {
127137
const geometry = event.geometry as GeoJSON.Point;
128138
const [longitude, latitude] = geometry.coordinates;
@@ -141,7 +151,14 @@ const LocationPicker: React.FC<LocationPickerProps> = ({ initialLocation, onLoca
141151
return (
142152
<Box style={[styles.container, { height }]}>
143153
<Mapbox.MapView ref={mapRef} style={styles.map} logoEnabled={false} attributionEnabled={false} compassEnabled={true} zoomEnabled={true} rotateEnabled={true} onPress={handleMapPress}>
144-
<Mapbox.Camera ref={cameraRef} zoomLevel={hasUserLocation ? 15 : 4} centerCoordinate={[currentLocation.longitude, currentLocation.latitude]} animationMode="flyTo" animationDuration={1000} />
154+
{/* Without a real location the camera sits on the department center, so it frames it at the department's configured zoom rather than a hardcoded one. */}
155+
<Mapbox.Camera
156+
ref={cameraRef}
157+
zoomLevel={hasUserLocation ? 15 : departmentCenter.zoomLevel}
158+
centerCoordinate={[currentLocation.longitude, currentLocation.latitude]}
159+
animationMode="flyTo"
160+
animationDuration={1000}
161+
/>
145162
{/* Marker for the selected location */}
146163
<Mapbox.PointAnnotation id="selectedLocation" coordinate={[currentLocation.longitude, currentLocation.latitude]} title={t('common.selected_location')}>
147164
<Box className="items-center justify-center">

src/components/maps/map-view.web.tsx

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import React, { forwardRef, useCallback, useContext, useEffect, useImperativeHan
1010
import { createRoot } from 'react-dom/client';
1111

1212
import { Env } from '@/lib/env';
13-
import { getDepartmentMapCenter } from '@/lib/map-center';
13+
import { useDepartmentMapCenter } from '@/lib/map-center';
1414

1515
// Set the access token globally
1616
mapboxgl.accessToken = Env.IC_MAPBOX_PUBKEY;
@@ -209,6 +209,10 @@ export const MapView = forwardRef<any, MapViewProps>(
209209
const map = useRef<any | null>(null);
210210
const [isLoaded, setIsLoaded] = useState(false);
211211
const [hasSize, setHasSize] = useState(false);
212+
// Reactive: department config can land after the map is constructed.
213+
const departmentCenter = useDepartmentMapCenter();
214+
const appliedDepartmentCenterRef = useRef<string | null>(null);
215+
const hasUserMovedRef = useRef(false);
212216

213217
useImperativeHandle(ref, () => ({
214218
getMap: () => map.current,
@@ -253,7 +257,7 @@ export const MapView = forwardRef<any, MapViewProps>(
253257
try {
254258
// Use initialCenter/initialZoom if provided so the map starts at the
255259
// correct position without needing a programmatic camera move later.
256-
const startCenter = initialCenter && isFinite(initialCenter[0]) && isFinite(initialCenter[1]) ? initialCenter : ([getDepartmentMapCenter().longitude, getDepartmentMapCenter().latitude] as [number, number]); // Default US center
260+
const startCenter = initialCenter && isFinite(initialCenter[0]) && isFinite(initialCenter[1]) ? initialCenter : ([departmentCenter.longitude, departmentCenter.latitude] as [number, number]); // Department center
257261
const startZoom = initialZoom != null && isFinite(initialZoom) ? initialZoom : 4;
258262

259263
const newMap = new mapboxgl.Map({
@@ -293,6 +297,9 @@ export const MapView = forwardRef<any, MapViewProps>(
293297
// We tag all programmatic camera moves with { _programmatic: true } so the
294298
// moveend handler can distinguish them from real user interactions.
295299
const wasUser = !e._programmatic;
300+
if (wasUser) {
301+
hasUserMovedRef.current = true;
302+
}
296303
onCameraChanged?.({ properties: { isUserInteraction: wasUser } });
297304
});
298305

@@ -385,6 +392,22 @@ export const MapView = forwardRef<any, MapViewProps>(
385392
// eslint-disable-next-line react-hooks/exhaustive-deps
386393
}, [hasSize]);
387394

395+
// The constructor center is fixed at build time, and department config can land after that.
396+
// Recenter when it changes — but only when the caller pinned no initial center and the user
397+
// has not already moved the map themselves.
398+
useEffect(() => {
399+
if (!isLoaded || !map.current) return;
400+
if (initialCenter && isFinite(initialCenter[0]) && isFinite(initialCenter[1])) return;
401+
if (hasUserMovedRef.current) return;
402+
403+
const center = `${departmentCenter.longitude},${departmentCenter.latitude}`;
404+
if (appliedDepartmentCenterRef.current === center) return;
405+
appliedDepartmentCenterRef.current = center;
406+
407+
// Tagged programmatic so moveend does not report this as a user interaction.
408+
map.current.easeTo({ center: [departmentCenter.longitude, departmentCenter.latitude], duration: 500 }, { _programmatic: true });
409+
}, [isLoaded, initialCenter, departmentCenter.longitude, departmentCenter.latitude]);
410+
388411
// Keep the map canvas in sync with container size changes.
389412
useEffect(() => {
390413
if (!map.current || !mapContainer.current) return;

0 commit comments

Comments
 (0)