JamesLMilner / terra-draw

A library for drawing on maps that supports Mapbox, MapLibre, Google Maps, OpenLayers and Leaflet out the box
https://terradraw.io
MIT License
415 stars 48 forks source link

Select Mode easier to extend #168

Open alanOracle opened 6 months ago

alanOracle commented 6 months ago

Im trying to create my own implementation of select mode (Im basically removing all mid and selection points ONLY while dragging the features, so this way those layers wont get updated 60 times per second, to improve performance in maplibre) , I will reuse most of the methods and properties that are already in the mode i will only override some of them.

So the proposal is to make all properties and methods of select mode protected instead of private, so this way i can make references to those in my own subclass.

I realized that this way is a perfect fit for my implementation of the tool, and discovered it by browsing the documentation where its mentioned that you can create your own select method.

Also while starting this implementation i realized i will be using an slightly changed version of src/modes/select/behaviors/drag-coordinate.behavior.ts and src/modes/select/behaviors/selection-point.behavior.ts again only changing a few or a single method of the classes so it would be nice to have a way to import those classes from the Terra draw API and be able to call its method from a subclass.

Thanks for the suggestion of creating your own implementation of the mode, it really opened my mind.

JamesLMilner commented 6 months ago

Would it be easier if we provided a way to disable selection/midpoints whilst dragging in the builtin in Select mode?

alanOracle commented 6 months ago

Hi, thanks @JamesLMilner that could be a nice thing to have, but actually I was able to find a workaround to extend the mode, and now I'm not only doing the removal of selection and midpoints, I'm addressing some other behaviors.

What I did was to extend the select mode and cast this to any so that typescript wont check if Im calling a private method. (Not the best approach since in these cases im getting rid of the Typescript type checking).

Here's an illustrative example:

export class ExtendedSelectMode extends TerraDrawSelectMode {

    override start(): void {
        const self = (this as any);

        // dragCoordinate is a private member of select mode
        self.dragCoordinate.isDragging()
    }

}

I think the best approach on my side is to have everything as "vanilla" as possible so let me list the things im doing in this extended class aside from the removal of the selection/mid points for you to know, and maybe suggest some workaround and avoid having this extended class.

1- Im not setting 'selecting' state on start.

The problem here was that sometimes the user was selecting features rapidly, which would lead to the PointerUp being sufficiently far way from the PointerDown event to be consider as click instead of drag. So basically the user (wanting to select a feature) unintentionally drag a little bit. This would lead to the user to think that the tool was not working.

To fix this I first set minPixelDragDistance to 30, yes NOT minPixelDragDistanceSelecting.

Then i changed start method to NOT set 'selecting' state.

Then i added a new method in the extendedSelectMode, I have a listener for 'PointerDown' and that listener would call this method which looks like this:

onPointerDown(event: TerraDrawMouseEvent){
        // We only need to stop the map dragging if
        // we actually have something selected
        if (!(this as any).selected.length) {
            return;
        }

        // If the selected feature is not draggable
        // don't do anything
        const properties = this.store.getPropertiesCopy((this as any).selected[0]!);
        const modeFlags = (this as any).flags[properties["mode"] as string];
        const draggable =
            modeFlags &&
            modeFlags.feature &&
            (modeFlags.feature.draggable ||
                (modeFlags.feature.coordinates &&
                    modeFlags.feature.coordinates.draggable));

        if (!draggable) {
            return;
        }

        const selectedId = (this as any).selected[0]!;
        const draggableCoordinateIndex = (this as any).dragCoordinate.getDraggableIndex(
            event,
            selectedId
        );

        if (
            modeFlags &&
            modeFlags.feature &&
            modeFlags.feature.coordinates &&
            modeFlags.feature.coordinates.draggable &&
            draggableCoordinateIndex !== -1
        ) {
            if (this.state === 'started'){
                this.setSelecting();
            }
            return;
        }

        if (
            modeFlags &&
            modeFlags.feature &&
            modeFlags.feature.draggable &&
            (this as any).dragFeature.canDrag(event, selectedId)
        ) {
            if (this.state === 'started'){
                this.setSelecting();
            }
            return;
        }
    }

As you can see all this method do is setSelecting state ONLY if the pointerDown event could be interpreted as drag.

I also had to change onDragEnd , onClick and stop methods. Adding the following to make sure 'selecting' is only being the state in the time period BETWEEN the pointerDown and the pointerUp events.

        if (self.state === 'selecting'){
            self.setStarted();
        }

What i accomplish with this is that I can drag inside a feature a little bit (30 pixels max) and still select the feature i want. This without affecting the drag behavior anywhere else, meaning when i want to drag the map it wont take 30 pixels for the map to start moving it'll start moving right away.

2- override onMouseMove

If the user is near a selection point i change the cursor style to 'pointer', to let the user know if he clicks at that moment it will be affecting that selection point and not the whole feature.

    // Set "pointer" cursor style if user is close to a selection point
    override onMouseMove(event: TerraDrawMouseEvent): void {
                super.onMouseMove(event);

        // Set "pointer" cursor style when close to a selection point
        (this as any).selectionPoints.ids.forEach((id: string) => {
            const geometry = this.store.getGeometryCopy<Point>(id);
            const distance = (this as any).pixelDistance.measure(event, geometry.coordinates);
            if (distance < this.pointerDistance) {
                this.setCursor("pointer");
            }
        });
    }

3- Public onRightClick

I created a public method to access onRightClick private method so that I could programmatically remove selection points. So here it would be nice to be able to remove selection points programmatically.

4- New Method called getSelectionPointAtMouseEvent

A method to get the selection point near an event, this is necessary since getFeaturesAtPointerEvent with the ignoreSelectFeatures: false not always retrieves the selection point the user is referring to. A clear example of this is that there is a zone in which you can 'pointerDown' near a selection point and terra draw would interpret with that event that the user want to drag this selection point, however if you pass that exact same 'pointerDown' event to getFeaturesAtPointerEvent (even with the flag ignoreSelectFeatures: false) it wont retrieve that selection point(only the whole feature in some cases).

I think this is due a difference in how these 2 method work

In getFeaturesAtPointerEvent at some point the features are being gotten like

const features = this._store.search(bbox as BBoxPolygon);

How my method works (identical to how it is identified if a user want to drag a selection point in onMouseMove)

getSelectionPointAtMouseEvent(event: TerraDrawMouseEvent) {
    const self = (this as any);

    return self.selectionPoints.ids.some((id: string) => {
        const geometry = this.store.getGeometryCopy<Point>(id);
        const distance = self.pixelDistance.measure(event, geometry.coordinates);

        return distance < self.pointerDistance;
    });
}

Thanks for the help, hope these are also helpful and understandable.