| New file |
| | |
| | | import Taro from "@tarojs/taro"; |
| | | import { useEffect, useRef, useState } from 'react'; |
| | | import { View, Map, Text, Image, ScrollView } from '@tarojs/components'; |
| | | import './index.less'; |
| | | |
| | | import backIcon from '../../../robotaxi/assets/back-icon.png'; |
| | | import originIcon from '../../../robotaxi/assets/origin-icon.png'; |
| | | |
| | | import { getOpenCity } from '../../api/select-city'; |
| | | import { getGeofence } from '../../api/home'; |
| | | import { extractPointsFromPolygons, getDistance } from '../../../utils/Robotaxi-public-method'; |
| | | |
| | | export default function OperatingArea() { |
| | | const [longitude, setLongitude] = useState<number | null>(null); |
| | | const [latitude, setLatitude] = useState<number | null>(null); |
| | | const [polygons, setPolygons] = useState<any[]>([]); |
| | | const [polylines, setPolylines] = useState<any[]>([]); |
| | | const [cityList, setCityList] = useState<any[]>([]); |
| | | const [selectedCityCode, setSelectedCityCode] = useState<string>(''); |
| | | const [rawGeofenceData, setRawGeofenceData] = useState<any[]>([]); |
| | | const [selectedPolygonId, setSelectedPolygonId] = useState<number | null>(null); |
| | | const [operatingTimeInfo, setOperatingTimeInfo] = useState<any>(null); |
| | | const lastPolygonTapTimeRef = useRef(0); |
| | | |
| | | useEffect(() => { |
| | | const cachedSelectedCity = Taro.getStorageSync('selectedCity'); |
| | | getOpenCity().then((res: any) => { |
| | | const openCities = res.cities || []; |
| | | setCityList(openCities); |
| | | |
| | | const cachedCityCode = cachedSelectedCity?.code; |
| | | const matchedCity = openCities.find((city: any) => (city.code || city.adcode) === cachedCityCode); |
| | | |
| | | if (matchedCity) { |
| | | handleSelectCity(matchedCity.code || matchedCity.adcode); |
| | | } else if (cachedCityCode) { |
| | | handleSelectCity(cachedCityCode); |
| | | } else if (openCities.length > 0) { |
| | | handleSelectCity(openCities[0].code || openCities[0].adcode); |
| | | } else { |
| | | getLocationInfo(); |
| | | } |
| | | }).catch(err => { |
| | | console.error('获取城市数据失败', err); |
| | | getLocationInfo(); |
| | | }); |
| | | }, []); |
| | | |
| | | const normalizeGeofenceData = (geofenceList: any[] = []) => { |
| | | return geofenceList.map((item, index) => ({ |
| | | ...item, |
| | | mapPolygonId: index + 1, |
| | | polygonPoints: buildPolygonPoints(item.points), |
| | | polylinePoints: buildPolylinePoints(item.points) |
| | | })); |
| | | }; |
| | | |
| | | const buildPolygonPoints = (points: any[] = []) => { |
| | | return points.map((point) => ({ |
| | | latitude: Number(point.lat), |
| | | longitude: Number(point.lng) |
| | | })); |
| | | }; |
| | | |
| | | const buildPolylinePoints = (points: any[] = []) => { |
| | | const polygonPoints = buildPolygonPoints(points); |
| | | if (polygonPoints.length === 0) { |
| | | return []; |
| | | } |
| | | return [...polygonPoints, polygonPoints[0]]; |
| | | }; |
| | | |
| | | const updatePolygonsStyle = (geofenceList: any[], selectedId: number | null) => { |
| | | if (!geofenceList || !Array.isArray(geofenceList)) { |
| | | return []; |
| | | } |
| | | return geofenceList.map(item => { |
| | | return { |
| | | points: item.polygonPoints || [], |
| | | strokeWidth: 0, |
| | | strokeColor: 'rgba(0, 0, 0, 0)', |
| | | fillColor: '#6d7fd933', |
| | | zIndex: 1, |
| | | id: item.mapPolygonId |
| | | } |
| | | }); |
| | | }; |
| | | |
| | | const updatePolylineStyle = (geofenceList: any[], selectedId: number | null) => { |
| | | if (!geofenceList || !Array.isArray(geofenceList)) { |
| | | return []; |
| | | } |
| | | return geofenceList.map((item, index) => { |
| | | const isSelected = item.mapPolygonId === selectedId; |
| | | return { |
| | | points: item.polylinePoints || [], |
| | | color: isSelected ? '#6D7FD9' : '#A1B1FF', |
| | | width: isSelected ? 4 : 2, |
| | | dottedLine: !isSelected, |
| | | arrowLine: false, |
| | | borderColor: isSelected ? '#6D7FD9' : '#A1B1FF', |
| | | borderWidth: 0, |
| | | zIndex: isSelected ? 3 : 2, |
| | | id: Number(item.mapPolygonId ?? index + 1) |
| | | }; |
| | | }); |
| | | }; |
| | | |
| | | const selectPolygon = (polygonId: number | null, geofenceList: any[]) => { |
| | | if (polygonId === null || polygonId === selectedPolygonId) { |
| | | return; |
| | | } |
| | | setSelectedPolygonId(polygonId); |
| | | const targetPolygon = geofenceList.find(item => item.mapPolygonId === polygonId); |
| | | if (targetPolygon) { |
| | | setOperatingTimeInfo(targetPolygon); |
| | | } |
| | | setPolygons(updatePolygonsStyle(geofenceList, polygonId)); |
| | | setPolylines(updatePolylineStyle(geofenceList, polygonId)); |
| | | }; |
| | | |
| | | const projectPoint = (point: { latitude: number; longitude: number }, referenceLatitude: number) => { |
| | | const metersPerDegreeLat = 111320; |
| | | const metersPerDegreeLng = 111320 * Math.cos(referenceLatitude * Math.PI / 180); |
| | | return { |
| | | x: point.longitude * metersPerDegreeLng, |
| | | y: point.latitude * metersPerDegreeLat |
| | | }; |
| | | }; |
| | | |
| | | const getDistanceToSegment = ( |
| | | point: { latitude: number; longitude: number }, |
| | | start: { latitude: number; longitude: number }, |
| | | end: { latitude: number; longitude: number } |
| | | ) => { |
| | | const referenceLatitude = (point.latitude + start.latitude + end.latitude) / 3; |
| | | const pointXY = projectPoint(point, referenceLatitude); |
| | | const startXY = projectPoint(start, referenceLatitude); |
| | | const endXY = projectPoint(end, referenceLatitude); |
| | | const dx = endXY.x - startXY.x; |
| | | const dy = endXY.y - startXY.y; |
| | | const lengthSquared = dx * dx + dy * dy; |
| | | |
| | | if (lengthSquared === 0) { |
| | | return Math.sqrt((pointXY.x - startXY.x) * (pointXY.x - startXY.x) + (pointXY.y - startXY.y) * (pointXY.y - startXY.y)); |
| | | } |
| | | |
| | | let ratio = ((pointXY.x - startXY.x) * dx + (pointXY.y - startXY.y) * dy) / lengthSquared; |
| | | ratio = Math.max(0, Math.min(1, ratio)); |
| | | const closestX = startXY.x + ratio * dx; |
| | | const closestY = startXY.y + ratio * dy; |
| | | |
| | | return Math.sqrt((pointXY.x - closestX) * (pointXY.x - closestX) + (pointXY.y - closestY) * (pointXY.y - closestY)); |
| | | }; |
| | | |
| | | const isPointInPolygon = ( |
| | | point: { latitude: number; longitude: number }, |
| | | polygonPoints: Array<{ latitude: number; longitude: number }> |
| | | ) => { |
| | | if (!Array.isArray(polygonPoints) || polygonPoints.length < 3) { |
| | | return false; |
| | | } |
| | | |
| | | let isInside = false; |
| | | const pointX = point.longitude; |
| | | const pointY = point.latitude; |
| | | |
| | | for (let index = 0, lastIndex = polygonPoints.length - 1; index < polygonPoints.length; lastIndex = index, index += 1) { |
| | | const currentPoint = polygonPoints[index]; |
| | | const previousPoint = polygonPoints[lastIndex]; |
| | | const currentX = currentPoint.longitude; |
| | | const currentY = currentPoint.latitude; |
| | | const previousX = previousPoint.longitude; |
| | | const previousY = previousPoint.latitude; |
| | | |
| | | const isIntersected = ((currentY > pointY) !== (previousY > pointY)) |
| | | && (pointX < ((previousX - currentX) * (pointY - currentY)) / ((previousY - currentY) || Number.EPSILON) + currentX); |
| | | |
| | | if (isIntersected) { |
| | | isInside = !isInside; |
| | | } |
| | | } |
| | | |
| | | return isInside; |
| | | }; |
| | | |
| | | const findNearestPolygonIdByTap = (latitudeValue: number, longitudeValue: number) => { |
| | | const tapPoint = { latitude: latitudeValue, longitude: longitudeValue }; |
| | | const insidePolygon = rawGeofenceData.find((item) => { |
| | | const polygonPoints = item.polygonPoints || []; |
| | | return isPointInPolygon(tapPoint, polygonPoints); |
| | | }); |
| | | |
| | | if (insidePolygon) { |
| | | return insidePolygon.mapPolygonId; |
| | | } |
| | | |
| | | let nearestPolygonId: number | null = null; |
| | | let nearestDistance = Number.POSITIVE_INFINITY; |
| | | |
| | | rawGeofenceData.forEach((item) => { |
| | | const polygonPoints = item.polygonPoints || []; |
| | | if (polygonPoints.length < 2) { |
| | | return; |
| | | } |
| | | |
| | | let minDistanceToBorder = Number.POSITIVE_INFINITY; |
| | | for (let index = 0; index < polygonPoints.length; index += 1) { |
| | | const start = polygonPoints[index]; |
| | | const end = polygonPoints[(index + 1) % polygonPoints.length]; |
| | | const distanceToSegment = getDistanceToSegment(tapPoint, start, end); |
| | | if (distanceToSegment < minDistanceToBorder) { |
| | | minDistanceToBorder = distanceToSegment; |
| | | } |
| | | } |
| | | |
| | | const distanceToFirstPoint = getDistance( |
| | | latitudeValue, |
| | | longitudeValue, |
| | | polygonPoints[0].latitude, |
| | | polygonPoints[0].longitude |
| | | ); |
| | | const matchDistance = Math.min(minDistanceToBorder, distanceToFirstPoint); |
| | | |
| | | if (matchDistance < nearestDistance) { |
| | | nearestDistance = matchDistance; |
| | | nearestPolygonId = item.mapPolygonId; |
| | | } |
| | | }); |
| | | |
| | | return nearestDistance <= 180 ? nearestPolygonId : null; |
| | | }; |
| | | |
| | | const handleSelectCity = (cityCode: string) => { |
| | | if (!cityCode) return; |
| | | setSelectedCityCode(cityCode); |
| | | getGeofence({ cityCode }).then((res: any) => { |
| | | if (res && Array.isArray(res)) { |
| | | const normalizedGeofenceData = normalizeGeofenceData(res); |
| | | setRawGeofenceData(normalizedGeofenceData); |
| | | const firstPolygonId = normalizedGeofenceData.length > 0 ? normalizedGeofenceData[0].mapPolygonId : null; |
| | | setSelectedPolygonId(firstPolygonId); |
| | | |
| | | if (firstPolygonId !== null) { |
| | | const firstPolygonData = normalizedGeofenceData.find(item => item.mapPolygonId === firstPolygonId); |
| | | setOperatingTimeInfo(firstPolygonData); |
| | | } else { |
| | | setOperatingTimeInfo(null); |
| | | } |
| | | const newPolygons = updatePolygonsStyle(normalizedGeofenceData, firstPolygonId); |
| | | setPolygons(newPolygons); |
| | | setPolylines(updatePolylineStyle(normalizedGeofenceData, firstPolygonId)); |
| | | |
| | | if (normalizedGeofenceData.length > 0 && normalizedGeofenceData[0]?.points?.length > 0) { |
| | | const firstPoint = normalizedGeofenceData[0].points[0]; |
| | | const lat = Number(firstPoint.lat); |
| | | const lng = Number(firstPoint.lng); |
| | | setLatitude(lat); |
| | | setLongitude(lng); |
| | | |
| | | const mapCtx = Taro.createMapContext('map'); |
| | | const points = extractPointsFromPolygons(newPolygons); |
| | | if (points.length > 0) { |
| | | setTimeout(() => { |
| | | mapCtx.includePoints({ |
| | | points: points, |
| | | padding: [10, 10, 10, 10] |
| | | }); |
| | | }, 500); |
| | | } |
| | | } |
| | | } else { |
| | | setRawGeofenceData([]); |
| | | setPolygons([]); |
| | | setPolylines([]); |
| | | setSelectedPolygonId(null); |
| | | setOperatingTimeInfo(null); |
| | | } |
| | | }).catch(err => { |
| | | console.error('获取电子围栏失败', err); |
| | | setRawGeofenceData([]); |
| | | setPolygons([]); |
| | | setPolylines([]); |
| | | setSelectedPolygonId(null); |
| | | setOperatingTimeInfo(null); |
| | | }); |
| | | }; |
| | | |
| | | const handlePolygonTap = (e: any) => { |
| | | let tapPolygonId = null; |
| | | if (e.detail && e.detail.polygonId) { |
| | | tapPolygonId = e.detail.polygonId; |
| | | } else if (e.markerId) { |
| | | tapPolygonId = e.markerId; |
| | | } |
| | | |
| | | if (tapPolygonId !== null && rawGeofenceData.length > 0) { |
| | | lastPolygonTapTimeRef.current = Date.now(); |
| | | selectPolygon(tapPolygonId, rawGeofenceData); |
| | | } |
| | | }; |
| | | |
| | | const handleMapTap = (e: any) => { |
| | | if (Date.now() - lastPolygonTapTimeRef.current < 200) { |
| | | return; |
| | | } |
| | | const tapLatitude = e?.detail?.latitude; |
| | | const tapLongitude = e?.detail?.longitude; |
| | | if (!Number.isFinite(tapLatitude) || !Number.isFinite(tapLongitude) || rawGeofenceData.length === 0) { |
| | | return; |
| | | } |
| | | const nearestPolygonId = findNearestPolygonIdByTap(tapLatitude, tapLongitude); |
| | | if (nearestPolygonId !== null) { |
| | | selectPolygon(nearestPolygonId, rawGeofenceData); |
| | | } |
| | | }; |
| | | |
| | | const getLocationInfo = () => { |
| | | Taro.getLocation({ type: 'gcj02' }) |
| | | .then(res => { |
| | | setLatitude(res.latitude); |
| | | setLongitude(res.longitude); |
| | | }).catch(err => { |
| | | console.error('获取位置失败', err); |
| | | }); |
| | | }; |
| | | |
| | | const toBack = () => { |
| | | Taro.navigateBack({ |
| | | delta: 1, |
| | | }); |
| | | }; |
| | | |
| | | const toOrigin = () => { |
| | | const mapCtx = Taro.createMapContext('map'); |
| | | const points = extractPointsFromPolygons(polygons); |
| | | if (points.length > 0) { |
| | | mapCtx.includePoints({ |
| | | points, |
| | | padding: [10, 10, 10, 10] |
| | | }); |
| | | } |
| | | }; |
| | | |
| | | return ( |
| | | <View className="operating-area-container"> |
| | | <Map |
| | | id="map" |
| | | className="map" |
| | | longitude={longitude || 116.397128} |
| | | latitude={latitude || 39.916527} |
| | | // scale={30} |
| | | polygons={polygons} |
| | | polyline={polylines} |
| | | showLocation={false} |
| | | style={{ width: '100%', height: '65%' }} |
| | | onPolygonTap={handlePolygonTap} |
| | | onTap={handleMapTap} |
| | | > |
| | | {/* 返回按钮 */} |
| | | <View onClick={toBack} className="back-btn"> |
| | | <Image className="back-icon" src={backIcon} /> |
| | | </View> |
| | | </Map> |
| | | |
| | | {/* 底部面板 */} |
| | | <View className="bottom-sheet"> |
| | | <Image className="origin-icon" src={originIcon} onClick={toOrigin} /> |
| | | <View className="header"> |
| | | <Text className="title">查看运营区域</Text> |
| | | {/* <Image onClick={toBack} className="close-icon" src={closeIcon} /> */} |
| | | </View> |
| | | |
| | | <ScrollView scrollX className="city-scroll"> |
| | | <View className="city-list"> |
| | | {cityList.map((city, index) => { |
| | | const code = city.code || city.adcode; |
| | | return ( |
| | | <View |
| | | key={index} |
| | | className={`city-item ${selectedCityCode === code ? 'active' : ''}`} |
| | | onClick={() => handleSelectCity(code)} |
| | | > |
| | | {city.name} |
| | | </View> |
| | | ); |
| | | })} |
| | | {/* Fallback mock data if cityList is empty */} |
| | | {cityList.length === 0 && ( |
| | | <> |
| | | <View className="city-item active">北京市</View> |
| | | <View className="city-item">广州市</View> |
| | | <View className="city-item">佛山市</View> |
| | | </> |
| | | )} |
| | | </View> |
| | | </ScrollView> |
| | | |
| | | <View className="content-box"> |
| | | <View className="content-title">区域运营时间:</View> |
| | | <View className="content-time"> |
| | | 工作日: {operatingTimeInfo?.workDayOpenTime || '07:00'}-{operatingTimeInfo?.workDayCloseTime || '22:00'} 周末: {operatingTimeInfo?.weekendOpenTime || '07:00'}-{operatingTimeInfo?.weekendCloseTime || '22:00'} |
| | | </View> |
| | | <View className="content-desc"> |
| | | <View className="desc-item"> |
| | | <Text className="index">1.</Text> |
| | | <Text className="text">在运营时段可正常使用打车服务,非运营时段服务暂停</Text> |
| | | </View> |
| | | <View className="desc-item"> |
| | | <Text className="index">2.</Text> |
| | | <Text className="text">各站点运营时间可能存在调整,实际运营情况请以页面实时展示为准</Text> |
| | | </View> |
| | | <View className="desc-item"> |
| | | <Text className="index">3.</Text> |
| | | <Text className="text">如有问题,请拨打客服热线xxxxxx</Text> |
| | | </View> |
| | | </View> |
| | | </View> |
| | | </View> |
| | | </View> |
| | | ); |
| | | } |