feat(earth): add cable-landing point relation via city_id
Backend: - Fix arcgis_landing collector to extract city_id - Fix arcgis_relation collector to extract city_id - Fix convert_landing_point_to_geojson to use city_id mapping Frontend: - Update landing point cableNames to use array - Add applyLandingPointVisualState for cable lock highlight - Dim all landing points when satellite is locked
This commit is contained in:
@@ -96,37 +96,48 @@ def convert_cable_to_geojson(records: List[CollectedData]) -> Dict[str, Any]:
|
|||||||
return {"type": "FeatureCollection", "features": features}
|
return {"type": "FeatureCollection", "features": features}
|
||||||
|
|
||||||
|
|
||||||
def convert_landing_point_to_geojson(records: List[CollectedData]) -> Dict[str, Any]:
|
def convert_landing_point_to_geojson(records: List[CollectedData], city_to_cable_ids_map: Dict[int, List[int]] = None, cable_id_to_name_map: Dict[int, str] = None) -> Dict[str, Any]:
|
||||||
"""Convert landing point records to GeoJSON FeatureCollection"""
|
|
||||||
features = []
|
features = []
|
||||||
|
|
||||||
for record in records:
|
for record in records:
|
||||||
try:
|
try:
|
||||||
lat = float(record.latitude) if record.latitude else None
|
lat = float(record.latitude) if record.latitude else None
|
||||||
lon = float(record.longitude) if record.longitude else None
|
lon = float(record.longitude) if record.longitude else None
|
||||||
except (ValueError, TypeError):
|
except (ValueError, TypeError):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if lat is None or lon is None:
|
if lat is None or lon is None:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
metadata = record.extra_data or {}
|
metadata = record.extra_data or {}
|
||||||
|
city_id = metadata.get("city_id")
|
||||||
|
|
||||||
|
props = {
|
||||||
|
"id": record.id,
|
||||||
|
"source_id": record.source_id,
|
||||||
|
"name": record.name,
|
||||||
|
"country": record.country,
|
||||||
|
"city": record.city,
|
||||||
|
"is_tbd": metadata.get("is_tbd", False),
|
||||||
|
}
|
||||||
|
|
||||||
|
cable_names = []
|
||||||
|
if city_to_cable_ids_map and city_id in city_to_cable_ids_map:
|
||||||
|
for cable_id in city_to_cable_ids_map[city_id]:
|
||||||
|
if cable_id_to_name_map and cable_id in cable_id_to_name_map:
|
||||||
|
cable_names.append(cable_id_to_name_map[cable_id])
|
||||||
|
|
||||||
|
if cable_names:
|
||||||
|
props["cable_names"] = cable_names
|
||||||
|
|
||||||
features.append(
|
features.append(
|
||||||
{
|
{
|
||||||
"type": "Feature",
|
"type": "Feature",
|
||||||
"geometry": {"type": "Point", "coordinates": [lon, lat]},
|
"geometry": {"type": "Point", "coordinates": [lon, lat]},
|
||||||
"properties": {
|
"properties": props,
|
||||||
"id": record.id,
|
|
||||||
"source_id": record.source_id,
|
|
||||||
"name": record.name,
|
|
||||||
"country": record.country,
|
|
||||||
"city": record.city,
|
|
||||||
"is_tbd": metadata.get("is_tbd", False),
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
return {"type": "FeatureCollection", "features": features}
|
return {"type": "FeatureCollection", "features": features}
|
||||||
|
|
||||||
|
|
||||||
@@ -264,19 +275,45 @@ async def get_cables_geojson(db: AsyncSession = Depends(get_db)):
|
|||||||
|
|
||||||
@router.get("/geo/landing-points")
|
@router.get("/geo/landing-points")
|
||||||
async def get_landing_points_geojson(db: AsyncSession = Depends(get_db)):
|
async def get_landing_points_geojson(db: AsyncSession = Depends(get_db)):
|
||||||
"""获取登陆点 GeoJSON 数据 (Point)"""
|
|
||||||
try:
|
try:
|
||||||
stmt = select(CollectedData).where(CollectedData.source == "arcgis_landing_points")
|
landing_stmt = select(CollectedData).where(CollectedData.source == "arcgis_landing_points")
|
||||||
result = await db.execute(stmt)
|
landing_result = await db.execute(landing_stmt)
|
||||||
records = result.scalars().all()
|
records = landing_result.scalars().all()
|
||||||
|
|
||||||
|
relation_stmt = select(CollectedData).where(CollectedData.source == "arcgis_cable_landing_relation")
|
||||||
|
relation_result = await db.execute(relation_stmt)
|
||||||
|
relation_records = relation_result.scalars().all()
|
||||||
|
|
||||||
|
cable_stmt = select(CollectedData).where(CollectedData.source == "arcgis_cables")
|
||||||
|
cable_result = await db.execute(cable_stmt)
|
||||||
|
cable_records = cable_result.scalars().all()
|
||||||
|
|
||||||
|
city_to_cable_ids_map = {}
|
||||||
|
for rel in relation_records:
|
||||||
|
if rel.extra_data:
|
||||||
|
city_id = rel.extra_data.get("city_id")
|
||||||
|
cable_id = rel.extra_data.get("cable_id")
|
||||||
|
if city_id is not None and cable_id is not None:
|
||||||
|
if city_id not in city_to_cable_ids_map:
|
||||||
|
city_to_cable_ids_map[city_id] = []
|
||||||
|
if cable_id not in city_to_cable_ids_map[city_id]:
|
||||||
|
city_to_cable_ids_map[city_id].append(cable_id)
|
||||||
|
|
||||||
|
cable_id_to_name_map = {}
|
||||||
|
for cable in cable_records:
|
||||||
|
if cable.extra_data:
|
||||||
|
cable_id = cable.extra_data.get("cable_id")
|
||||||
|
cable_name = cable.name
|
||||||
|
if cable_id and cable_name:
|
||||||
|
cable_id_to_name_map[cable_id] = cable_name
|
||||||
|
|
||||||
if not records:
|
if not records:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=404,
|
status_code=404,
|
||||||
detail="No landing point data found. Please run the arcgis_landing_points collector first.",
|
detail="No landing point data found. Please run the arcgis_landing_points collector first.",
|
||||||
)
|
)
|
||||||
|
|
||||||
return convert_landing_point_to_geojson(records)
|
return convert_landing_point_to_geojson(records, city_to_cable_ids_map, cable_id_to_name_map)
|
||||||
except HTTPException:
|
except HTTPException:
|
||||||
raise
|
raise
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -285,7 +322,6 @@ async def get_landing_points_geojson(db: AsyncSession = Depends(get_db)):
|
|||||||
|
|
||||||
@router.get("/geo/all")
|
@router.get("/geo/all")
|
||||||
async def get_all_geojson(db: AsyncSession = Depends(get_db)):
|
async def get_all_geojson(db: AsyncSession = Depends(get_db)):
|
||||||
"""获取所有可视化数据 (电缆 + 登陆点)"""
|
|
||||||
cables_stmt = select(CollectedData).where(CollectedData.source == "arcgis_cables")
|
cables_stmt = select(CollectedData).where(CollectedData.source == "arcgis_cables")
|
||||||
cables_result = await db.execute(cables_stmt)
|
cables_result = await db.execute(cables_stmt)
|
||||||
cables_records = cables_result.scalars().all()
|
cables_records = cables_result.scalars().all()
|
||||||
@@ -293,6 +329,29 @@ async def get_all_geojson(db: AsyncSession = Depends(get_db)):
|
|||||||
points_stmt = select(CollectedData).where(CollectedData.source == "arcgis_landing_points")
|
points_stmt = select(CollectedData).where(CollectedData.source == "arcgis_landing_points")
|
||||||
points_result = await db.execute(points_stmt)
|
points_result = await db.execute(points_stmt)
|
||||||
points_records = points_result.scalars().all()
|
points_records = points_result.scalars().all()
|
||||||
|
|
||||||
|
relation_stmt = select(CollectedData).where(CollectedData.source == "arcgis_cable_landing_relation")
|
||||||
|
relation_result = await db.execute(relation_stmt)
|
||||||
|
relation_records = relation_result.scalars().all()
|
||||||
|
|
||||||
|
city_to_cable_ids_map = {}
|
||||||
|
for rel in relation_records:
|
||||||
|
if rel.extra_data:
|
||||||
|
city_id = rel.extra_data.get("city_id")
|
||||||
|
cable_id = rel.extra_data.get("cable_id")
|
||||||
|
if city_id is not None and cable_id is not None:
|
||||||
|
if city_id not in city_to_cable_ids_map:
|
||||||
|
city_to_cable_ids_map[city_id] = []
|
||||||
|
if cable_id not in city_to_cable_ids_map[city_id]:
|
||||||
|
city_to_cable_ids_map[city_id].append(cable_id)
|
||||||
|
|
||||||
|
cable_id_to_name_map = {}
|
||||||
|
for cable in cables_records:
|
||||||
|
if cable.extra_data:
|
||||||
|
cable_id = cable.extra_data.get("cable_id")
|
||||||
|
cable_name = cable.name
|
||||||
|
if cable_id and cable_name:
|
||||||
|
cable_id_to_name_map[cable_id] = cable_name
|
||||||
|
|
||||||
cables = (
|
cables = (
|
||||||
convert_cable_to_geojson(cables_records)
|
convert_cable_to_geojson(cables_records)
|
||||||
@@ -300,7 +359,7 @@ async def get_all_geojson(db: AsyncSession = Depends(get_db)):
|
|||||||
else {"type": "FeatureCollection", "features": []}
|
else {"type": "FeatureCollection", "features": []}
|
||||||
)
|
)
|
||||||
points = (
|
points = (
|
||||||
convert_landing_point_to_geojson(points_records)
|
convert_landing_point_to_geojson(points_records, city_to_cable_ids_map, cable_id_to_name_map)
|
||||||
if points_records
|
if points_records
|
||||||
else {"type": "FeatureCollection", "features": []}
|
else {"type": "FeatureCollection", "features": []}
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -59,6 +59,7 @@ class ArcGISLandingPointCollector(BaseCollector):
|
|||||||
"unit": "",
|
"unit": "",
|
||||||
"metadata": {
|
"metadata": {
|
||||||
"objectid": props.get("OBJECTID"),
|
"objectid": props.get("OBJECTID"),
|
||||||
|
"city_id": props.get("city_id"),
|
||||||
"cable_id": props.get("cable_id"),
|
"cable_id": props.get("cable_id"),
|
||||||
"cable_name": props.get("cable_name"),
|
"cable_name": props.get("cable_name"),
|
||||||
"facility": props.get("facility"),
|
"facility": props.get("facility"),
|
||||||
|
|||||||
@@ -50,6 +50,7 @@ class ArcGISCableLandingRelationCollector(BaseCollector):
|
|||||||
"unit": "",
|
"unit": "",
|
||||||
"metadata": {
|
"metadata": {
|
||||||
"objectid": props.get("OBJECTID"),
|
"objectid": props.get("OBJECTID"),
|
||||||
|
"city_id": props.get("city_id"),
|
||||||
"cable_id": props.get("cable_id"),
|
"cable_id": props.get("cable_id"),
|
||||||
"cable_name": props.get("cable_name"),
|
"cable_name": props.get("cable_name"),
|
||||||
"landing_point_id": props.get("landing_point_id"),
|
"landing_point_id": props.get("landing_point_id"),
|
||||||
|
|||||||
@@ -286,7 +286,7 @@ export async function loadLandingPoints(scene, earthObj) {
|
|||||||
sphere.userData = {
|
sphere.userData = {
|
||||||
type: 'landingPoint',
|
type: 'landingPoint',
|
||||||
name: properties.name || '未知登陆站',
|
name: properties.name || '未知登陆站',
|
||||||
cableName: properties.cable_system || '未知系统',
|
cableNames: properties.cable_names || [],
|
||||||
country: properties.country || '未知国家',
|
country: properties.country || '未知国家',
|
||||||
status: properties.status || 'Unknown'
|
status: properties.status || 'Unknown'
|
||||||
};
|
};
|
||||||
@@ -362,3 +362,47 @@ export function getCableStateInfo() {
|
|||||||
});
|
});
|
||||||
return states;
|
return states;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getLandingPointsByCableName(cableName) {
|
||||||
|
return landingPoints.filter(lp => lp.userData.cableNames?.includes(cableName));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getAllLandingPoints() {
|
||||||
|
return landingPoints;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function applyLandingPointVisualState(lockedCableName, dimAll = false) {
|
||||||
|
const pulse = (Math.sin(Date.now() * 0.003) + 1) * 0.5;
|
||||||
|
const brightness = 0.3;
|
||||||
|
|
||||||
|
landingPoints.forEach(lp => {
|
||||||
|
const isRelated = !dimAll && lp.userData.cableNames?.includes(lockedCableName);
|
||||||
|
|
||||||
|
if (isRelated) {
|
||||||
|
lp.material.color.setHex(0xffaa00);
|
||||||
|
lp.material.emissive.setHex(0x442200);
|
||||||
|
lp.material.emissiveIntensity = 0.5 + pulse * 0.5;
|
||||||
|
lp.material.opacity = 0.8 + pulse * 0.2;
|
||||||
|
lp.scale.setScalar(1.2 + pulse * 0.3);
|
||||||
|
} else {
|
||||||
|
const r = 255 * brightness;
|
||||||
|
const g = 170 * brightness;
|
||||||
|
const b = 0 * brightness;
|
||||||
|
lp.material.color.setRGB(r / 255, g / 255, b / 255);
|
||||||
|
lp.material.emissive.setHex(0x000000);
|
||||||
|
lp.material.emissiveIntensity = 0;
|
||||||
|
lp.material.opacity = 0.3;
|
||||||
|
lp.scale.setScalar(1.0);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resetLandingPointVisualState() {
|
||||||
|
landingPoints.forEach(lp => {
|
||||||
|
lp.material.color.setHex(0xffaa00);
|
||||||
|
lp.material.emissive.setHex(0x442200);
|
||||||
|
lp.material.emissiveIntensity = 0.5;
|
||||||
|
lp.material.opacity = 1.0;
|
||||||
|
lp.scale.setScalar(1.0);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import {
|
|||||||
hideTooltip
|
hideTooltip
|
||||||
} from './ui.js';
|
} from './ui.js';
|
||||||
import { createEarth, createClouds, createTerrain, createStars, createGridLines, toggleTerrain, getEarth } from './earth.js';
|
import { createEarth, createClouds, createTerrain, createStars, createGridLines, toggleTerrain, getEarth } from './earth.js';
|
||||||
import { loadGeoJSONFromPath, loadLandingPoints, handleCableClick, clearCableSelection, getCableLines, getCablesById, lockedCable as cableLocked, getCableState, setCableState, clearAllCableStates } from './cables.js';
|
import { loadGeoJSONFromPath, loadLandingPoints, handleCableClick, clearCableSelection, getCableLines, getCablesById, lockedCable as cableLocked, getCableState, setCableState, clearAllCableStates, applyLandingPointVisualState, resetLandingPointVisualState, getAllLandingPoints } from './cables.js';
|
||||||
import { createSatellites, loadSatellites, updateSatellitePositions, toggleSatellites, toggleTrails, getShowSatellites, selectSatellite, getSatelliteData, getSatellitePoints, setSatelliteRingState, updateLockedRingPosition, updateHoverRingPosition, getSatellitePositions } from './satellites.js';
|
import { createSatellites, loadSatellites, updateSatellitePositions, toggleSatellites, toggleTrails, getShowSatellites, selectSatellite, getSatelliteData, getSatellitePoints, setSatelliteRingState, updateLockedRingPosition, updateHoverRingPosition, getSatellitePositions } from './satellites.js';
|
||||||
import { setupControls, getAutoRotate, getShowTerrain, zoomLevel, setAutoRotate, toggleAutoRotate, resetView } from './controls.js';
|
import { setupControls, getAutoRotate, getShowTerrain, zoomLevel, setAutoRotate, toggleAutoRotate, resetView } from './controls.js';
|
||||||
import { initInfoCard, showInfoCard, hideInfoCard, getCurrentType, setInfoCardNoBorder } from './info-card.js';
|
import { initInfoCard, showInfoCard, hideInfoCard, getCurrentType, setInfoCardNoBorder } from './info-card.js';
|
||||||
@@ -475,6 +475,14 @@ function animate() {
|
|||||||
|
|
||||||
applyCableVisualState();
|
applyCableVisualState();
|
||||||
|
|
||||||
|
if (lockedObjectType === 'cable' && lockedObject) {
|
||||||
|
applyLandingPointVisualState(lockedObject.userData.name, false);
|
||||||
|
} else if (lockedObjectType === 'satellite' && lockedSatellite) {
|
||||||
|
applyLandingPointVisualState(null, true);
|
||||||
|
} else {
|
||||||
|
resetLandingPointVisualState();
|
||||||
|
}
|
||||||
|
|
||||||
updateSatellitePositions(16);
|
updateSatellitePositions(16);
|
||||||
|
|
||||||
const satPositions = getSatellitePositions();
|
const satPositions = getSatellitePositions();
|
||||||
|
|||||||
Reference in New Issue
Block a user