import React, { useRef, useState, useEffect, useCallback, useMemo, FC } from "react"; import { Platform, View, ScrollView, LayoutAnimation, Text, InteractionManager, useWindowDimensions, Image, StatusBar } from "react-native"; import Constants from "expo-constants"; import { useSafeAreaInsets } from "react-native-safe-area-context"; import { configureReanimatedLogger, ReanimatedLogLevel, } from "react-native-reanimated"; import { useThemeColors } from "@/lib/theme"; import { useResponsive } from "@/lib/responsive"; import { LED_vision } from "@/components/発車時刻表/LED_vidion"; import { TitleBar } from "@/components/Menu/TitleBar"; import { FixedContentBottom } from "@/components/Menu/FixedContentBottom"; import { lineList, stationIDPair } from "@/lib/getStationList"; import { useFavoriteStation } from "@/stateBox/useFavoriteStation"; import { useNavigation, useIsFocused } from "@react-navigation/native"; import { useStationList } from "@/stateBox/useStationList"; import { TopMenuButton } from "@/components/Menu/TopMenuButton"; import { JRSTraInfoBox } from "@/components/Menu/JRSTraInfoBox"; import MapView, { Marker } from "react-native-maps"; import { CarouselBox } from "@/components/Menu/Carousel/CarouselBox"; import { CarouselTypeChanger } from "@/components/Menu/Carousel/CarouselTypeChanger"; import { useUserPosition } from "@/stateBox/useUserPosition"; import { AS } from "@/storageControl"; import { STORAGE_KEYS } from "@/constants"; import { lineList_LineWebID } from "@/lib/getStationList"; import { StationProps } from "@/lib/CommonTypes"; import { LocationObject } from "expo-location"; import { StationSource } from "@/types"; configureReanimatedLogger({ level: ReanimatedLogLevel.error, // Set the log level to error strict: true, // Reanimated runs in strict mode by default }); type props = { scrollRef: React.RefObject; mapHeight: number; MapFullHeight: number; mapMode: boolean; setMapMode: React.Dispatch>; }; const MAP_PIN_SIZE = Platform.OS === "android" ? 32 : 36; export const Menu: FC = (props) => { const { scrollRef, mapHeight, MapFullHeight, mapMode, setMapMode } = props; const { navigate } = useNavigation(); const { colors, isDark } = useThemeColors(); const isMenuFocused = useIsFocused(); const { verticalScale } = useResponsive(); const insets = useSafeAreaInsets(); const { favoriteStation } = useFavoriteStation(); const { originalStationList, getStationDataFromNameBase } = useStationList(); const [stationSource, _setStationSource] = useState({ type: "position" }); // 時刻表βの遅延レンダリング(画面遷移アニメーション完了後に描画) const [ledReady, setLedReady] = useState(false); useEffect(() => { const handle = InteractionManager.runAfterInteractions(() => setLedReady(true)); return () => handle.cancel(); }, []); // 検索モードを閉じたときに戻るソースを記憶 const prevNonSearchTypeRef = useRef<"position" | "favorite">("position"); // stationSource を更新するラッパー(position/favorite に切り替えた際に戻り先を記録) const setStationSource = useCallback((source: StationSource) => { if (source.type !== "search") prevNonSearchTypeRef.current = source.type; _setStationSource(source); }, []); // 検索モードを閉じて直前の position/favorite に戻る const closeSearch = useCallback(() => { _setStationSource({ type: prevNonSearchTypeRef.current }); }, []); useEffect(() => { AS.getItem(STORAGE_KEYS.STATION_LIST_MODE) .then((res) => { if (res === "position" || res === "favorite") { setStationSource({ type: res }); } }) .catch(() => {}); }, []); const mapsRef = useRef(null); const returnToTop = (bool = true) => { scrollRef?.current.scrollTo({ y: mapHeight > verticalScale(80) ? mapHeight - verticalScale(80) : 0, animated: bool, }); }; const goToMap = () => { scrollRef?.current.scrollTo({ y: 0, animated: true, }); }; useEffect(() => { setTimeout(() => { returnToTop(false); }, 10); }, [mapHeight]); const [scrollStartPosition, setScrollStartPosition] = useState(0); const onScrollBeginDrag = (e) => { LayoutAnimation.configureNext({ duration: 300, create: { type: LayoutAnimation.Types.easeInEaseOut, property: LayoutAnimation.Properties.opacity, }, update: { type: LayoutAnimation.Types.easeInEaseOut, property: LayoutAnimation.Properties.opacity, }, }); setScrollStartPosition(e.nativeEvent.contentOffset.y); setMapMode(false); }; //現在地基準の駅名標リストアップ機能 const { position, locationStatus } = useUserPosition(); const [nearPositionStation, setNearPositionStation] = useState< StationProps[][] >([]); //第三要素 useEffect(() => { if (!position) return () => {}; makeCurrentStation(position); }, [position, stationSource.type]); const makeCurrentStation = (location: LocationObject) => { if (!originalStationList) return () => {}; const findStationEachLine = (selectLine) => { const searchArea = 0.055; //検索範囲 const _calcDistance = (from, to) => { let lat = Math.abs(from.lat - to.lat); let lng = Math.abs(from.lng - to.lng); return Math.sqrt(lat * lat + lng * lng); }; let NearStation = selectLine.filter( (d) => _calcDistance(d, { lat: location.coords.latitude, lng: location.coords.longitude, }) < searchArea ); //NearStationを距離の近い順にソート NearStation.sort((a, b) => { return ( _calcDistance(a, { lat: location.coords.latitude, lng: location.coords.longitude, }) - _calcDistance(b, { lat: location.coords.latitude, lng: location.coords.longitude, }) ); }); return NearStation; }; let _stList: StationProps[] = lineList .map((d) => findStationEachLine(originalStationList[d])) .filter((d) => d.length > 0) .reduce((pre, current) => { pre.push(...current); return pre; }, []); // 観光スポットも位置情報検索の対象に含める const nearSpots = findStationEachLine(originalStationList["観光スポット"] ?? []); nearSpots.forEach((d) => _stList.push(d)); if (_stList.length == 0) setNearPositionStation([]); else { let returnData: StationProps[][] = []; _stList.forEach((d, index, array) => { const stationName = d.Station_JP; if (returnData.findIndex((d) => d[0].Station_JP == stationName) != -1) return; returnData.push(array.filter((d2) => d2.Station_JP == stationName)); }); //returnDataを距離の近い順にソート returnData.sort((a, b) => { const _calcDistance = (from, to) => { let lat = Math.abs(from.lat - to.lat); let lng = Math.abs(from.lng - to.lng); return Math.sqrt(lat * lat + lng * lng); }; return ( _calcDistance(a[0], { lat: location.coords.latitude, lng: location.coords.longitude, }) - _calcDistance(b[0], { lat: location.coords.latitude, lng: location.coords.longitude, }) ); }); setNearPositionStation(returnData); } }; const [listIndex, setListIndex] = useState(0); // listUpStation を useMemo で派生(useLayoutEffect + setState を排除) // stationSource が変わるたびに再計算。input も query として統合済みなので // テキスト入力のたびに正しくリストが更新される(旧来のバグを修正)。 const listUpStation = useMemo(() => { if (stationSource.type === "search") { const { query, lineId } = stationSource; const returnData: StationProps[][] = []; // query も lineId も未入力の初期状態では空リストを返す if (!query && !lineId) return returnData; if (!query) { // lineId のみ指定:その路線の全駅 Object.keys(lineList_LineWebID).forEach((d) => { originalStationList[d]?.forEach((D) => { if (lineId !== stationIDPair[lineList_LineWebID[d]]) return; if (!D.StationNumber) return; returnData.push([D]); }); }); } else { const found = getStationDataFromNameBase(query); found.forEach((d, _, array) => { const name = d.Station_JP; if (returnData.findIndex((r) => r[0].Station_JP === name) !== -1) return; returnData.push(array.filter((d2) => d2.Station_JP === name)); }); } return returnData; } else if (stationSource.type === "position") { return nearPositionStation.filter((d) => d != undefined); } else { return favoriteStation.filter((d) => d != undefined); } }, [stationSource, nearPositionStation, favoriteStation, originalStationList, getStationDataFromNameBase]); // ソース種別が切り替わったら listIndex を即座にリセット(ループ防止) useEffect(() => { setListIndex(0); }, [stationSource.type]); // listUpStation が縮小した場合、有効な範囲に1ステップでクランプ(漸減ループを防ぐ) useEffect(() => { if (listIndex < 0) return; // ソートモード中(未選択状態)は無視 if (listUpStation.length === 0) { setListIndex(0); return; } if (listIndex >= listUpStation.length) { setMapMode(false); setListIndex(listUpStation.length - 1); } // listIndex を依存に入れると再発火ループになるため除外 // eslint-disable-next-line react-hooks/exhaustive-deps }, [listUpStation]); useEffect(() => { if (originalStationList == undefined) return; if (listUpStation.length == 0) return; if (listUpStation[listIndex] == undefined) return; const { lat, lng } = listUpStation[listIndex][0]; const mapRegion = { latitude: lat, longitude: lng, latitudeDelta: 0.05, longitudeDelta: 0.05, }; if (mapMode) { mapsRef?.current.fitToCoordinates( listUpStation.map((d) => ({ latitude: d[0].lat, longitude: d[0].lng, })), { edgePadding: { top: verticalScale(80), bottom: verticalScale(120), left: 50, right: 50 } } // Add margin values here ); } else { mapsRef.current.animateToRegion(mapRegion, 1000); } }, [listIndex, listUpStation]); return ( {isMenuFocused && ( )} {!mapMode ? : <>} { if (e.nativeEvent.contentOffset.y < mapHeight - verticalScale(80)) { if (scrollStartPosition > e.nativeEvent.contentOffset.y) { goToMap(); } else { returnToTop(); } } }} > { LayoutAnimation.configureNext({ duration: 300, create: { type: LayoutAnimation.Types.easeInEaseOut, property: LayoutAnimation.Properties.opacity, }, update: { type: LayoutAnimation.Types.easeInEaseOut, property: LayoutAnimation.Properties.opacity, }, }); setMapMode(true); goToMap(); }} > {listUpStation.map(([{ lat, lng, StationNumber }], index) => ( { setMapMode(false); setListIndex(index); if (mapsRef.current) { mapsRef.current.animateToRegion( { latitude: lat, longitude: lng, latitudeDelta: 0.05, longitudeDelta: 0.05, }, 1000 ); } LayoutAnimation.configureNext({ duration: 300, create: { type: LayoutAnimation.Types.easeInEaseOut, property: LayoutAnimation.Properties.opacity, }, update: { type: LayoutAnimation.Types.easeInEaseOut, property: LayoutAnimation.Properties.opacity, }, }); returnToTop(); }} > ))} {!mapMode && ( )} {originalStationList.length != 0 && ( <> {listUpStation[listIndex] && ledReady && ( )} )} {mapMode && ( )} ); };