Skip to content

Instantly share code, notes, and snippets.

@hungtrn75
Last active April 11, 2024 12:29
Show Gist options
  • Save hungtrn75/2804c68a5a5c2a95e2afdab2f7634c53 to your computer and use it in GitHub Desktop.
Save hungtrn75/2804c68a5a5c2a95e2afdab2f7634c53 to your computer and use it in GitHub Desktop.
import React, { useCallback, useEffect, useRef, useState } from "react";
import { Platform, StyleSheet, TouchableOpacity, View } from "react-native";
import Svg, { Circle, Line, Path, Rect } from "react-native-svg";
import Animated, {
runOnJS,
useAnimatedGestureHandler,
useAnimatedProps,
useAnimatedStyle,
useDerivedValue,
useSharedValue,
} from "react-native-reanimated";
import { PanGestureHandler } from "react-native-gesture-handler";
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
import Colors from "react-native/Libraries/NewAppScreen/components/Colors";
import { ReText } from "react-native-redash";
import * as turf from "@turf/turf";
const AnimatedCircle = Animated.createAnimatedComponent(Circle);
const AnimatedPath = Animated.createAnimatedComponent(Path);
const AnimatedLine = Animated.createAnimatedComponent(Line);
const AnimatedRect = Animated.createAnimatedComponent(Rect);
export const ACTION_TYPE = {
NONE: "none",
LINE: "line",
RECTANGLE: "rectangle",
CIRCLE: "circle",
};
export const MAP_TYPE = {
MAPBOX: "mapbox",
GOOGLE_MAP: "google_map",
};
const Canvas = ({ mapRef, mapType = MAP_TYPE.GOOGLE_MAP, onDrawEnd }) => {
const panRef = useRef();
const [action, setAction] = useState(ACTION_TYPE.NONE);
// Constant
const minusPlatform = useSharedValue(Platform.select({
ios: 10.5,
android: 15.5,
}));
const factor = useSharedValue(0);
// LINE
const d = useSharedValue("");
const memoPoints = useSharedValue([]);
// CIRCLE
const cx = useSharedValue(0);
const cy = useSharedValue(0);
const r = useSharedValue(0);
//RECTANGLE
const rx = useSharedValue(0);
const ry = useSharedValue(0);
const rw = useSharedValue(0);
const rh = useSharedValue(0);
const bootstrap = async () => {
if (mapType === MAP_TYPE.GOOGLE_MAP) {
const p1 = await mapRef?.current?.coordinateForPoint({
x: 0,
y: 0,
});
const p2 = await mapRef?.current?.coordinateForPoint({
x: 100,
y: 0,
});
const c1 = turf.point([p1.longitude, p1.latitude]);
const c2 = turf.point([p2.longitude, p2.latitude]);
factor.value = turf.distance(c1, c2, {
units: "kilometers",
});
} else if (mapType === MAP_TYPE.MAPBOX) {
const p1 = await mapRef?.current?.getCoordinateFromView([0, 0]);
const p2 = await mapRef?.current?.getCoordinateFromView([100, 0]);
const c1 = turf.point(p1);
const c2 = turf.point(p2);
factor.value = turf.distance(c1, c2, {
units: "kilometers",
});
}
};
useEffect(() => {
d.value = "";
memoPoints.value = [];
cx.value = 0;
cy.value = 0;
r.value = 0;
rw.value = 0;
rh.value = 0;
rx.value = 0;
ry.value = 0;
if (action === ACTION_TYPE.CIRCLE || action === ACTION_TYPE.RECTANGLE) {
bootstrap();
}
}, [action]);
const onPressAction = useCallback(val => () => {
setAction(action === val ? ACTION_TYPE.NONE : val);
}, [action]);
const onDrawLineEnd = async (mPoints) => {
if (mapType === MAP_TYPE.GOOGLE_MAP) {
const coordinates = await Promise.all(mPoints.map(async el => {
return mapRef?.current?.coordinateForPoint({
x: el.x,
y: el.y,
});
}));
const points = turf.featureCollection(coordinates.map(el => turf.point([el.longitude, el.latitude])));
const shape = turf.convex(points, {
concavity: 1,
});
onDrawEnd({
type: ACTION_TYPE.LINE,
payload: {
coordinates: shape.geometry.coordinates[0].map(el => ({
latitude: el[1],
longitude: el[0],
})),
},
});
} else {
const coordinates = await Promise.all(mPoints.map(async el => {
return mapRef?.current?.getCoordinateFromView([el.x, el.y]);
}));
const points = turf.featureCollection(coordinates.map(el => turf.point(el)));
const shape = turf.convex(points, {
concavity: 1,
});
onDrawEnd({
type: ACTION_TYPE.LINE,
payload: shape,
});
}
};
const onDrawCircleEnd = async (mPoint, radius) => {
if (mapType === MAP_TYPE.GOOGLE_MAP) {
const center = await mapRef?.current?.coordinateForPoint({
x: mPoint.x,
y: mPoint.y,
});
onDrawEnd({
type: ACTION_TYPE.CIRCLE,
payload: {
center,
radius,
},
});
} else {
const center = await mapRef?.current?.getCoordinateFromView([mPoint.x, mPoint.y]);
const shape = turf.circle(center, radius, {
units: "meters",
});
onDrawEnd({
type: ACTION_TYPE.CIRCLE,
payload: shape,
});
}
};
const onDrawRectEnd = async (mPoint, width, height) => {
if (mapType === MAP_TYPE.GOOGLE_MAP) {
const p1 = await mapRef?.current?.coordinateForPoint({ x: mPoint.x, y: mPoint.y });
const p3 = await mapRef?.current?.getCoordinateFromView({ x: mPoint.x + width, y: mPoint.y + height });
const p2 = {
longitude: p1.longitude,
latitude: p3.latitude,
};
const p4 = {
longitude: p3.longitude,
latitude: p1.latitude,
};
const coordinates = [p1, p2, p3, p4];
onDrawEnd({
type: ACTION_TYPE.LINE,
payload: {
coordinates,
},
});
} else {
const p1 = await mapRef?.current?.getCoordinateFromView([mPoint.x, mPoint.y]);
const p3 = await mapRef?.current?.getCoordinateFromView([mPoint.x + width, mPoint.y + height]);
const p2 = [p1[0], p3[1]];
const p4 = [p3[0], p1[1]];
const points = [p1, p2, p3, p4, p1];
const shape = turf.polygon([points]);
return onDrawEnd({
type: ACTION_TYPE.LINE,
payload: shape,
});
}
};
const panHandler = useAnimatedGestureHandler({
onStart: (evt, ctx) => {
switch (action) {
case ACTION_TYPE.LINE:
memoPoints.value = [{ x: evt.x, y: evt.y }];
d.value = `M${evt.x} ${evt.y}`;
break;
case ACTION_TYPE.CIRCLE:
cx.value = evt.x;
cy.value = evt.y;
break;
case ACTION_TYPE.RECTANGLE:
rx.value = evt.x;
ry.value = evt.y;
ctx.x = evt.x;
ctx.y = evt.y;
break;
default:
break;
}
},
onActive: (evt, ctx) => {
switch (action) {
case ACTION_TYPE.LINE:
d.value += ` L${evt.x} ${evt.y}`;
memoPoints.value.push({ x: evt.x, y: evt.y });
break;
case ACTION_TYPE.CIRCLE:
const x = evt.translationX;
const y = evt.translationY;
r.value = Math.sqrt(x * x + y * y);
break;
case ACTION_TYPE.RECTANGLE:
rw.value = (evt.x - ctx.x);
rh.value = (evt.y - ctx.y);
break;
default:
break;
}
},
onEnd: (evt, ctx) => {
switch (action) {
case ACTION_TYPE.LINE:
d.value += ` L${evt.x} ${evt.y}`;
memoPoints.value.push({ x: evt.x, y: evt.y });
runOnJS(onDrawLineEnd)(memoPoints.value);
break;
case ACTION_TYPE.CIRCLE:
runOnJS(onDrawCircleEnd)({ x: cx.value, y: cy.value }, r.value * factor.value * 10);
break;
case ACTION_TYPE.RECTANGLE:
runOnJS(onDrawRectEnd)({ x: rx.value, y: ry.value }, rw.value, rh.value);
break;
default:
break;
}
runOnJS(setAction)(ACTION_TYPE.NONE);
},
}, [action]);
const animatedLineProps = useAnimatedProps(() => {
return {
d: d.value,
};
});
const animatedCircleProps = useAnimatedProps(() => {
return {
cx: `${cx.value}`,
cy: `${cy.value}`,
r: `${r.value}`,
};
}, [cx, cy, r]);
const animatedRProps = useAnimatedProps(() => {
return {
x1: cx.value,
y1: cy.value,
y2: cy.value,
x2: cx.value + r.value,
};
}, [cx, cy, r]);
const animatedC1Props = useAnimatedProps(() => {
return {
cx: `${cx.value}`,
cy: `${cy.value}`,
opacity: r.value > 0 ? 1 : 0,
};
}, [cx, cy, r]);
const animatedC2Props = useAnimatedProps(() => {
return {
cx: `${cx.value + r.value}`,
cy: `${cy.value}`,
opacity: r.value > 0 ? 1 : 0,
};
}, [cx, cy, r]);
const animatedRTextStyle = useAnimatedStyle(() => {
return {
left: cx.value,
top: cy.value - minusPlatform.value,
width: r.value,
opacity: r.value > 90 ? 1 : 0,
};
}, [cx, cy, r]);
const animatedWTextStyle = useAnimatedStyle(() => {
const aw = Math.abs(rw.value);
return {
left: rw.value < 0 ? rx.value + rw.value : rx.value,
top: ry.value,
width: aw,
opacity: aw > 90 ? 1 : 0,
};
}, [rw, rh, rx, ry]);
const animatedHTextStyle = useAnimatedStyle(() => {
const ah = Math.abs(rh.value);
return {
left: rx.value,
top: rh.value < 0 ? ry.value + rh.value : ry.value,
height: ah,
opacity: Math.abs(rh.value) > 60 ? 1 : 0,
};
}, [rw, rh, rx, ry]);
const animatedRectProps = useAnimatedStyle(() => {
return {
x: rx.value,
y: ry.value,
width: rw.value,
height: rh.value,
opacity: rw.value !== 0 && rh.value !== 0 ? 1: 0,
};
}, [rw, rh, rx, ry]);
const distanceStr = useDerivedValue(() => `${(r.value * factor.value / 100).toFixed(2)}km`, [r, factor]);
const widthStr = useDerivedValue(() => `${(Math.abs(rw.value) * factor.value / 100).toFixed(2)}km`, [rw, factor]);
const heightStr = useDerivedValue(() => `${(Math.abs(rh.value) * factor.value / 100).toFixed(2)}km`, [rh, factor]);
return (
<>
<View style={StyleSheet.absoluteFillObject} pointerEvents={"box-none"}>
{action !== ACTION_TYPE.NONE ?
<>
<Svg style={{
...StyleSheet.absoluteFillObject,
}}>
<AnimatedPath
animatedProps={animatedLineProps}
fill="none"
stroke={Colors.primary}
strokeWidth={2}
strokeLinecap={"round"}
/>
<AnimatedCircle
animatedProps={animatedCircleProps}
strokeWidth={2}
stroke={Colors.primary}
fill={Colors.primary}
fillOpacity={0.3}
/>
<AnimatedLine animatedProps={animatedRProps} stroke={Colors.primary} strokeWidth="2"
strokeDasharray={[4, 4]} />
<AnimatedCircle
animatedProps={animatedC1Props}
r={3.5}
fill={Colors.primary}
/>
<AnimatedCircle
animatedProps={animatedC2Props}
r={3.5}
fill={Colors.primary}
/>
<AnimatedRect
animatedProps={animatedRectProps}
fill={Colors.primary}
fillOpacity={0.3}
stroke={Colors.primary}
strokeWidth={2}
strokeDasharray={[4, 4]}
/>
</Svg>
<PanGestureHandler ref={panRef} onGestureEvent={panHandler} minDist={0}>
<Animated.View style={{ flex: 1 }}>
</Animated.View>
</PanGestureHandler>
</>
: null
}
<Animated.View style={[styles.v3, animatedRTextStyle]}>
<View style={styles.v1}>
<ReText text={distanceStr} style={styles.t3} />
</View>
</Animated.View>
<Animated.View style={[styles.v3, styles.v5, animatedWTextStyle]}>
<View style={styles.v1}>
<ReText text={widthStr} style={styles.t3} />
</View>
</Animated.View>
<Animated.View style={[styles.v3, styles.v4, animatedHTextStyle]}>
<View style={[styles.v1, {
// transform: [{ rotate: "90deg" }],
}]}>
<ReText text={heightStr} style={styles.t3} />
</View>
</Animated.View>
</View>
<View style={styles.v2}>
<ActionButton icon={"shape-rectangle-plus"} active={action === ACTION_TYPE.RECTANGLE}
onPress={onPressAction(ACTION_TYPE.RECTANGLE)} />
<ActionButton icon={"shape-polygon-plus"} active={action === ACTION_TYPE.LINE}
onPress={onPressAction(ACTION_TYPE.LINE)} />
<ActionButton icon={"vector-circle-variant"} active={action === ACTION_TYPE.CIRCLE}
onPress={onPressAction(ACTION_TYPE.CIRCLE)} />
</View>
</>
);
};
export default Canvas;
const ActionButton = ({
icon,
active = false,
onPress,
}) => {
return (
<TouchableOpacity style={[styles.t1, { backgroundColor: active ? Colors.primary : "white" }]} onPress={onPress}>
<Icon name={icon} size={18} color={active ? "white" : "black"} />
</TouchableOpacity>
);
};
const styles = StyleSheet.create({
v1: {
backgroundColor: Colors.primary,
paddingHorizontal: 6.5,
paddingVertical: 1.5,
borderRadius: 6,
},
v2: {
position: "absolute",
bottom: 15,
right: 10,
backgroundColor: "white",
padding: 5,
borderRadius: 5,
},
v3: {
position: "absolute",
justifyContent: "center",
alignItems: "center",
},
v4: {
paddingHorizontal: 10,
},
v5: {
paddingVertical: 10,
},
t1: {
padding: 5,
borderRadius: 5,
},
t3: {
fontSize: 13,
color: "white",
padding: 0,
margin: 0,
},
});
import React, { useMemo, useRef, useState } from "react";
import { StyleSheet } from "react-native";
import { SafeAreaView } from "react-native-safe-area-context";
import { Circle, Polygon } from "react-native-maps";
import Canvas, { ACTION_TYPE, MAP_TYPE } from "./index";
import Colors from "react-native/Libraries/NewAppScreen/components/Colors";
import MapboxGL from "@react-native-mapbox-gl/maps";
const CustomMap = () => {
const mapRef = useRef();
const [action, setAction] = useState(null);
const onDrawEnd = (action) => {
setAction(action);
};
const getActionView = () => {
console.log(action?.payload);
switch (action?.type) {
case ACTION_TYPE.CIRCLE:
return <Circle center={action.payload.center} radius={action.payload.radius} strokeColor={Colors.primary}
strokeWidth={2} fillColor={"rgba(18, 146, 180, 0.3)"} />;
case ACTION_TYPE.LINE:
return <Polygon coordinates={action.payload.coordinates} strokeWidth={2} strokeColor={Colors.primary}
fillColor={"rgba(18, 146, 180, 0.3)"} />;
default:
return null;
}
};
const shape = useMemo(() => {
switch (action?.type) {
case ACTION_TYPE.CIRCLE:
return action.payload;
case ACTION_TYPE.LINE:
return action.payload;
default:
return null;
}
}, [action]);
return (
<SafeAreaView edges={["bottom"]} mode={"margin"} style={{
flex: 1,
}}>
{/*<MapView*/}
{/* ref={mapRef}*/}
{/* style={StyleSheet.absoluteFillObject}*/}
{/* initialRegion={{*/}
{/* latitude: 37.78825,*/}
{/* longitude: -122.4324,*/}
{/* latitudeDelta: 0.0922,*/}
{/* longitudeDelta: 0.0421,*/}
{/* }}*/}
{/*>*/}
{/* {getActionView()}*/}
{/*</MapView>*/}
<MapboxGL.MapView style={StyleSheet.absoluteFillObject} ref={mapRef}>
<MapboxGL.Camera
zoomLevel={initCamera.zoomLevel}
followUserLocation={false}
animationMode="moveTo"
centerCoordinate={initCamera.centerCoordinate}
/>
{shape ? <MapboxGL.ShapeSource id={"canvas-source"} shape={shape}>
<MapboxGL.FillLayer id={"fill-layer"} style={{
fillColor: Colors.primary,
fillOpacity: 0.3,
fillOutlineColor: Colors.primary,
}
} />
<MapboxGL.LineLayer id={"line-layer"} style={{
lineWidth: 2,
lineColor: Colors.primary,
}} />
</MapboxGL.ShapeSource> : null}
</MapboxGL.MapView>
<Canvas mapRef={mapRef} onDrawEnd={onDrawEnd} mapType={MAP_TYPE.MAPBOX} />
</SafeAreaView>
);
};
export default CustomMap;
const initCamera = {
centerCoordinate: [78.92767958437497, 22.521597693584795],
zoomLevel: 10,
animationDuration: 0,
};
const styles = StyleSheet.create({});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment