Skip to content
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

Merged
merged 22 commits into from
Aug 16, 2022
Merged

[ERA-7681] - Map Drawing Tools #725

merged 22 commits into from
Aug 16, 2022

Conversation

JoshuaVulcan
Copy link
Collaborator

https://allenai.atlassian.net/browse/ERA-7681

Refactoring the map ruler code for use as drawing tools.

@JoshuaVulcan JoshuaVulcan marked this pull request as draft August 4, 2022 22:52
Copy link
Contributor

@luixlive luixlive left a 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) => {
Copy link
Contributor

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.

Copy link
Collaborator Author

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.

return () => {
map.off(...args);
};
} else {
Copy link
Contributor

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.

Copy link
Contributor

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.


export const useMapSource = (sourceId, data, config = { type: 'geojson' }) => {
const map = useContext(MapContext);
let source = map.getSource(sourceId);
Copy link
Contributor

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

let source = map.getSource(sourceId);

useEffect(() => {
if (map) {
Copy link
Contributor

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

useEffect(() => {
return () => {
setTimeout(() => {
map.getSource(sourceId) && map.removeSource(sourceId);
Copy link
Contributor

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);
Copy link
Contributor

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.

Copy link
Collaborator Author

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();
Copy link
Contributor

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?

Copy link
Collaborator Author

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}
Copy link
Contributor

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.

Copy link
Collaborator Author

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]);
Copy link
Contributor

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) => {
Copy link
Contributor

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?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Yep!

Copy link
Contributor

@luixlive luixlive left a 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}</>;
Copy link
Contributor

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?

Copy link
Collaborator Author

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 }) => {
Copy link
Contributor

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 component from that library, to add our own since we needed to call N hooks depending on an array. I know this involves a change to what you've done, but a possible solution that keep us away from patterns like this is to have the hooks working like a factory more than like a method:


  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().

Copy link
Collaborator Author

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.

Copy link
Contributor

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.

Copy link
Collaborator Author

@JoshuaVulcan JoshuaVulcan Aug 12, 2022

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';
Copy link
Contributor

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?

Copy link
Collaborator Author

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

type: 'geojson',
data: polygons,
};
useMapSource('feature-line-source', lines);
Copy link
Contributor

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.

Copy link
Collaborator Author

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/HeatLayer/index.js Outdated Show resolved Hide resolved
src/MapDrawingTools/MapLayers.js Outdated Show resolved Hide resolved
@JoshuaVulcan JoshuaVulcan marked this pull request as ready for review August 11, 2022 22:42
@JoshuaVulcan JoshuaVulcan requested a review from luixlive August 11, 2022 22:42
@JoshuaVulcan JoshuaVulcan changed the title [ERA-7681] [ERA-7681] - Map Drawing Tools Aug 11, 2022
{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} />}
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

@Alcoto95 @luixlive

We can consider drawingMode={DRAWING_MODES.POLYGON} the feature flag for testing the polygon side of things -- feel free to toggle it back to DRAWING_MODES.LINE to see the regular ol' ruler

Copy link
Contributor

@luixlive luixlive left a 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 }) => {
Copy link
Contributor

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.

@Alcoto95
Copy link
Contributor

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

@JoshuaVulcan
Copy link
Collaborator Author

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 🙁
We can disable 3d map terrain ("settings" --> uncheck "3d map terrain") for now to ensure the drawing works correctly.
I'll keep cracking on a solution in the meantime; would you consider it a blocker? It's a bug in a library, not our code, and doesn't block functionality with a workaround. Not great though either...

Copy link
Contributor

@Alcoto95 Alcoto95 left a 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.

@JoshuaVulcan JoshuaVulcan merged commit 7b9d9a2 into develop Aug 16, 2022
@JoshuaVulcan JoshuaVulcan deleted the ERA-7681 branch August 16, 2022 04:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants