Files
SeaGateway-App/app/(tabs)/index.tsx
2025-11-04 16:48:01 +07:00

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",
},
});