-
Notifications
You must be signed in to change notification settings - Fork 0
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
[ERA-7681] - Map Drawing Tools #725
Conversation
…er componentry into drawing components. polygon and line segment draw modes.
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.
I didn't review the ruler files, just the whole abstraction. I added several suggestions and comments, most of them trivial stuff. I like the whole approach!
@@ -49,3 +51,105 @@ export const useDeepCompareEffect = (callback, dependencies) => { | |||
} | |||
}, [callback, dependencies]); | |||
}; | |||
|
|||
|
|||
export const useMapEventBinding = (eventType = 'click', handlerFn = noop, layerId = null, condition = true) => { |
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.
Do you think it's a good idea to add click
as the default event type and a noops as the default handler? It feels like we should always define explicitly these arguments when calling this hook so it throws an error if we accidentally miss something.
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.
I honestly don't think it matters much -- errors will just fail, meaning test coverage would fail as well in expecting side effects. I like the inline self-documentation side of this, but if you feel strongly I can be persuaded to remove.
src/hooks/index.js
Outdated
return () => { | ||
map.off(...args); | ||
}; | ||
} else { |
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 map is undefined we will fall in this else
, triggering map.off(...args);
which would throw an exception. I think this else is not really necessary, just having the on
/ off
methods trigger inside the if
should keep the memory clean.
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.
Also, in that case, we could take args
inside the if so we don't run the .filter operation when it's not necessary.
src/hooks/index.js
Outdated
|
||
export const useMapSource = (sourceId, data, config = { type: 'geojson' }) => { | ||
const map = useContext(MapContext); | ||
let source = map.getSource(sourceId); |
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.
I think this could be a const
src/hooks/index.js
Outdated
let source = map.getSource(sourceId); | ||
|
||
useEffect(() => { | ||
if (map) { |
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.
Two ifs that could become one :) map && !source
src/hooks/index.js
Outdated
useEffect(() => { | ||
return () => { | ||
setTimeout(() => { | ||
map.getSource(sourceId) && map.removeSource(sourceId); |
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.
We already have map.getSource(sourceId)
in the variable source
, no need to fetch it again 👍
const [pointerLocation, setPointerLocation] = useState(null); | ||
|
||
const cursorPopupCoords = useMemo(() => pointerLocation ? [pointerLocation.lng, pointerLocation.lat] : points[points.length - 1], [pointerLocation, points]); | ||
const data = useDrawToolGeoJson(points, (drawing && cursorPopupCoords), drawingMode); |
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.
Again, I don't think data
is a good name for this. We could follow the hook convention drawToolGeoJson
, but honestly I'd rather receive the two properties spread: { drawnLineSegmentsGeoJson, fillPolygonGeoJson }
so it's really clear what we are handling here.
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.
I like it for the dataContainer, in passing up the data on eventing. for the MapLayers
component I agree, it can be broken apart as props and passed explicitly. Will reactor.
|
||
e.preventDefault(); | ||
e.originalEvent.stopPropagation(); | ||
onMapClick.cancel(); |
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.
What does this cancel
do?
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.
This ensures we don't have funny sync-related issues where a second click fires. It's ported over from the existing ruler code
return <> | ||
{drawing && <CursorPopup coords={cursorPopupCoords} lineLength={lineLength} points={points} />} | ||
<MapLayers data={data} /> | ||
{children} |
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.
I don't see the purpose of supporting children
here if we are not going to wrap them.
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.
For ❇️ the future ❇️ but it's trivial to add/remove
return lineSegments; | ||
}; | ||
|
||
export const createFillPolygonForCoords = (coords) => polygon([coords]); |
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.
I guess we have this trivial method just to keep it in utils with createLineSegmentGeoJsonForCoords
even though it seems unnecessary. I'd just mention that it would be great to follow the same name convention, we are missing GeoJson
here
points: PropTypes.array, | ||
}; | ||
|
||
const CursorPopup = (props) => { |
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.
I think it would be cleaner to have this one in its own file, with its own tests and all. What do you think?
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.
Yep!
…t of technical debt.
… up empty files, typo correction, build improvements
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.
Another pass, a few comments.
@@ -14,13 +14,31 @@ const RASTER_SOURCE_OPTIONS = { | |||
'tileSize': 256, | |||
}; | |||
|
|||
const RenderFunction = ({ children }) => <>{children}</>; |
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.
Hmm what's this for? Can't we just render the SourceComponent
?
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.
@@ -14,13 +14,31 @@ const RASTER_SOURCE_OPTIONS = { | |||
'tileSize': 256, | |||
}; | |||
|
|||
const RenderFunction = ({ children }) => <>{children}</>; | |||
|
|||
const SourceComponent = ({ id, tileUrl, sourceConfig }) => { |
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.
We ended up removing a
const createMapSource = useCreateMapSource();
arrayWithUnknownLength.forEach((value) => createMapSource(value.id, null, config));
We only need to call the hook once and now we can create as many sources (and layers) as we want with a memoized function (so it doesn't trigger renders) :) Similar to const dispatch = useDispatch()
.
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.
This is just render props, a well-known and proven React pattern. Iterative hook generation, however, is an anti-pattern.
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.
Hmm I don't know what I'm missing, but I don't see the render prop pattern here. As far as I know that pattern looks like this: <Component renderProp={() => <div>This function returning JSX is the render prop</div>} />
but here I just see a renderless component within a "render function" that is just rendering regular children within a fragment 🤔 (this paragraph is rendering the word render a few times jeje)
Iterative hook generation definitely sounds like an antipattern, my suggestion was not to call or generate hooks within a loop, but just call it once and get a memoized function that let us create layers and sources freely. I don't see why that is an antipattern, it's literally what standard hooks like useCallback
or the setter of a useState
do.
It was a suggestion though, I hadn't seen anything like this and it felt confusing at first, but if you like it that's ok.
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.
Yeah sort of -- except I'm using it for children
à là the docs, where the child is a function. Then abstracting the child function further as a named component ("SourceComponent").
You're right that I misread your suggestion. I still like my pattern better 🙃 hahahaha but I'm open to discussing further if you have strong feelings
@@ -1,6 +1,12 @@ | |||
import { useState, useEffect, useRef } from 'react'; |
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.
I'm not a big fan of having this hooks
file which could grow infinitely larger with more and more hooks. This also involves having a single test file with lots of tests covering different stuff. We could have each hook file and its test in a separate folder and import / export them in the index, so we don't have to go deep in the folder structure when we want to import them. What do you think?
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.
Eventually, yes. I don't think we're there yet, though it would be nice. If it's easy, which it should be, I'll break it up a bit more
src/FeatureLayer/index.js
Outdated
type: 'geojson', | ||
data: polygons, | ||
}; | ||
useMapSource('feature-line-source', lines); |
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.
I guess this technical debt was already there, but it should be nice to have these source names as constants.
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.
Agreed! That's pretty trivial, I'll hit that on the final pass.
src/MapRulerControl/index.js
Outdated
{drawing && <PointPopup map={map} points={points} pointIndex={points.length - 1} drawing={drawing} onClickFinish={onFinish} />} | ||
{!drawing && popupPointSelected && <PointPopup map={map} points={points} pointIndex={selectedPointIndex} drawing={drawing} />} | ||
</>} | ||
{active && <MapDrawingTools drawing={drawing} drawingMode={DRAWING_MODES.POLYGON} points={points} onChange={onDrawChange} onClickPoint={onClickPoint} />} |
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.
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.
Great! I think there are still a few things in the previous comments pending to be implemented, but given our agreements and the code, I'm ready to approve this 💯
@@ -14,13 +14,31 @@ const RASTER_SOURCE_OPTIONS = { | |||
'tileSize': 256, | |||
}; | |||
|
|||
const RenderFunction = ({ children }) => <>{children}</>; | |||
|
|||
const SourceComponent = ({ id, tileUrl, sourceConfig }) => { |
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.
Hmm I don't know what I'm missing, but I don't see the render prop pattern here. As far as I know that pattern looks like this: <Component renderProp={() => <div>This function returning JSX is the render prop</div>} />
but here I just see a renderless component within a "render function" that is just rendering regular children within a fragment 🤔 (this paragraph is rendering the word render a few times jeje)
Iterative hook generation definitely sounds like an antipattern, my suggestion was not to call or generate hooks within a loop, but just call it once and get a memoized function that let us create layers and sources freely. I don't see why that is an antipattern, it's literally what standard hooks like useCallback
or the setter of a useState
do.
It was a suggestion though, I hadn't seen anything like this and it felt confusing at first, but if you like it that's ok.
Sometimes it doesn't allow you to add the second point 🤔 is this something that should work in this change? You will see in this video that I am trying to add the second point a bit above the first one, and as it doesn't allow me to do so, I add it below, which it seems to be allowed. And it happens all the time, if you add a point, then another one below that first point, and try to add another one above the two of them, it just doesn't allow to do so. And you can see that the pointer changes from the finger pointer to the cross pointer moving the cursos as anyone would (not being fast on purpose). So I think that is causing us not to be able to add another point. Screen.Recording.2022-08-12.at.11.04.35.mov |
This is caused by this bug thanks for our 3D-enabled maps 🙁 |
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.
Let's move on with this one, we know there's an open issue for mapbox that affects this implementation, and there's a simple workaround that helps, just turning 3D Map Terrain Off.
https://allenai.atlassian.net/browse/ERA-7681
Refactoring the map ruler code for use as drawing tools.