582 lines
17 KiB
TypeScript
582 lines
17 KiB
TypeScript
import { PolygonWithLabel } from "@/components/map/PolygonWithLabel";
|
|
import { PolylineWithLabel } from "@/components/map/PolylineWithLabel";
|
|
import { showToastError } from "@/config";
|
|
import {
|
|
AUTO_REFRESH_INTERVAL,
|
|
ENTITY,
|
|
IOS_PLATFORM,
|
|
LIGHT_THEME,
|
|
} from "@/constants";
|
|
import {
|
|
queryAlarm,
|
|
queryEntities,
|
|
queryGpsData,
|
|
queryTrackPoints,
|
|
} from "@/controller/DeviceController";
|
|
import { useColorScheme } from "@/hooks/use-color-scheme.web";
|
|
import { usePlatform } from "@/hooks/use-platform";
|
|
import { getShipIcon } from "@/services/map_service";
|
|
import { useBanzones } from "@/state/use-banzones";
|
|
import {
|
|
convertWKTLineStringToLatLngArray,
|
|
convertWKTtoLatLngString,
|
|
} from "@/utils/geom";
|
|
import { Image as ExpoImage } from "expo-image";
|
|
import { useEffect, useRef, useState } from "react";
|
|
import { StyleSheet, Text, TouchableOpacity, View } from "react-native";
|
|
|
|
import MapView, { Circle, Marker } from "react-native-maps";
|
|
import { SafeAreaProvider } from "react-native-safe-area-context";
|
|
|
|
const testPolyline =
|
|
"MULTIPOLYGON(((108.7976074 17.5392966,110.390625 14.2217886,109.4677734 10.8548863,112.9227161 10.6933337,116.4383411 12.565622,116.8997669 17.0466095,109.8685169 17.8013229,108.7973446 17.5393669,108.7976074 17.5392966)))";
|
|
|
|
export default function HomeScreen() {
|
|
const [gpsData, setGpsData] = useState<Model.GPSResonse | null>(null);
|
|
const [alarmData, setAlarmData] = useState<Model.AlarmResponse | null>(null);
|
|
const [trackPoints, setTrackPoints] = useState<Model.ShipTrackPoint[] | null>(
|
|
null
|
|
);
|
|
const [circleRadius, setCircleRadius] = useState(100);
|
|
const [zoomLevel, setZoomLevel] = useState(10);
|
|
const [isFirstLoad, setIsFirstLoad] = useState(true);
|
|
const [polylineCoordinates, setPolylineCoordinates] = useState<
|
|
number[][] | null
|
|
>(null);
|
|
const [polygonCoordinates, setPolygonCoordinates] = useState<
|
|
number[][][] | null
|
|
>(null);
|
|
const [, setZoneGeometries] = useState<Map<string, any>>(new Map());
|
|
const intervalRef = useRef<ReturnType<typeof setInterval> | null>(null);
|
|
const platform = usePlatform();
|
|
const theme = useColorScheme();
|
|
const { banzones, getBanzone } = useBanzones();
|
|
const banzonesRef = useRef(banzones);
|
|
// console.log("Platform: ", platform);
|
|
// console.log("Theme: ", theme);
|
|
|
|
const getGpsData = async () => {
|
|
try {
|
|
const response = await queryGpsData();
|
|
// console.log("GpsData: ", response.data);
|
|
// console.log(
|
|
// "Heading value:",
|
|
// response.data?.h,
|
|
// "Type:",
|
|
// typeof response.data?.h
|
|
// );
|
|
setGpsData(response.data);
|
|
} catch (error) {
|
|
console.error("Error fetching GPS data:", error);
|
|
showToastError("Lỗi", "Không thể lấy dữ liệu GPS");
|
|
}
|
|
};
|
|
|
|
const drawPolyline = () => {
|
|
const data = convertWKTtoLatLngString(testPolyline);
|
|
console.log("Data: ", data);
|
|
// setPolygonCoordinates(data[0]);
|
|
// ;
|
|
// console.log("Banzones: ", banzones.length);
|
|
};
|
|
|
|
useEffect(() => {
|
|
banzonesRef.current = banzones;
|
|
}, [banzones]);
|
|
|
|
const areGeometriesEqual = (
|
|
left?: {
|
|
geom_type: number;
|
|
geom_lines?: string | null;
|
|
geom_poly?: string | null;
|
|
},
|
|
right?: {
|
|
geom_type: number;
|
|
geom_lines?: string | null;
|
|
geom_poly?: string | null;
|
|
}
|
|
) => {
|
|
if (!left && !right) {
|
|
return true;
|
|
}
|
|
|
|
if (!left || !right) {
|
|
return false;
|
|
}
|
|
|
|
return (
|
|
left.geom_type === right.geom_type &&
|
|
(left.geom_lines || "") === (right.geom_lines || "") &&
|
|
(left.geom_poly || "") === (right.geom_poly || "")
|
|
);
|
|
};
|
|
|
|
const areCoordinatesEqual = (
|
|
current: number[][] | null,
|
|
next: number[][] | null
|
|
) => {
|
|
if (!current || !next || current.length !== next.length) {
|
|
return false;
|
|
}
|
|
|
|
return current.every(
|
|
(coord, index) =>
|
|
coord[0] === next[index][0] && coord[1] === next[index][1]
|
|
);
|
|
};
|
|
|
|
const areMultiPolygonCoordinatesEqual = (
|
|
current: number[][][] | null,
|
|
next: number[][][] | null
|
|
) => {
|
|
if (!current || !next || current.length !== next.length) {
|
|
return false;
|
|
}
|
|
|
|
return current.every((polygon, polyIndex) => {
|
|
const nextPolygon = next[polyIndex];
|
|
if (!nextPolygon || polygon.length !== nextPolygon.length) {
|
|
return false;
|
|
}
|
|
return polygon.every(
|
|
(coord, coordIndex) =>
|
|
coord[0] === nextPolygon[coordIndex][0] &&
|
|
coord[1] === nextPolygon[coordIndex][1]
|
|
);
|
|
});
|
|
};
|
|
|
|
const getAlarmData = async () => {
|
|
try {
|
|
const response = await queryAlarm();
|
|
// console.log("AlarmData: ", response.data);
|
|
setAlarmData(response.data);
|
|
} catch (error) {
|
|
console.error("Error fetching Alarm Data: ", error);
|
|
showToastError("Lỗi", "Không thể lấy dữ liệu báo động");
|
|
}
|
|
};
|
|
|
|
const getEntities = async () => {
|
|
try {
|
|
const entities = await queryEntities();
|
|
if (!entities) {
|
|
// Clear tất cả khu vực khi không có dữ liệu
|
|
setPolylineCoordinates(null);
|
|
setPolygonCoordinates(null);
|
|
setZoneGeometries(new Map());
|
|
return;
|
|
}
|
|
|
|
const currentBanzones = banzonesRef.current || [];
|
|
let nextPolyline: number[][] | null = null;
|
|
let nextMultiPolygon: number[][][] | null = null;
|
|
let foundPolyline = false;
|
|
let foundPolygon = false;
|
|
|
|
// Process zones để tìm geometries
|
|
for (const entity of entities) {
|
|
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(null);
|
|
setZoneGeometries(new Map());
|
|
return;
|
|
}
|
|
|
|
for (const zone of zones) {
|
|
const geom = currentBanzones.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) {
|
|
foundPolyline = true;
|
|
const coordinates = convertWKTLineStringToLatLngArray(
|
|
geom_lines || ""
|
|
);
|
|
if (coordinates.length > 0) {
|
|
nextPolyline = coordinates;
|
|
}
|
|
} else if (geom_type === 1) {
|
|
foundPolygon = true;
|
|
const coordinates = convertWKTtoLatLngString(geom_poly || "");
|
|
if (coordinates.length > 0) {
|
|
console.log("Polygon Coordinate: ", coordinates);
|
|
nextMultiPolygon = coordinates;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Update state sau khi đã process xong
|
|
setZoneGeometries((prevGeometries) => {
|
|
const updated = new Map(prevGeometries);
|
|
let hasChanges = false;
|
|
|
|
for (const entity of entities) {
|
|
if (entity.id !== ENTITY.ZONE_ALARM_LIST) {
|
|
continue;
|
|
}
|
|
|
|
let zones: any[] = [];
|
|
try {
|
|
zones = entity.valueString ? JSON.parse(entity.valueString) : [];
|
|
} catch {
|
|
continue;
|
|
}
|
|
|
|
if (zones.length === 0) {
|
|
if (updated.size > 0) {
|
|
hasChanges = true;
|
|
updated.clear();
|
|
}
|
|
break;
|
|
}
|
|
|
|
for (const zone of zones) {
|
|
const geom = currentBanzones.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;
|
|
}
|
|
|
|
const key = `${zone.zone_id}_${geom_type}`;
|
|
const newGeomData = { geom_type, geom_lines, geom_poly };
|
|
const oldGeom = updated.get(key);
|
|
|
|
if (!areGeometriesEqual(oldGeom, newGeomData)) {
|
|
hasChanges = true;
|
|
updated.set(key, newGeomData);
|
|
console.log("Geometry changed", { key, oldGeom, newGeomData });
|
|
}
|
|
}
|
|
}
|
|
|
|
return hasChanges ? updated : prevGeometries;
|
|
});
|
|
|
|
// Cập nhật hoặc clear polyline
|
|
if (foundPolyline && nextPolyline) {
|
|
setPolylineCoordinates((prev) =>
|
|
areCoordinatesEqual(prev, nextPolyline) ? prev : nextPolyline
|
|
);
|
|
} else if (!foundPolyline) {
|
|
console.log("Hết cảnh báo qua polyline");
|
|
setPolylineCoordinates(null);
|
|
}
|
|
|
|
// Cập nhật hoặc clear polygon
|
|
if (foundPolygon && nextMultiPolygon) {
|
|
setPolygonCoordinates((prev) =>
|
|
areMultiPolygonCoordinatesEqual(prev, nextMultiPolygon)
|
|
? prev
|
|
: nextMultiPolygon
|
|
);
|
|
} else if (!foundPolygon) {
|
|
console.log("Hết cảnh báo qua polygon");
|
|
setPolygonCoordinates(null);
|
|
}
|
|
} catch (error) {
|
|
console.error("Error fetching Entities: ", error);
|
|
// Clear tất cả khi có lỗi
|
|
setPolylineCoordinates(null);
|
|
setPolygonCoordinates(null);
|
|
setZoneGeometries(new Map());
|
|
}
|
|
};
|
|
|
|
const getShipTrackPoints = async () => {
|
|
try {
|
|
const response = await queryTrackPoints();
|
|
// console.log("TrackPoints Data Length: ", response.data.length);
|
|
setTrackPoints(response.data);
|
|
} catch (error) {
|
|
console.error("Error fetching TrackPoints Data: ", error);
|
|
showToastError("Lỗi", "Không thể lấy lịch sử di chuyển");
|
|
}
|
|
};
|
|
|
|
const fetchAllData = async () => {
|
|
await Promise.all([
|
|
getGpsData(),
|
|
getAlarmData(),
|
|
getShipTrackPoints(),
|
|
getEntities(),
|
|
]);
|
|
};
|
|
|
|
const setupAutoRefresh = () => {
|
|
// Clear existing interval if any
|
|
if (intervalRef.current) {
|
|
clearInterval(intervalRef.current);
|
|
}
|
|
|
|
// Set new interval to refresh data every 5 seconds
|
|
// Không fetch banzones vì dữ liệu không thay đổi
|
|
intervalRef.current = setInterval(async () => {
|
|
// console.log("Auto-refreshing data...");
|
|
await fetchAllData();
|
|
}, AUTO_REFRESH_INTERVAL);
|
|
};
|
|
|
|
const handleMapReady = () => {
|
|
// console.log("Map loaded successfully!");
|
|
// Gọi fetchAllData ngay lập tức (không cần đợi banzones)
|
|
fetchAllData();
|
|
setupAutoRefresh();
|
|
// Set isFirstLoad to false sau khi map ready để chỉ zoom lần đầu tiên
|
|
setTimeout(() => {
|
|
setIsFirstLoad(false);
|
|
}, 2000);
|
|
};
|
|
|
|
// Cleanup interval on component unmount
|
|
useEffect(() => {
|
|
return () => {
|
|
if (intervalRef.current) {
|
|
clearInterval(intervalRef.current);
|
|
}
|
|
};
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
getBanzone();
|
|
}, [getBanzone]);
|
|
|
|
// 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,
|
|
};
|
|
};
|
|
|
|
return (
|
|
<SafeAreaProvider style={styles.container}>
|
|
{banzones.length > 0 && (
|
|
<Text className="hidden">Banzones loaded: {banzones.length}</Text>
|
|
)}
|
|
<MapView
|
|
onMapReady={handleMapReady}
|
|
onPoiClick={(point) => {
|
|
console.log("Poi clicked: ", point.nativeEvent);
|
|
}}
|
|
onRegionChangeComplete={handleRegionChangeComplete}
|
|
style={styles.map}
|
|
// initialRegion={getMapRegion()}
|
|
region={getMapRegion()}
|
|
userInterfaceStyle={theme === LIGHT_THEME ? "light" : "dark"}
|
|
showsBuildings={false}
|
|
showsIndoors={false}
|
|
loadingEnabled={true}
|
|
mapType="standard"
|
|
>
|
|
{trackPoints &&
|
|
trackPoints.length > 0 &&
|
|
trackPoints.map((point, index) => {
|
|
// console.log(`Rendering circle ${index}:`, point);
|
|
return (
|
|
<Circle
|
|
key={index}
|
|
center={{
|
|
latitude: point.lat,
|
|
longitude: point.lon,
|
|
}}
|
|
zIndex={50}
|
|
radius={circleRadius}
|
|
fillColor="rgba(16, 85, 201, 0.6)"
|
|
strokeColor="rgba(16, 85, 201, 0.8)"
|
|
strokeWidth={2}
|
|
/>
|
|
);
|
|
})}
|
|
{polylineCoordinates && (
|
|
<PolylineWithLabel
|
|
coordinates={polylineCoordinates.map((coord) => ({
|
|
latitude: coord[0],
|
|
longitude: coord[1],
|
|
}))}
|
|
label="Tuyến bờ"
|
|
strokeColor="#FF5733"
|
|
strokeWidth={4}
|
|
showDistance={false}
|
|
zIndex={50}
|
|
/>
|
|
)}
|
|
{polygonCoordinates && polygonCoordinates.length > 0 && (
|
|
<>
|
|
{polygonCoordinates.map((polygon, index) => {
|
|
// Tạo key ổn định từ tọa độ đầu tiên của polygon
|
|
const polygonKey =
|
|
polygon.length > 0
|
|
? `polygon-${polygon[0][0]}-${polygon[0][1]}-${index}`
|
|
: `polygon-${index}`;
|
|
|
|
return (
|
|
<PolygonWithLabel
|
|
key={polygonKey}
|
|
coordinates={polygon.map((coords) => ({
|
|
latitude: coords[0],
|
|
longitude: coords[1],
|
|
}))}
|
|
label="Test khu đánh bắt"
|
|
content="Thời gian cấm (từ tháng 1 đến tháng 12)"
|
|
fillColor="rgba(16, 85, 201, 0.6)"
|
|
strokeColor="rgba(16, 85, 201, 0.8)"
|
|
strokeWidth={2}
|
|
zIndex={50}
|
|
zoomLevel={zoomLevel}
|
|
/>
|
|
);
|
|
})}
|
|
</>
|
|
)}
|
|
{gpsData && (
|
|
<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={100}
|
|
anchor={{ x: 0.5, y: 0.5 }}
|
|
>
|
|
<View className="w-8 h-8 items-center justify-center">
|
|
<ExpoImage
|
|
source={getShipIcon(alarmData?.level || 0, gpsData.fishing)}
|
|
style={{
|
|
width: 32,
|
|
height: 32,
|
|
transform: [
|
|
{
|
|
rotate: `${
|
|
typeof gpsData.h === "number" && !isNaN(gpsData.h)
|
|
? gpsData.h
|
|
: 0
|
|
}deg`,
|
|
},
|
|
],
|
|
}}
|
|
/>
|
|
</View>
|
|
</Marker>
|
|
)}
|
|
</MapView>
|
|
<TouchableOpacity style={styles.button} onPress={drawPolyline}>
|
|
<Text style={styles.buttonText}>Get GPS Data</Text>
|
|
</TouchableOpacity>
|
|
</SafeAreaProvider>
|
|
);
|
|
}
|
|
|
|
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",
|
|
},
|
|
titleContainer: {
|
|
flexDirection: "row",
|
|
alignItems: "center",
|
|
gap: 8,
|
|
},
|
|
stepContainer: {
|
|
gap: 8,
|
|
marginBottom: 8,
|
|
},
|
|
reactLogo: {
|
|
height: 178,
|
|
width: 290,
|
|
bottom: 0,
|
|
left: 0,
|
|
position: "absolute",
|
|
},
|
|
});
|