Skip to content

ERA-10349: Support EFB schemas in ER > Location Field [2] #1263

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

Merged
merged 2 commits into from
Mar 27, 2025
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 30 additions & 25 deletions src/LocationPicker/MenuPopover/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ const MenuPopover = ({
}, ref) => {
const { t } = useTranslation('components', { keyPrefix: 'locationPicker.menuPopover' });

const isPickingLocation = useSelector((state) => state.view.mapLocationSelection.isPickingLocation);
const showUserLocation = useSelector((state) => state.view.showUserLocation);

const gpsFormatToggleRef = useRef();
Expand Down Expand Up @@ -105,34 +106,38 @@ const MenuPopover = ({
}, []);

useEffect(() => {
const onMouseDown = (event) => {
if (!wrapperRef.current.contains(event.target) && !setLocationButtonRef.current.contains(event.target)) {
onClose(event);

if (!target.current.contains(event.target)) {
// Clicking away from our picker when the menu is open doesn't trigger the wrapper's blur event, so we need
// to trigger the onBlur callback manually.
const blurEvent = new FocusEvent('blur', {
bubbles: true,
cancelable: false,
relatedTarget: event.target,
});

Object.defineProperties(blurEvent, {
target: {
value: target.current,
},
});

onBlur(blurEvent);
// Add a mouse down event to close the menu if the user clicks outside only if the user is not picking a location
// from the map.
if (!isPickingLocation) {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This fixes the issue when chosing a location and clicking something that is not the map. We just remove the listener while user picks a location so its logic doesn't un-focuses the location picker getting the app into a broken state.

const onMouseDown = (event) => {
if (!wrapperRef.current.contains(event.target) && !setLocationButtonRef.current.contains(event.target)) {
onClose(event);

if (onBlur && !target.current.contains(event.target)) {
// Clicking away from our picker when the menu is open doesn't trigger the wrapper's blur event, so we need
// to trigger the onBlur callback manually.
const blurEvent = new FocusEvent('blur', {
bubbles: true,
cancelable: false,
relatedTarget: event.target,
});

Object.defineProperties(blurEvent, {
target: {
value: target.current,
},
});

onBlur(blurEvent);
}
}
}
};
};

document.addEventListener('mousedown', onMouseDown);
document.addEventListener('mousedown', onMouseDown);

return () => document.removeEventListener('mousedown', onMouseDown);
}, [onBlur, onClose, setLocationButtonRef, target]);
return () => document.removeEventListener('mousedown', onMouseDown);
}
}, [isPickingLocation, onBlur, onClose, setLocationButtonRef, target]);

return <Popover
className={`${className} ${styles.menuPopover}`}
Expand Down
42 changes: 42 additions & 0 deletions src/LocationPicker/MenuPopover/index.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@ describe('LocationPicker - MenuPopover', () => {
beforeEach(() => {
store = {
view: {
mapLocationSelection: {
isPickingLocation: false,
},
showUserLocation: false,
userLocation: null,
userPreferences: {
Expand Down Expand Up @@ -247,4 +250,43 @@ describe('LocationPicker - MenuPopover', () => {
expect(onClose).toHaveBeenCalledTimes(1);
expect(onBlur).not.toHaveBeenCalled();
});

test('does not close the menu if the user clicks outside while picking a location', () => {
store.view.mapLocationSelection.isPickingLocation = true;

render(<>
<div data-testid="outside" />

<Provider store={mockStore(store)}>
<MapContext.Provider value={map}>
<MenuPopover
className="className"
id="locationPicker"
onBlur={onBlur}
onChange={onChange}
onClose={onClose}
setLocationButtonRef={{
current: {
contains: () => false,
focus: setLocationButtonRefFocus,
},
}}
style={{}}
target={{
current: {
contains: () => false,
offsetWidth: 100,
},
}}
value={null}
/>
</MapContext.Provider>
</Provider>
</>);

userEvent.click(screen.getByTestId('outside'));

expect(onClose).not.toHaveBeenCalled();
expect(onBlur).not.toHaveBeenCalled();
});
});
3 changes: 3 additions & 0 deletions src/LocationPicker/index.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ describe('LocationPicker', () => {

store = {
view: {
mapLocationSelection: {
isPickingLocation: false,
},
showUserLocation: false,
userLocation: null,
userPreferences: {
Expand Down
2 changes: 0 additions & 2 deletions src/MapDrawingTools/index.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -100,8 +100,6 @@ describe('MapDrawingTools', () => {
});

test('adding a layer if the source exists', () => {
map.getSource.mockReturnValue({ whatever: 'yes' });

render(
<MapContext.Provider value={map}>
<MapDrawingToolsContextProvider>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@ describe('ReportManager - DetailsSection - SchemaForm - fields - Location', () =

store = {
view: {
mapLocationSelection: {
isPickingLocation: false,
},
showUserLocation: false,
userLocation: null,
userPreferences: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,18 +34,15 @@ const useLocationMarkersLayer = (eventLocation, onMarkerClickCallback) => {
))
), [markers]);

// GeoJSON feature collection with line strings connecting each marker to the event location.
const markerConnectingLinesFeatureCollection = useMemo(() => {
if (eventLocation?.latitude && eventLocation?.longitude) {
return featureCollection(
Object.values(markers).map((markerLocation) => lineString([
[markerLocation.longitude, markerLocation.latitude],
[eventLocation.longitude, eventLocation.latitude],
]))
);
}
return null;
}, [eventLocation?.latitude, eventLocation?.longitude, markers]);
// GeoJSON feature collection with line strings connecting each marker to the event location if it is defined.
const markerConnectingLinesFeatureCollection = useMemo(() => featureCollection(
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This fixes the source data inconsistency issue. I was returning null for the source data, which isn't handled properly by mapbox. Returning an empty feature collection instead works fine 😄

eventLocation?.latitude && eventLocation?.longitude
? Object.values(markers).map((markerLocation) => lineString([
[markerLocation.longitude, markerLocation.latitude],
[eventLocation.longitude, eventLocation.latitude],
]))
: []
), [eventLocation?.latitude, eventLocation?.longitude, markers]);

// Map sources for the marker points and connecting lines.
useMapSources([{ data: markerPointsFeatureCollection, id: MARKERS_SOURCE_ID }]);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,47 @@ describe('ReportManager - DetailsSection - SchemaForm - Utils - useLocationMarke
expect(onMarkerClickCallback).toHaveBeenCalledWith('clicked-marker');
});

it('adds the location markers source to the map', () => {
renderHook(() =>
useLocationMarkersLayer(
{ latitude: 10, longitude: 10 },
onMarkerClickCallback
)
);

expect(useMapSources).toHaveBeenCalledTimes(2);
expect(useMapSources).toHaveBeenCalledWith([
{
data: { features: [], type: 'FeatureCollection' },
id: 'event-location-markers-source',
},
]);
expect(useMapSources).toHaveBeenCalledWith([
{
data: { features: [], type: 'FeatureCollection' },
id: 'event-location-markers-source-lines',
},
]);
});

it('adds the location markers source to the map for an event without location', () => {
renderHook(() => useLocationMarkersLayer(null, onMarkerClickCallback));

expect(useMapSources).toHaveBeenCalledTimes(2);
expect(useMapSources).toHaveBeenCalledWith([
{
data: { features: [], type: 'FeatureCollection' },
id: 'event-location-markers-source',
},
]);
expect(useMapSources).toHaveBeenCalledWith([
{
data: { features: [], type: 'FeatureCollection' },
id: 'event-location-markers-source-lines',
},
]);
});

it('updates the markers in the map', () => {
const { result } = renderHook(() =>
useLocationMarkersLayer(
Expand Down Expand Up @@ -190,7 +231,8 @@ describe('ReportManager - DetailsSection - SchemaForm - Utils - useLocationMarke
)
);

const { blurLocationMarker, focusLocationMarker, updateLocationMarkers } = result.current;
const { blurLocationMarker, focusLocationMarker, updateLocationMarkers } =
result.current;

updateLocationMarkers({
'location-1': {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,5 @@ describe('TrackLegend - TimeOfDaySettings - TimeZoneSelect', () => {
userEvent.click(screen.getAllByRole('option')[100]);

expect(setTimeOfDayTimeZone).toHaveBeenCalledTimes(1);
// This test may break if someday the IANA standard updates.
expect(setTimeOfDayTimeZone).toHaveBeenCalledWith('America/Marigot');
Copy link
Contributor

Choose a reason for hiding this comment

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

What happened here, why to delete this assert?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is the line that has been bothering us for a couple weeks with tests breaking in different timezone servers. Joshua sent a fix in another PR removing the entire test, but that is not necessary, removing this line is enough and we still keep some coverage 😄

});
});
65 changes: 31 additions & 34 deletions src/hooks/useMapSources/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,54 +2,51 @@ import { useContext, useEffect, useRef } from 'react';

import { MapContext } from '../../App';

const DEFAULT_CONFIGURATION = { type: 'geojson' };

const useMapSources = (sourceConfigsBatch = [], defaultConfig = { type: 'geojson' }) => {
const useMapSources = (sourceConfigurations = [], defaultConfiguration = DEFAULT_CONFIGURATION) => {
const map = useContext(MapContext);
const sourceIdsRef = useRef([]);

const idsOfSourcesAddedToMapRef = useRef([]);

useEffect(() => {
if (map) {
sourceConfigsBatch.forEach(sourceConfig => {
if (sourceConfig?.id && !map.getSource(sourceConfig.id)){
const { id, data = {}, options = {} } = sourceConfig;
const fullSourceConfig = { ...defaultConfig, ...options };
map.addSource(id, {
...fullSourceConfig,
data,
sourceConfigurations.forEach((sourceConfiguration) => {
const source = map.getSource(sourceConfiguration.id);
if (source) {
// If the source is already in the map, update its data.
source.setData(sourceConfiguration.data);
} else {
// If the source is not in the map yet, add it.
map.addSource(sourceConfiguration.id, {
...defaultConfiguration,
...sourceConfiguration.options,
data: sourceConfiguration.data,
});
sourceIdsRef.current.push(id);

idsOfSourcesAddedToMapRef.current = [...idsOfSourcesAddedToMapRef.current, sourceConfiguration.id];
}
});
}
}, [map, sourceConfigsBatch, defaultConfig]);
}, [defaultConfiguration, map, sourceConfigurations]);

useEffect(() => {
sourceConfigsBatch.forEach(sourceConfig => {
const source = map?.getSource?.(sourceConfig?.id);
if (sourceConfig?.id && sourceConfig?.data && source){
source.setData?.(sourceConfig.data);
}
});
}, [map, sourceConfigsBatch]);
if (map) {
const idsOfSourcesAddedToMap = idsOfSourcesAddedToMapRef.current;

useEffect(() => {
const refs = sourceIdsRef?.current;
return () => {
if (map) {
setTimeout(() => {
refs.forEach(id => {
if (map?.getSource(id)) {
map.removeSource(id);
}
});
});
}
};
// Remove the sources from the map on unmount.
return () => setTimeout(() => idsOfSourcesAddedToMap.forEach((sourceId) => {
if (map.getSource(sourceId)) {
map.removeSource(sourceId);
}
}));
}
}, [map]);

return sourceConfigsBatch
.map((sourceConfig) => sourceConfig.id ? map?.getSource(sourceConfig.id) : null)
.filter(sourceConfig => !!sourceConfig);
// Return the sources that are already defined in the map.
return sourceConfigurations
.map((sourceConfiguration) => map?.getSource(sourceConfiguration.id))
.filter((source) => !!source);
};

export default useMapSources;
Loading