502 lines
15 KiB
TypeScript
502 lines
15 KiB
TypeScript
import GPSInfoPanel from "@/components/map/GPSInfoPanel";
|
|
import type { PolygonWithLabelProps } from "@/components/map/PolygonWithLabel";
|
|
import { PolygonWithLabel } from "@/components/map/PolygonWithLabel";
|
|
import type { PolylineWithLabelProps } from "@/components/map/PolylineWithLabel";
|
|
import { PolylineWithLabel } from "@/components/map/PolylineWithLabel";
|
|
import SosButton from "@/components/map/SosButton";
|
|
import {
|
|
ENTITY,
|
|
EVENT_ALARM_DATA,
|
|
EVENT_BANZONE_DATA,
|
|
EVENT_ENTITY_DATA,
|
|
EVENT_GPS_DATA,
|
|
EVENT_TRACK_POINTS_DATA,
|
|
IOS_PLATFORM,
|
|
LIGHT_THEME,
|
|
} from "@/constants";
|
|
import { useColorScheme } from "@/hooks/use-color-scheme.web";
|
|
import { usePlatform } from "@/hooks/use-platform";
|
|
import {
|
|
getAlarmEventBus,
|
|
getBanzonesEventBus,
|
|
getEntitiesEventBus,
|
|
getGpsEventBus,
|
|
getTrackPointsEventBus,
|
|
} from "@/services/device_events";
|
|
import { getShipIcon } from "@/services/map_service";
|
|
import eventBus from "@/utils/eventBus";
|
|
import {
|
|
convertWKTLineStringToLatLngArray,
|
|
convertWKTtoLatLngString,
|
|
} from "@/utils/geom";
|
|
import { useEffect, useRef, useState } from "react";
|
|
import {
|
|
Animated,
|
|
Image as RNImage,
|
|
StyleSheet,
|
|
View
|
|
} from "react-native";
|
|
import MapView, { Circle, Marker } from "react-native-maps";
|
|
|
|
|
|
export default function HomeScreen() {
|
|
const [gpsData, setGpsData] = useState<Model.GPSResonse | undefined>(
|
|
undefined
|
|
);
|
|
const [alarmData, setAlarmData] = useState<Model.AlarmResponse | null>(null);
|
|
const [entityData, setEntityData] = useState<
|
|
Model.TransformedEntity[] | null
|
|
>(null);
|
|
const [banzoneData, setBanzoneData] = useState<Model.Zone[] | null>(null);
|
|
const [trackPointsData, setTrackPointsData] = useState<
|
|
Model.ShipTrackPoint[] | null
|
|
>(null);
|
|
const [circleRadius, setCircleRadius] = useState(100);
|
|
const [zoomLevel, setZoomLevel] = useState(10);
|
|
const [isFirstLoad, setIsFirstLoad] = useState(true);
|
|
const [polylineCoordinates, setPolylineCoordinates] = useState<
|
|
PolylineWithLabelProps | null
|
|
>(null);
|
|
const [polygonCoordinates, setPolygonCoordinates] = useState<
|
|
PolygonWithLabelProps[]
|
|
>([]);
|
|
const platform = usePlatform();
|
|
const theme = useColorScheme();
|
|
const scale = useRef(new Animated.Value(0)).current;
|
|
const opacity = useRef(new Animated.Value(1)).current;
|
|
|
|
// console.log("Platform: ", platform);
|
|
// console.log("Theme: ", theme);
|
|
|
|
// const [number, setNumber] = useState(0);
|
|
|
|
useEffect(() => {
|
|
getGpsEventBus();
|
|
getAlarmEventBus();
|
|
getEntitiesEventBus();
|
|
getBanzonesEventBus();
|
|
getTrackPointsEventBus();
|
|
const queryGpsData = (gpsData: Model.GPSResonse) => {
|
|
if (gpsData) {
|
|
// console.log("GPS Data: ", gpsData);
|
|
setGpsData(gpsData);
|
|
} else {
|
|
setGpsData(undefined);
|
|
setPolygonCoordinates([]);
|
|
setPolylineCoordinates(null);
|
|
}
|
|
};
|
|
const queryAlarmData = (alarmData: Model.AlarmResponse) => {
|
|
// console.log("Alarm Data: ", alarmData.alarms.length);
|
|
setAlarmData(alarmData);
|
|
};
|
|
const queryEntityData = (entityData: Model.TransformedEntity[]) => {
|
|
// console.log("Entities Length Data: ", entityData.length);
|
|
setEntityData(entityData);
|
|
};
|
|
const queryBanzonesData = (banzoneData: Model.Zone[]) => {
|
|
// console.log("Banzone Data: ", banzoneData.length);
|
|
|
|
setBanzoneData(banzoneData);
|
|
};
|
|
const queryTrackPointsData = (TrackPointsData: Model.ShipTrackPoint[]) => {
|
|
// console.log("TrackPoints Data: ", TrackPointsData.length);
|
|
if (TrackPointsData && TrackPointsData.length > 0) {
|
|
setTrackPointsData(TrackPointsData);
|
|
} else {
|
|
setTrackPointsData(null);
|
|
}
|
|
};
|
|
|
|
eventBus.on(EVENT_GPS_DATA, queryGpsData);
|
|
// console.log("Registering event handlers in HomeScreen");
|
|
eventBus.on(EVENT_GPS_DATA, queryGpsData);
|
|
// console.log("Subscribed to EVENT_GPS_DATA");
|
|
eventBus.on(EVENT_ALARM_DATA, queryAlarmData);
|
|
// console.log("Subscribed to EVENT_ALARM_DATA");
|
|
eventBus.on(EVENT_ENTITY_DATA, queryEntityData);
|
|
// console.log("Subscribed to EVENT_ENTITY_DATA");
|
|
eventBus.on(EVENT_TRACK_POINTS_DATA, queryTrackPointsData);
|
|
// console.log("Subscribed to EVENT_TRACK_POINTS_DATA");
|
|
eventBus.once(EVENT_BANZONE_DATA, queryBanzonesData);
|
|
// console.log("Subscribed once to EVENT_BANZONE_DATA");
|
|
|
|
return () => {
|
|
// console.log("Unregistering event handlers in HomeScreen");
|
|
eventBus.off(EVENT_GPS_DATA, queryGpsData);
|
|
// console.log("Unsubscribed EVENT_GPS_DATA");
|
|
eventBus.off(EVENT_ALARM_DATA, queryAlarmData);
|
|
// console.log("Unsubscribed EVENT_ALARM_DATA");
|
|
eventBus.off(EVENT_ENTITY_DATA, queryEntityData);
|
|
// console.log("Unsubscribed EVENT_ENTITY_DATA");
|
|
eventBus.off(EVENT_TRACK_POINTS_DATA, queryTrackPointsData);
|
|
// console.log("Unsubscribed EVENT_TRACK_POINTS_DATA");
|
|
};
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
if (polylineCoordinates !== null) {
|
|
console.log("Polyline Khac null");
|
|
} else {
|
|
console.log("Polyline null");
|
|
}
|
|
}, [polylineCoordinates]);
|
|
|
|
useEffect(() => {
|
|
if (polygonCoordinates.length > 0) {
|
|
console.log("Polygon Khac null");
|
|
} else {
|
|
console.log("Polygon null");
|
|
}
|
|
}, [polygonCoordinates]);
|
|
|
|
useEffect(() => {
|
|
setPolylineCoordinates(null);
|
|
setPolygonCoordinates([]);
|
|
if (!entityData) return;
|
|
if (!banzoneData) return;
|
|
for (const entity of entityData) {
|
|
if (entity.id !== ENTITY.ZONE_ALARM_LIST) {
|
|
continue;
|
|
}
|
|
|
|
let zones: any[] = [];
|
|
try {
|
|
zones = entity.valueString ? JSON.parse(entity.valueString) : [];
|
|
} catch (parseError) {
|
|
console.error("Error parsing zone list:", parseError);
|
|
continue;
|
|
}
|
|
// Nếu danh sách zone rỗng, clear tất cả
|
|
if (zones.length === 0) {
|
|
setPolylineCoordinates(null);
|
|
setPolygonCoordinates([]);
|
|
return;
|
|
}
|
|
|
|
for (const zone of zones) {
|
|
console.log("Zone Data: ", zone);
|
|
const geom = banzoneData.find((b) => b.id === zone.zone_id);
|
|
if (!geom) {
|
|
continue;
|
|
}
|
|
const { geom_type, geom_lines, geom_poly } = geom.geom || {};
|
|
if (typeof geom_type !== "number") {
|
|
continue;
|
|
}
|
|
if (geom_type === 2) {
|
|
// if(oldEntityData.find(e => e.id === ))
|
|
// foundPolyline = true;
|
|
const coordinates = convertWKTLineStringToLatLngArray(
|
|
geom_lines || ""
|
|
);
|
|
if (coordinates.length > 0) {
|
|
setPolylineCoordinates({
|
|
coordinates: coordinates.map((coord) => ({
|
|
latitude: coord[0],
|
|
longitude: coord[1],
|
|
})),
|
|
label: zone?.zone_name ?? "",
|
|
content: zone?.message ?? "",
|
|
});
|
|
|
|
}
|
|
} else if (geom_type === 1) {
|
|
// foundPolygon = true;
|
|
const coordinates = convertWKTtoLatLngString(geom_poly || "");
|
|
if (coordinates.length > 0) {
|
|
console.log("Polygon Coordinate: ", coordinates);
|
|
setPolygonCoordinates(
|
|
coordinates.map((polygon) => ({
|
|
coordinates: polygon.map((coord) => ({
|
|
latitude: coord[0],
|
|
longitude: coord[1],
|
|
})),
|
|
label: zone?.zone_name ?? "",
|
|
content: zone?.message ?? "",
|
|
}))
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}, [banzoneData, entityData]);
|
|
|
|
// Hàm tính radius cố định khi zoom change
|
|
const calculateRadiusFromZoom = (zoom: number) => {
|
|
const baseZoom = 10;
|
|
const baseRadius = 100;
|
|
const zoomDifference = baseZoom - zoom;
|
|
const calculatedRadius = baseRadius * Math.pow(2, zoomDifference);
|
|
// console.log("Caculate Radius: ", calculatedRadius);
|
|
|
|
return Math.max(calculatedRadius, 50);
|
|
};
|
|
|
|
// Xử lý khi region (zoom) thay đổi
|
|
const handleRegionChangeComplete = (newRegion: any) => {
|
|
// Tính zoom level từ latitudeDelta
|
|
// zoom = log2(360 / (latitudeDelta * 2)) + 8
|
|
const zoom = Math.round(Math.log2(360 / (newRegion.latitudeDelta * 2)) + 8);
|
|
const newRadius = calculateRadiusFromZoom(zoom);
|
|
setCircleRadius(newRadius);
|
|
setZoomLevel(zoom);
|
|
// console.log("Zoom level:", zoom, "Circle radius:", newRadius);
|
|
};
|
|
|
|
// Tính toán region để focus vào marker của tàu (chỉ lần đầu tiên)
|
|
const getMapRegion = () => {
|
|
if (!isFirstLoad) {
|
|
// Sau lần đầu, return undefined để không force region
|
|
return undefined;
|
|
}
|
|
if (!gpsData) {
|
|
return {
|
|
latitude: 15.70581,
|
|
longitude: 116.152685,
|
|
latitudeDelta: 0.05,
|
|
longitudeDelta: 0.05,
|
|
};
|
|
}
|
|
|
|
return {
|
|
latitude: gpsData.lat,
|
|
longitude: gpsData.lon,
|
|
latitudeDelta: 0.05,
|
|
longitudeDelta: 0.05,
|
|
};
|
|
};
|
|
|
|
const handleMapReady = () => {
|
|
setTimeout(() => {
|
|
setIsFirstLoad(false);
|
|
}, 2000);
|
|
};
|
|
|
|
useEffect(() => {
|
|
if (alarmData?.level === 3) {
|
|
const loop = Animated.loop(
|
|
Animated.sequence([
|
|
Animated.parallel([
|
|
Animated.timing(scale, {
|
|
toValue: 3, // nở to 3 lần
|
|
duration: 1500,
|
|
useNativeDriver: true,
|
|
}),
|
|
Animated.timing(opacity, {
|
|
toValue: 0, // mờ dần
|
|
duration: 1500,
|
|
useNativeDriver: true,
|
|
}),
|
|
]),
|
|
Animated.parallel([
|
|
Animated.timing(scale, {
|
|
toValue: 0,
|
|
duration: 0,
|
|
useNativeDriver: true,
|
|
}),
|
|
Animated.timing(opacity, {
|
|
toValue: 1,
|
|
duration: 0,
|
|
useNativeDriver: true,
|
|
}),
|
|
]),
|
|
])
|
|
);
|
|
loop.start();
|
|
return () => loop.stop();
|
|
}
|
|
}, [alarmData?.level, scale, opacity]);
|
|
|
|
return (
|
|
<View
|
|
// edges={["top"]}
|
|
style={styles.container}>
|
|
<MapView
|
|
onMapReady={handleMapReady}
|
|
onRegionChangeComplete={handleRegionChangeComplete}
|
|
style={styles.map}
|
|
// initialRegion={getMapRegion()}
|
|
region={getMapRegion()}
|
|
userInterfaceStyle={theme === LIGHT_THEME ? "light" : "dark"}
|
|
showsBuildings={false}
|
|
showsIndoors={false}
|
|
loadingEnabled={true}
|
|
mapType={platform === IOS_PLATFORM ? "mutedStandard" : "standard"}
|
|
rotateEnabled={false}
|
|
>
|
|
{trackPointsData &&
|
|
trackPointsData.length > 0 &&
|
|
trackPointsData.map((point, index) => {
|
|
// console.log(`Rendering circle ${index}:`, point);
|
|
return (
|
|
<Circle
|
|
key={index}
|
|
center={{
|
|
latitude: point.lat,
|
|
longitude: point.lon,
|
|
}}
|
|
zIndex={50}
|
|
// radius={platform === IOS_PLATFORM ? 200 : 50}
|
|
radius={circleRadius}
|
|
strokeColor="rgba(16, 85, 201, 0.7)"
|
|
fillColor="rgba(16, 85, 201, 0.7)"
|
|
strokeWidth={2}
|
|
/>
|
|
);
|
|
})}
|
|
{polylineCoordinates && (
|
|
<PolylineWithLabel
|
|
coordinates={polylineCoordinates.coordinates}
|
|
label={polylineCoordinates.label}
|
|
content={polylineCoordinates.content}
|
|
strokeColor="#FF5733"
|
|
strokeWidth={4}
|
|
showDistance={false}
|
|
zIndex={50}
|
|
/>
|
|
)}
|
|
{polygonCoordinates.length > 0 && (
|
|
<>
|
|
{polygonCoordinates.map((polygon, index) => {
|
|
// Tạo key ổn định từ tọa độ đầu tiên của polygon
|
|
const polygonKey =
|
|
polygon.coordinates.length > 0
|
|
? `polygon-${polygon.coordinates[0].latitude}-${polygon.coordinates[0].longitude}-${index}`
|
|
: `polygon-${index}`;
|
|
|
|
return (
|
|
<PolygonWithLabel
|
|
key={polygonKey}
|
|
coordinates={polygon.coordinates}
|
|
label={polygon.label}
|
|
content={polygon.content}
|
|
fillColor="rgba(16, 85, 201, 0.6)"
|
|
strokeColor="rgba(16, 85, 201, 0.8)"
|
|
strokeWidth={2}
|
|
zIndex={50}
|
|
zoomLevel={zoomLevel}
|
|
/>
|
|
);
|
|
})}
|
|
</>
|
|
)}
|
|
{gpsData !== undefined && (
|
|
<Marker
|
|
coordinate={{
|
|
latitude: gpsData.lat,
|
|
longitude: gpsData.lon,
|
|
}}
|
|
title={
|
|
platform === IOS_PLATFORM
|
|
? "Tàu của mình - iOS"
|
|
: "Tàu của mình - Android"
|
|
}
|
|
zIndex={200}
|
|
anchor={
|
|
platform === IOS_PLATFORM
|
|
? { x: 0.5, y: 0.5 }
|
|
: { x: 0.6, y: 0.4 }
|
|
}
|
|
>
|
|
<View className="w-8 h-8 items-center justify-center">
|
|
<View style={styles.pingContainer}>
|
|
{alarmData?.level === 3 && (
|
|
<Animated.View
|
|
style={[
|
|
styles.pingCircle,
|
|
{
|
|
transform: [{ scale }],
|
|
opacity,
|
|
},
|
|
]}
|
|
/>
|
|
)}
|
|
<RNImage
|
|
source={(() => {
|
|
const icon = getShipIcon(
|
|
alarmData?.level || 0,
|
|
gpsData.fishing
|
|
);
|
|
return typeof icon === "string" ? { uri: icon } : icon;
|
|
})()}
|
|
style={{
|
|
width: 32,
|
|
height: 32,
|
|
transform: [
|
|
{
|
|
rotate: `${typeof gpsData.h === "number" && !isNaN(gpsData.h)
|
|
? gpsData.h
|
|
: 0
|
|
}deg`,
|
|
},
|
|
],
|
|
}}
|
|
/>
|
|
</View>
|
|
</View>
|
|
</Marker>
|
|
)}
|
|
</MapView>
|
|
|
|
<View className="absolute top-14 right-2 shadow-md">
|
|
<SosButton />
|
|
</View>
|
|
<GPSInfoPanel gpsData={gpsData} />
|
|
</View>
|
|
);
|
|
}
|
|
|
|
const styles = StyleSheet.create({
|
|
container: {
|
|
flex: 1,
|
|
},
|
|
map: {
|
|
flex: 1,
|
|
},
|
|
button: {
|
|
// display: "none",
|
|
position: "absolute",
|
|
top: 50,
|
|
right: 20,
|
|
backgroundColor: "#007AFF",
|
|
paddingHorizontal: 16,
|
|
paddingVertical: 12,
|
|
borderRadius: 8,
|
|
elevation: 5,
|
|
shadowColor: "#000",
|
|
shadowOffset: {
|
|
width: 0,
|
|
height: 2,
|
|
},
|
|
shadowOpacity: 0.25,
|
|
shadowRadius: 3.84,
|
|
},
|
|
buttonText: {
|
|
color: "#fff",
|
|
fontSize: 16,
|
|
fontWeight: "600",
|
|
},
|
|
|
|
pingContainer: {
|
|
width: 32,
|
|
height: 32,
|
|
alignItems: "center",
|
|
justifyContent: "center",
|
|
overflow: "visible",
|
|
},
|
|
pingCircle: {
|
|
position: "absolute",
|
|
width: 40,
|
|
height: 40,
|
|
borderRadius: 20,
|
|
backgroundColor: "#ED3F27",
|
|
},
|
|
centerDot: {
|
|
width: 20,
|
|
height: 20,
|
|
borderRadius: 10,
|
|
backgroundColor: "#0096FF",
|
|
},
|
|
});
|