-
Notifications
You must be signed in to change notification settings - Fork 0
feat:connecting trawl lines between linked buoys #1276
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 1 commit
524aab2
49cfbaa
5f8516a
c95d9f2
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,70 @@ | ||
import React from 'react'; | ||
import { useSelector } from 'react-redux'; | ||
|
||
import { getMapSubjectFeatureCollectionWithVirtualPositioning } from '../selectors/subjects'; | ||
import { lineString } from '@turf/turf'; | ||
import useMapLayers from '../hooks/useMapLayers'; | ||
import useMapSources from '../hooks/useMapSources'; | ||
|
||
const lineLayout = { | ||
'line-join': 'round', | ||
'line-cap': 'round', | ||
}; | ||
|
||
const linePaint = { | ||
'line-color': 'black', | ||
'line-opacity': 0.7, | ||
'line-gap-width': 1, | ||
'line-width': 2, | ||
}; | ||
|
||
const subjectIsBuoyLineEligible = (subjectFeature = {}, _index, allSubjects = []) => { | ||
const subject = subjectFeature.properties; | ||
|
||
const is_buoy = subject.subject_subtype === 'ropeless_buoy_device'; | ||
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. This string should be a constant. Maybe we won't need it anywhere else so at least putting it at the top of the file like: Second question: why snake case here (and in all other variables of this method)? It's not our usual approach 🤔 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. The snake case and non-constantified strings are just me doing this as a quickie/prototype. 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. fixed! |
||
if (!is_buoy) return false; | ||
|
||
const devices = subject.additional?.devices ?? []; | ||
const is_line = devices.length > 1; | ||
|
||
if (!is_line) return false; | ||
|
||
const line_contains_valid_subjects = devices.every(({ device_id }) => allSubjects.find(({ properties }) => properties.name === device_id)); | ||
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. This variable name sounds odd, it almost sounds like a boolean for a condition: 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. This is a boolean for a condition -- it's just using 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. Ohh right, I just understood the every and where this is being used 👍 |
||
|
||
return line_contains_valid_subjects; | ||
}; | ||
|
||
const createTrawlLineGeoJSON = (buoySubjectFeatures) => { | ||
return buoySubjectFeatures.reduce((accumulator, { properties }) => { | ||
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. Honestly, I've stared at this function for a while haha. I feel like we could make it a bit easier to understand maybe by changing some variable names or not extracting properties from objects like: 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. Summary version: We can say that the devices listed for a buoy feature's We can't say that the coordinates included in the additional data So the eligibility-checker function first ensures a trawl line is completely valid and comprised of available subjects for the user. Then 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. That makes sense. Maybe a couple tweaks or a brief comment could make that more obvious in the code for future reference. 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. Added a descriptive inline comment |
||
const coordinates = | ||
properties.additional.devices.map(({ device_id }) => | ||
buoySubjectFeatures.find(({ properties }) => | ||
properties.name === device_id)?.geometry?.coordinates ?? [] | ||
); | ||
|
||
accumulator.features.push(lineString(coordinates)); | ||
return accumulator; | ||
|
||
}, { type: 'FeatureCollection', features: [] }); | ||
}; | ||
|
||
const BuoyLineLayer = (_props) => { | ||
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. nit: why the 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. just a matter of preference! 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. I mean, it's a default linter rule ( |
||
const mapSubjects = useSelector(getMapSubjectFeatureCollectionWithVirtualPositioning); | ||
|
||
const buoySubjects = mapSubjects.features.filter(subjectIsBuoyLineEligible); | ||
const trawlLineGeoJSON = createTrawlLineGeoJSON(buoySubjects); | ||
|
||
useMapSources([{ id: 'trawl-lines-source', data: trawlLineGeoJSON }]); | ||
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. We have constants for sources and layers that we should use to keep our code consistent and the ids accessible. Also, I don't know if you tested the properly addition, update and removal of these sources and layers (seeing Chromes console for warnings and errors), but as I mentioned recently, these hooks don't follow Mapbox sources and layers flows correctly from beginning to end. That's why I think it would be better to stop using them and instead use the map API directly. 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. I did test absence/presence/filtering etc, and timesliding, and it all works as expected. I kept the names out of our constants file because I'd like this to be as self-isolated as possible. I will however make the strings constants within this file. 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. Oh with tested I meant manually. In unit testing we usually use mocks for the map and other methods and the real flow of addition, interaction and removal (or not removal, causing memory leaks) of sources and layers are obscured by all the mocking. Anyway, we agreed this is out of the scope. About the layers and sources being isolated, why's that 🤔 ? I know this is a feature for the specific scenario of buoy, but I don't get why we don't want to treat it as part of our app if now it is. Just like stationary subjects, or patrols, which are features not everyone use. I feel I'm missing some information or something haha. 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.
Me too :-) "Features not everyone uses" can be considered a spectrum -- with this Buoy work being at one pretty extreme end of the spectrum, "features designed for a specific site and fringe use-cases". It's so specifically for a special sub-project, that I'd rather not pollute the broader app with random |
||
useMapLayers([{ | ||
id: 'trawl-lines-layer', | ||
type: 'line', | ||
sourceId: 'trawl-lines-source', | ||
layout: lineLayout, | ||
paint: linePaint, | ||
}]); | ||
|
||
return null; | ||
|
||
}; | ||
|
||
export default BuoyLineLayer; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,127 @@ | ||
import React from 'react'; | ||
import { render } from '@testing-library/react'; | ||
import { useSelector } from 'react-redux'; | ||
import BuoyLineLayer from './'; | ||
import useMapLayers from '../hooks/useMapLayers'; | ||
import useMapSources from '../hooks/useMapSources'; | ||
|
||
jest.mock('react-redux', () => ({ | ||
useSelector: jest.fn(), | ||
})); | ||
|
||
jest.mock('../hooks/useMapLayers', () => jest.fn()); | ||
jest.mock('../hooks/useMapSources', () => jest.fn()); | ||
|
||
const createMockSubject = (name, subtype, devices, coordinates) => ({ | ||
type: 'Feature', | ||
properties: { | ||
name, | ||
subject_subtype: subtype, | ||
additional: { devices }, | ||
}, | ||
geometry: { | ||
type: 'Point', | ||
coordinates, | ||
}, | ||
}); | ||
|
||
describe('BuoyLineLayer', () => { | ||
|
||
beforeEach(() => { | ||
jest.clearAllMocks(); | ||
}); | ||
|
||
test('should not render any lines when no buoy subjects are present', () => { | ||
useSelector.mockReturnValue({ | ||
features: [], | ||
}); | ||
|
||
render(<BuoyLineLayer />); | ||
|
||
expect(useMapSources).toHaveBeenCalledWith([ | ||
{ | ||
id: 'trawl-lines-source', | ||
data: { type: 'FeatureCollection', features: [] }, | ||
}, | ||
]); | ||
expect(useMapLayers).toHaveBeenCalled(); | ||
}); | ||
|
||
test('should filter out non-buoy subjects', () => { | ||
const mockFeatures = [ | ||
createMockSubject('buoy1', 'other_type', [], [10, 20]), | ||
createMockSubject('buoy2', 'ropeless_buoy_device', [{ device_id: 'device1' }], [30, 40]), | ||
]; | ||
|
||
useSelector.mockReturnValue({ | ||
features: mockFeatures, | ||
}); | ||
|
||
render(<BuoyLineLayer />); | ||
|
||
expect(useMapSources).toHaveBeenCalledWith([ | ||
{ | ||
id: 'trawl-lines-source', | ||
data: { type: 'FeatureCollection', features: [] }, | ||
}, | ||
]); | ||
}); | ||
|
||
test('should create trawl lines for valid buoy subjects', () => { | ||
const device1 = createMockSubject('device1', 'ropeless_buoy_device', [{ device_id: 'device1' }, { device_id: 'device2' }], [10, 20]); | ||
const device2 = createMockSubject('device2', 'ropeless_buoy_device', [{ device_id: 'device1' }, { device_id: 'device2' }], [30, 40]); | ||
|
||
const mockFeatures = [ | ||
device1, | ||
device2, | ||
]; | ||
|
||
useSelector.mockReturnValue({ | ||
features: mockFeatures, | ||
}); | ||
|
||
render(<BuoyLineLayer />); | ||
|
||
expect(useMapSources).toHaveBeenCalledWith([ | ||
{ | ||
id: 'trawl-lines-source', | ||
data: { | ||
type: 'FeatureCollection', | ||
features: expect.arrayContaining([ | ||
{ | ||
type: 'Feature', | ||
properties: {}, | ||
geometry: { | ||
type: 'LineString', | ||
coordinates: [[10, 20], [30, 40]] | ||
} | ||
} | ||
]) | ||
} | ||
} | ||
]); | ||
}); | ||
|
||
test('should handle invalid device references', () => { | ||
const mockFeatures = [ | ||
createMockSubject('device1', 'ropeless_buoy_device', [], [10, 20]), | ||
createMockSubject('buoy1', 'ropeless_buoy_device', [ | ||
{ device_id: 'device1' }, | ||
{ device_id: 'device2' }, // invalid reference | ||
], [50, 60]), | ||
]; | ||
|
||
useSelector.mockReturnValue({ | ||
features: mockFeatures, | ||
}); | ||
|
||
render(<BuoyLineLayer />); | ||
|
||
expect(useMapSources).toHaveBeenCalledWith([ | ||
{ | ||
id: 'trawl-lines-source', | ||
data: { type: 'FeatureCollection', features: [] }, | ||
}, | ||
]); | ||
}); | ||
}); |
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.
nit: this import should go in the first block.