Last active
April 11, 2024 12:29
-
-
Save hungtrn75/2804c68a5a5c2a95e2afdab2f7634c53 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, | |
}, | |
}); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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