import DraggablePanel from "@/components/DraggablePanel"; import IconButton from "@/components/IconButton"; import AlarmList from "@/components/map/AlarmList"; import ShipInfo from "@/components/map/ShipInfo"; import { TagState, TagStateCallbackPayload } from "@/components/map/TagState"; import ZoneInMap from "@/components/map/ZoneInMap"; import ShipSearchForm, { SearchShipResponse, } from "@/components/ShipSearchForm"; import { ThemedText } from "@/components/themed-text"; import { EVENT_SEARCH_THINGS, IOS_PLATFORM, LIGHT_THEME } from "@/constants"; import { queryBanzoneById } from "@/controller/MapController"; import { usePlatform } from "@/hooks/use-platform"; import { useThemeContext } from "@/hooks/use-theme-context"; import { searchThingEventBus } from "@/services/device_events"; import { getShipIcon } from "@/services/map_service"; import eventBus from "@/utils/eventBus"; import { AntDesign, Ionicons } from "@expo/vector-icons"; import { useEffect, useRef, useState } from "react"; import { Animated, Dimensions, Image, ScrollView, StyleSheet, Text, TouchableOpacity, View, } from "react-native"; import { GestureHandlerRootView } from "react-native-gesture-handler"; import MapView, { Marker } from "react-native-maps"; interface ZoneDataParsed { zone_type?: number; zone_name?: string; zone_id?: string; message?: string; alarm_type?: number; lat?: number; lon?: number; s?: number; h?: number; fishing?: boolean; gps_time?: number; } export interface AlarmData { thing_id: string; ship_name?: string; zone: ZoneDataParsed; type: "approaching" | "entered" | "fishing"; } export interface BanzoneWithAlarm { alarms: AlarmData; zone?: Model.Zone; } export default function HomeScreen() { const mapRef = useRef(null); const [alarms, setAlarms] = useState([]); const [banzoneWithAlarm, setBanzoneWithAlarm] = useState(null); const [allBanZones, setAllBanZones] = useState([]); const [showAllAlarmsOnMap, setShowAllAlarmsOnMap] = useState(false); const [isFirstLoad, setIsFirstLoad] = useState(true); const [shipSearchFormOpen, setShipSearchFormOpen] = useState(false); const [isPanelExpanded, setIsPanelExpanded] = useState(false); const [things, setThings] = useState(null); const platform = usePlatform(); const theme = useThemeContext().colorScheme; const scale = useRef(new Animated.Value(0)).current; const opacity = useRef(new Animated.Value(1)).current; const [shipSearchFormData, setShipSearchFormData] = useState< SearchShipResponse | undefined >(undefined); const [tagStatePayload, setTagStatePayload] = useState(null); const [isShowAlarmList, setIsShowAlarmList] = useState(false); // Control mount so we can animate close before unmounting const [isAlarmListMounted, setIsAlarmListMounted] = useState(false); // Thêm state để quản lý tracksViewChanges const [tracksViewChanges, setTracksViewChanges] = useState(true); // Alarm list animation const screenHeight = Dimensions.get("window").height; const alarmListHeight = Math.round(screenHeight * 0.3); const alarmTranslateY = useRef(new Animated.Value(alarmListHeight)).current; const alarmOpacity = useRef(new Animated.Value(0)).current; const uiAnim = useRef(new Animated.Value(1)).current; // 1 visible, 0 hidden useEffect(() => { if (tagStatePayload) { searchThings(); } }, [tagStatePayload]); const openAlarmList = () => { setIsAlarmListMounted(true); setIsShowAlarmList(true); }; const closeAlarmList = () => { // Trigger the animation; the effect will unmount when complete setIsShowAlarmList(false); setBanzoneWithAlarm(null); }; useEffect(() => { if (shipSearchFormData) { searchThings(); } }, [shipSearchFormData]); const bodySearchThings: Model.SearchThingBody = { offset: 0, limit: 50, order: "name", dir: "asc", metadata: { not_empty: "ship_id", }, }; useEffect(() => { searchThingEventBus(bodySearchThings); const querySearchThingsData = (thingsData: Model.ThingsResponse) => { const sortedThings: Model.Thing[] = (thingsData.things ?? []).sort( (a, b) => { const stateLevelA = a.metadata?.state_level || 0; const stateLevelB = b.metadata?.state_level || 0; return stateLevelB - stateLevelA; // Giảm dần } ); const sortedThingsResponse: Model.ThingsResponse = { ...thingsData, things: sortedThings, }; // console.log("Things Updated: ", sortedThingsResponse.things?.length); setThings(sortedThingsResponse); }; eventBus.on(EVENT_SEARCH_THINGS, querySearchThingsData); return () => { eventBus.off(EVENT_SEARCH_THINGS, querySearchThingsData); }; }, []); useEffect(() => { if (things?.things) { const alarmTypes = [ { key: "zone_approaching_alarm_list", type: "approaching" as const, }, { key: "zone_entered_alarm_list", type: "entered" as const }, { key: "zone_fishing_alarm_list", type: "fishing" as const }, ]; const newAlarms: AlarmData[] = []; for (const thing of things.things) { for (const { key, type } of alarmTypes) { if ((thing.metadata as any)?.[key] != "[]") { const zoneList: ZoneDataParsed[] = JSON.parse( (thing?.metadata as any)?.[key] || "[]" ); for (const zone of zoneList) { const alarmData: AlarmData = { thing_id: thing.id || "", ship_name: thing.metadata?.ship_name, zone: zone, type: type, }; newAlarms.push(alarmData); } } } } // Update alarms, removing old ones not in newAlarms setAlarms((prev) => { const toKeep = prev.filter((a) => newAlarms.some( (na) => na.thing_id === a.thing_id && na.zone.zone_id === a.zone.zone_id ) ); const toAdd = newAlarms.filter( (na) => !prev.some( (a) => a.thing_id === na.thing_id && a.zone.zone_id === a.zone.zone_id ) ); return [...toKeep, ...toAdd]; }); } }, [things]); useEffect(() => { const approaching = alarms.filter((a) => a.type === "approaching"); const entered = alarms.filter((a) => a.type === "entered"); const fishing = alarms.filter((a) => a.type === "fishing"); if (approaching.length > 0) { console.log("ZoneApproachingAlarm: ", approaching); } else { // console.log("No ZoneApproachingAlarm"); } if (entered.length > 0) { console.log("ZoneEnteredAlarm: ", entered); } else { // console.log("No ZoneEnteredAlarm"); } if (fishing.length > 0) { console.log("ZoneFishingAlarm: ", fishing); } else { // console.log("No ZoneFishingAlarm"); } }, [alarms]); // Load all banzones when alarms change (for showing all alarms on map) useEffect(() => { if (alarms.length > 0 && showAllAlarmsOnMap) { loadAllBanZones(); } }, [alarms, showAllAlarmsOnMap]); 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 (things?.things?.length === 0) { return { latitude: 15.70581, longitude: 116.152685, latitudeDelta: 0.05, longitudeDelta: 0.05, }; } return { latitude: things?.things?.[0]?.metadata?.gps ? JSON.parse(things.things[0].metadata.gps).lat : 15.70581, longitude: things?.things?.[0]?.metadata?.gps ? JSON.parse(things.things[0].metadata.gps).lon : 116.152685, latitudeDelta: 0.05, longitudeDelta: 0.05, }; }; const handleMapReady = () => { setTimeout(() => { setIsFirstLoad(false); }, 2000); }; useEffect(() => { for (var thing of things?.things || []) { if (thing.metadata?.state_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(); } } }, [things, scale, opacity]); // Tắt tracksViewChanges sau khi map đã load xong useEffect(() => { if (!isFirstLoad) { // Delay một chút để đảm bảo markers đã render xong const timer = setTimeout(() => { setTracksViewChanges(false); }, 3000); return () => clearTimeout(timer); } }, [isFirstLoad]); // Animate alarm panel when isShowAlarmList changes // Keep the overlay mounted while animating it in/out. useEffect(() => { if (isShowAlarmList) { // Ensure mounted then animate to visible setIsAlarmListMounted(true); Animated.parallel([ Animated.timing(alarmTranslateY, { toValue: 0, duration: 300, useNativeDriver: true, }), Animated.timing(alarmOpacity, { toValue: 1, duration: 300, useNativeDriver: true, }), ]).start(); } else { // Animate out, then unmount Animated.parallel([ Animated.timing(alarmTranslateY, { toValue: alarmListHeight, duration: 300, useNativeDriver: true, }), Animated.timing(alarmOpacity, { toValue: 0, duration: 300, useNativeDriver: true, }), ]).start(() => { setIsAlarmListMounted(false); }); } }, [alarmTranslateY, alarmListHeight, alarmOpacity, isShowAlarmList]); useEffect(() => { Animated.timing(uiAnim, { toValue: isAlarmListMounted ? 0 : 1, duration: 200, useNativeDriver: true, }).start(); }, [uiAnim, isAlarmListMounted]); const searchThings = async () => { // console.log("FormSearch Playload in Search Thing: ", shipSearchFormData); // Xây dựng query state dựa trên logic bạn cung cấp const stateNormalQuery = tagStatePayload?.isNormal ? "normal" : ""; const stateSosQuery = tagStatePayload?.isSos ? "sos" : ""; const stateWarningQuery = tagStatePayload?.isWarning ? stateNormalQuery + ",warning" : stateNormalQuery; const stateCriticalQuery = tagStatePayload?.isDangerous ? stateWarningQuery + ",critical" : stateWarningQuery; // Nếu bật tất cả filter thì không cần truyền stateQuery const stateQuery = tagStatePayload?.isNormal && tagStatePayload?.isWarning && tagStatePayload?.isDangerous && tagStatePayload?.isSos ? "" : [stateCriticalQuery, stateSosQuery].filter(Boolean).join(","); let metaFormQuery = {}; if (tagStatePayload?.isDisconected) metaFormQuery = { ...metaFormQuery, connected: false }; if (shipSearchFormData?.ship_name) { metaFormQuery = { ...metaFormQuery, ship_name: shipSearchFormData?.ship_name, }; } if (shipSearchFormData?.reg_number) { metaFormQuery = { ...metaFormQuery, reg_number: shipSearchFormData?.reg_number, }; } if ( shipSearchFormData?.ship_length[0] !== 0 || shipSearchFormData?.ship_length[1] !== 100 ) { metaFormQuery = { ...metaFormQuery, ship_length: shipSearchFormData?.ship_length, }; } if ( shipSearchFormData?.ship_power[0] !== 0 || shipSearchFormData?.ship_power[1] !== 100000 ) { metaFormQuery = { ...metaFormQuery, ship_power: shipSearchFormData?.ship_power, }; } if (shipSearchFormData?.alarm_list) { metaFormQuery = { ...metaFormQuery, alarm_list: shipSearchFormData?.alarm_list, }; } if (shipSearchFormData?.ship_type) { metaFormQuery = { ...metaFormQuery, ship_type: shipSearchFormData?.ship_type, }; } if (shipSearchFormData?.ship_group_id) { metaFormQuery = { ...metaFormQuery, ship_group_id: shipSearchFormData?.ship_group_id, }; } // Tạo metadata query const metaStateQuery = stateQuery !== "" ? { state_level: stateQuery } : null; // Tạo body search với filter const searchParams: Model.SearchThingBody = { offset: 0, limit: 50, order: "name", dir: "asc", metadata: { ...metaFormQuery, ...metaStateQuery, not_empty: "ship_id", }, }; // console.log("Search Params: ", searchParams); // Gọi API tìm kiếm searchThingEventBus(searchParams); }; const handleOnSubmitSearchForm = async (data: SearchShipResponse) => { setShipSearchFormData(data); setShipSearchFormOpen(false); }; const loadAllBanZones = async () => { try { const banzonePromises = alarms.map(async (alarm) => { try { const banzone = await queryBanzoneById(alarm.zone.zone_id || ""); return { alarms: alarm, zone: banzone.data, }; } catch (error) { console.warn(`Cannot get banzone for zone_id: ${alarm.zone.zone_id}`); return null; } }); const banzoneResults = await Promise.all(banzonePromises); const validBanZones = banzoneResults.filter( (banzone): banzone is NonNullable => banzone !== null ); setAllBanZones(validBanZones); } catch (error) { console.error("Error loading all banzones:", error); } }; const handleAlarmPress = async (alarm: AlarmData) => { console.log("Alarm pressed from list:", alarm); try { const banzone = await queryBanzoneById(alarm.zone.zone_id || ""); // console.log("Banzone API response:", banzone); // console.log("Banzone data:", banzone.data); // console.log("Zone geometry:", banzone.data?.geometry); const banzoneWithAlarm: BanzoneWithAlarm = { alarms: alarm, zone: banzone.data, }; setBanzoneWithAlarm(banzoneWithAlarm); setShowAllAlarmsOnMap(false); } catch (error) { console.error("Cannot get Banzone:", error); } }; const hasActiveFilters = shipSearchFormData ? shipSearchFormData.ship_name !== "" || shipSearchFormData.reg_number !== "" || shipSearchFormData.ship_length[0] !== 0 || shipSearchFormData.ship_length[1] !== 100 || shipSearchFormData.ship_power[0] !== 0 || shipSearchFormData.ship_power[1] !== 100000 || shipSearchFormData.ship_type !== "" || shipSearchFormData.alarm_list !== "" || shipSearchFormData.ship_group_id !== "" || shipSearchFormData.group_id !== "" : false; return ( {!banzoneWithAlarm && things?.things && things.things.length > 0 && ( <> {things.things .filter((thing) => thing.metadata?.gps) // Filter trước để tránh null check .map((thing, index) => { const gpsData: Model.GPSResponse = JSON.parse( thing.metadata!.gps! ); // Tạo unique key dựa trên thing.id hoặc tọa độ const uniqueKey = thing.id ? `marker-${thing.id}-${index}` : `marker-${gpsData.lat.toFixed(6)}-${gpsData.lon.toFixed( 6 )}-${index}`; return ( {/* */} {thing.metadata?.state_level === 3 && ( )} { const icon = getShipIcon( thing.metadata?.state_level || 0, gpsData.fishing ); // console.log("Ship icon:", icon, "for level:", alarmData?.level, "fishing:", 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`, }, ], }} /> ); })} )} {(banzoneWithAlarm || (showAllAlarmsOnMap && allBanZones.length > 0)) && ( )} {!isAlarmListMounted && !isPanelExpanded && ( } type="primary" size="large" style={{ borderRadius: 10, backgroundColor: hasActiveFilters ? "#B8D576" : "#fff", }} onPress={() => setShipSearchFormOpen(true)} > )} {/* */} {/* Draggable Panel */} {!isAlarmListMounted && ( { // console.log("Panel expanded:", expanded); setIsPanelExpanded(expanded); }} > <> { setTagStatePayload(tagState); }} /> Danh sách tàu thuyền {isPanelExpanded && ( } type="primary" shape="circle" size="middle" style={{ // borderRadius: 10, backgroundColor: hasActiveFilters ? "#B8D576" : "#fff", }} onPress={() => setShipSearchFormOpen(true)} > )} {things && things.things && things.things.length > 0 && ( <> {things.things.map((thing, index) => { return ( {index < (things.things?.length ?? 0) - 1 && ( )} ); })} )} )} {/* Alarm list overlay */} {isAlarmListMounted && ( Danh sách cảnh báo closeAlarmList()} style={{ padding: 6 }} > {/* Body */} )} setShipSearchFormOpen(false)} onSubmit={handleOnSubmitSearchForm} /> {!isAlarmListMounted && alarms.length > 0 && ( } type="danger" size="middle" onPress={() => openAlarmList()} > {alarms.length} )} ); } 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", }, });