Flag first and last events during mouse drag using rxjs - rxjs

I'm a newbie with rxjs but I want to create a drag and drop handler with it and I have the following function:
function createDraggable$(element) {
const mouseDown$ = fromEvent(element, 'mousedown');
const mouseMove$ = fromEvent(window, 'mousemove');
const mouseUp$ = fromEvent(window, 'mouseup');
return mouseDown$.pipe(
switchMap((start) => {
const {
offsetLeft: initialLeft,
offsetTop: initialTop,
} = element;
return mouseMove$.pipe(
map((move) => {
const deltaX = move.pageX - start.pageX;
const deltaY = move.pageY - start.pageY;
return {
isFirst: false,
isLast: false,
start,
move,
deltaX,
deltaY,
initialLeft,
initialTop,
left: initialLeft + deltaX,
top: initialTop + deltaY,
};
}),
takeUntil(mouseUp$),
);
}),
);
}
createDraggable$(el).subscribe((drag) => {
if (drag.isFirst) {
dragstart.emit();
} else if (drag.isLast) {
dragend.emit();
} else {
drag.emit();
}
if (isHorizontal) {
el.style.top = `${drag.top}px`;
} else {
el.style.left = `${drag.left}px`;
}
});
As you can see the isFirst and isLast values are constant, but I would like to change them to true accordingly. The first drag event is when the first mousemove fires and the last is the one when mouseup fires.
I've tried using combineLatest with two extra streams:
const first$ = mouseMove$.pipe(first());
const last$ = mouseMove$.pipe(takeUntil(mouseUp$), last());
return combineLatest([first$, mouseMove$, last$]).pipe(
switchMap(([_first, _move, _last]) => {
...
But to no avail, since combineLatest waits on last$ to produce at least one value, which it won't...
Any help in the right direction would be greatly appreciated.

Probably you can enforce an emission on $last stream
mouseMove$.pipe(takeUntil(mouseUp$), last(),startWith(null))
and in switchMap you can try to handle the null case and do nothing by returning empty
switchMap(([_first, _move, _last]) => { if(!last) return empty() ...

As clunky as it is this is my solution:
function createDraggable$(element) {
const mouseDown$ = fromEvent(element, 'mousedown');
const mouseMove$ = fromEvent(window, 'mousemove');
const mouseUp$ = fromEvent(window, 'mouseup');
return mouseDown$.pipe(
switchMap((start) => {
const {
offsetLeft: initialLeft,
offsetTop: initialTop,
} = element;
const drag$ = mouseMove$.pipe(takeUntil(mouseUp$));
return combineLatest([
merge(of(true), drag$.pipe(take(2), last(), switchMapTo(of(false)))),
merge(of(false), drag$.pipe(last(), switchMapTo(of(true)))),
drag$,
]).pipe(
map(([isFirst, isLast, move]) => {
const deltaX = move.pageX - start.pageX;
const deltaY = move.pageY - start.pageY;
return {
isFirst,
isLast,
start,
move,
deltaX,
deltaY,
initialLeft,
initialTop,
left: initialLeft + deltaX,
top: initialTop + deltaY,
};
}),
);
}),
);
}
It is not ideal, since the isFirst and isLast events double up:
1. isFirst: true, isLast: false, move.deltaX: -1
2. isFirst: false isLast: false, move.deltaX: -1 // deltaX is again -1
3. isFirst: false isLast: false, move.deltaX: -2
4. isFirst: false isLast: false, move.deltaX: -3
5. isFirst: false isLast: false, move.deltaX: -4
6. isFirst: false isLast: true, move.deltaX: -4 // deltaX is again -4
Ideally events 1 and 2 as well as 5 and 6 will be both combined, but I really don't know rxjs to properly do it.

Related

How to use React useContext with leaflet routing machine and react leaflet?

I'm trying to use a useContext hook inside a react-leaflet controlComponent but I have an error when my context fires the update function.
I use a react-leaflet controlComponent because of leaflet routing machine. I think the code + the error are better than word:
MainBoard.tsx
export const CartographyContext: React.Context<CartographyContextType> = React.createContext<CartographyContextType>({ positions: [] });
...
const routeSummaryValueContext = React.useMemo(
() => ({ routeSummary, setRouteSummary }),
[routeSummary]
);
const elevationProfileValueContext = React.useMemo(
() => ({ elevationProfile, setElevationProfile }),
[elevationProfile]
);
........
<CartographyContext.Provider value={{ positions, elevationProfileValueContext, routeSummaryValueContext, positionsValueContext, addPosition, changePosition }}>
.........
<RoutingMachine
orsOptions={{
....
}} />
..........
</CartographyContext.Provider>
RoutingMachine.tsx:
const CreateRoutineMachineLayer = (props: any) => {
const geoService = new GeoLocalisationService();
const cartographyContext: CartographyContextType = React.useContext<CartographyContextType>(CartographyContext);
const [routes, setRoutes] = React.useState<any[]>();
React.useEffect(() => {
if (routes) {
//The line which cause the error
cartographyContext.elevationProfileValueContext.setElevationProfile(geoService.getElevationProfile(decodePolyline(routes[0].geometry, true)));
const summary: RouteSummary = {
ascent: routes[0].routeSummary.ascent,
descent: routes[0].routeSummary.descent,
distance: routes[0].routeSummary.distance,
estimatedDuration: routes[0].routeSummary.duration
}
cartographyContext.routeSummaryValueContext.setRouteSummary(summary);
}
}, [routes]);
const { orsOptions } = props;
const instance = L.Routing.control({
router: new OpenRouteRouter(orsOptions),
lineOptions: {
styles: [{ color: "#3933ff", weight: 4 }],
extendToWaypoints: true,
missingRouteTolerance: 0
},
routeWhileDragging: true,
autoRoute: true,
geocoder: new geocoder.Geocoder(),
}).on('routesfound', (e) => {
setRoutes(e.routes);
});
useMapEvents({
click: (e: L.LeafletMouseEvent) => {
if (instance.getWaypoints().length === 2 && instance.getWaypoints()[0].latLng == null) {
instance.spliceWaypoints(0, 1, new L.Routing.Waypoint(e.latlng, null, {}));
} else if (instance.getWaypoints().length === 2 && instance.getWaypoints()[1].latLng == null) {
instance.spliceWaypoints(1, 1, new L.Routing.Waypoint(e.latlng, null, {}));
} else {
instance.spliceWaypoints(instance.getWaypoints().length, 0, new L.Routing.Waypoint(e.latlng, null, {}));
}
}
});
return instance;
};
const RoutingMachine = createControlComponent(CreateRoutineMachineLayer);
error :
g: React has detected a change in the order of Hooks called by ForwardRef(LeafComponent). This will lead to bugs and errors if not fixed. For more information, read the Rules of Hooks: https://reactjs.org/link/rules-of-hooks
Previous render Next render
------------------------------------------------------
1. useContext useContext
2. useRef useRef
3. useContext useRef
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
..............
Uncaught Error: Rendered fewer hooks than expected. This may be caused by an accidental early return statement.
I clearly doing something wrong here but I haven't found yet.
Thank you
Kind regards
Ok I found the good implementation :
const RoutingMachine: React.FC<RoutingMachineProps> = (props) => {
//const RoutineMachine = (props: any) => {
const geoService = new GeoLocalisationService();
const cartographyContext: CartographyContextType = React.useContext<CartographyContextType>(CartographyContext);
const [instance, setInstance] = React.useState<any>();
const [alreadyDisplayed, setAlreadyDisplayed] = React.useState(false);
const { orsOptions } = props;
const map = useMap();
//const instance = L.Routing.control({
React.useEffect(() => {
const instance = L.Routing.control({
router: new OpenRouteRouter(orsOptions),
lineOptions: {
styles: [{ color: "#3933ff", weight: 4 }],
extendToWaypoints: true,
missingRouteTolerance: 0
},
routeWhileDragging: true,
autoRoute: true,
geocoder: (L.Control as any).Geocoder.google({
apiKey: GOOGLE.googleMapApiKey,
}),
}).on('routesfound', (e) => {
const routes = e.routes;
cartographyContext.setElevationProfile(geoService.getElevationProfile(decodePolyline(routes[0].geometry, true)));
const summary: RouteSummary = {
ascent: routes[0].routeSummary.ascent,
descent: routes[0].routeSummary.descent,
distance: routes[0].routeSummary.distance,
estimatedDuration: routes[0].routeSummary.duration
}
cartographyContext.setRouteSummary(summary);
})
setInstance(instance);
instance.addTo(map);
}, []);
useMapEvents({
click: (e: L.LeafletMouseEvent) => {
if (instance) {
if (instance.getWaypoints().length === 2 && instance.getWaypoints()[0].latLng == null) {
instance.spliceWaypoints(0, 1, new L.Routing.Waypoint(e.latlng, null, {}));
} else if (instance.getWaypoints().length === 2 && instance.getWaypoints()[1].latLng == null) {
instance.spliceWaypoints(1, 1, new L.Routing.Waypoint(e.latlng, null, {}));
} else {
instance.spliceWaypoints(instance.getWaypoints().length, 0, new L.Routing.Waypoint(e.latlng, null, {}));
}
}
}
});
return null;
};
export default RoutingMachine;

How to control movement of a person in react three fiber?

I want to create a game where I have to make my model controllable with keyboard input. I don't know what's the best way to do it and how to implement it properly.
We can implement this with cannon.js
Create a custom hook to listen to user's input.
const usePersonControls = () => {
const keys = {
KeyW: 'forward',
KeyS: 'backward',
KeyA: 'left',
KeyD: 'right',
Space: 'jump',
}
const moveFieldByKey = (key) => keys[key]
const [movement, setMovement] = useState({
forward: false,
backward: false,
left: false,
right: false,
jump: false,
})
useEffect(() => {
const handleKeyDown = (e) => {
setMovement((m) => ({ ...m, [moveFieldByKey(e.code)]: true }))
}
const handleKeyUp = (e) => {
setMovement((m) => ({ ...m, [moveFieldByKey(e.code)]: false }))
}
document.addEventListener('keydown', handleKeyDown)
document.addEventListener('keyup', handleKeyUp)
return () => {
document.removeEventListener('keydown', handleKeyDown)
document.removeEventListener('keyup', handleKeyUp)
}
}, [])
return movement
}
Now use it like so,
const { forward, backward, left, right, jump } = usePersonControls()
Create a body for person in Cannon World.
const [mesh, api] = useSphere(() => ({
mass: 10,
position: [0, 1, 0],
type: 'Dynamic',
}))
Apply velocity to sphere body.
useFrame(() => {
// Calculating front/side movement ...
let frontVector = new Vector3(0,0,0);
let sideVector = new Vector3(0,0,0);
let direction = new Vector3(0,0,0);
frontVector.set(0, 0, Number(forward) - Number(backward))
sideVector.set(Number(right) - Number(left), 0, 0)
direction
.subVectors(frontVector, sideVector)
.normalize()
.multiplyScalar(SPEED)
api.velocity.set(direction.x, 0, direction.z)
})
Now our sphere body is able to move with user's input in Cannon World, so now just update your player model in fiber using sphere's position on each frame, like so
// Setting person model position to sphere body position ...
useFrame(() => {
...
mesh.current.getWorldPosition(playerModelReference.current.position)
})
Sorry for long explanation, hope you find it helpful.
look here for details on the where does frontVector gets defined
Line 24:9: 'frontVector' is not defined
How to make character to move around 3D world in React Three Fiber?
import React from "react";
import { useSphere } from "#react-three/cannon";
import { useThree, useFrame } from "#react-three/fiber";
import { useKeyboardControls } from "../hooks/useKeyboardControls";
import { Vector3 } from "three";
const SPEED = 6;
export function Person(props) {
const { camera } = useThree();
const { moveForward, moveBackward, moveLeft, moveRight } =
useKeyboardControls();
const [ref, api] = useSphere(() => ({
mass: 1,
type: "Dynamic",
...props,
}));
const velocity = React.useRef([0, 0, 0]);
React.useEffect(() => {
api.velocity.subscribe((v) => (velocity.current = v));
}, [api.velocity]);
useFrame(() => {
camera.position.copy(ref.current.position);
const direction = new Vector3();
const frontVector = new Vector3(
0,
0,
Number(moveBackward) - Number(moveForward)
);
const sideVector = new Vector3(
Number(moveLeft) - Number(moveRight),
0,
0
);
direction
.subVectors(frontVector, sideVector)
.normalize()
.multiplyScalar(SPEED)
.applyEuler(camera.rotation);
api.velocity.set(direction.x, velocity.current[1], direction.z);
});
return (
<>
<mesh ref={ref} />
</>
);
}

RxJS v7: How to dynamically add/remove and pause/resume observables?

Given some arrays:
const recordings = {
foo: [{delay: 5, data: 'a'}, {delay: 2, data: 'b'}],
bar: [{delay: 3, data: 'x'}, {delay: 7, data: 'y'}, {delay: 4, data: 'z'}]
};
Is there a way that I can pass them to a playback method that will do something like the following?:
beginPlayback(timeline, fromIndex = 0) {
let postedIndex = -1;
const observable = from(timeline.slice(fromIndex)).pipe(
concatMap((instant, i) => of({ i, instant }).pipe(delay(instant.delay))),
tap(async ({i, instant}) => {
try {
await post(instant.data);
postedIndex = i;
} catch (err) {
console.error(err);
// Continue playback
}
})
);
const handle = {
postedIndex,
subscription: observable.subscribe()
};
return handle;
}
...and then allow me to manage concurrent playback with an external/public interface like this?:
const handles = {};
const paused = {};
onAddNewRecordingRequest(id, timeline) {
recordings[id] = timeline;
}
onBeginPlaybackRequest(id) {
handles[id] = beginPlayback(recordings[id]);
}
onPauseRequested(id) {
const handle = handles[id];
paused[id] = handle.postedIndex;
handle.subscription.unsubscribe() // dispose of observable
}
onResumeRequested(id) {
const alreadyThroughIndex = paused[id];
handles[id] = beginPlayback(recordings[id], alreadyThroughIndex);
delete paused[id];
}
onStopRequested(id) {
const handle = handles[id];
handle.subscription.unsubscribe() // dispose of observable
}
Is there also a way to automatically remove the observable and the handle when the observable is completed?

How long was an observable delayed for when using debounceTime?

In this example: https://rxviz.com/v/0oqKpbWJ the delay in time from the first interval to when a value is emitted from the debounceTime operator is 4 seconds.
Is there a way to know that/be able to log the window that a debounce has debounced for?
Yes, you need timeInterval operator https://rxjs.dev/api/operators/timeInterval
Put it after the debounceTime
Update:
okay, I got it. You need a custom operator for sure. Try this
import { fromEvent, OperatorFunction } from 'rxjs';
import { debounceTime, tap, map } from 'rxjs/operators';
const clicks = fromEvent(document, 'click');
const result = clicks.pipe(debounceTimeWithIntervalTracking(1000));
result.subscribe(x => console.log(x));
function debounceTimeWithIntervalTracking<T>(time: number): OperatorFunction<T, { value: T, delayedFor: number }> {
let startedTime = new Date().getTime();
let restart = true;
return src$ => src$.pipe(
tap(() => {
if (restart) {
startedTime = new Date().getTime();
}
restart = false;
}),
debounceTime(time),
map(value => {
const delayedFor = new Date().getTime() - startedTime;
restart = true;
return { value, delayedFor };
})
)
}

Combine Inner Concat Observables

I'm new to RxJS and having a mental block. My solution works, but I know it's a hack. What's the proper way to do the following?
// Desired Console Log: {x: 'abcd', y: 'efgh'}
const x = Rx.Observable.of('abcd').delay(1000);
const y = Rx.Observable.of('efgh').delay(1000);
Rx.Observable.concat(x,y)
.scan((acc, cur) => {
if (cur.includes('a')) {
acc.x = cur;
}
if (cur.includes('e')) {
acc.y = cur;
}
return acc
}, {x: false, y: false})
.filter(x => x.y === 'efgh')
.subscribe(x => console.log(x))
// {x: 'abcd', y:'efgh'}
Rx.Observable.concat(x,y)
.zipIterable(['x','y'], (v1, v2) => {
let result = {};
result[v2] = v1;
return result;
})
.toArray()
.map(objects => Object.assign({}, ...objects))
.subscribe(console.log.bind(console, '>> '));
Provided that the order is kept, i.e. the first element is x and the second y

Resources