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:
linkong
2026-03-20 15:45:02 +08:00
parent 3e3090d72a
commit 3fcbae55dc
5 changed files with 139 additions and 26 deletions

View File

@@ -96,8 +96,7 @@ def convert_cable_to_geojson(records: List[CollectedData]) -> Dict[str, Any]:
return {"type": "FeatureCollection", "features": features}
def convert_landing_point_to_geojson(records: List[CollectedData]) -> Dict[str, Any]:
"""Convert landing point records to GeoJSON FeatureCollection"""
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]:
features = []
for record in records:
@@ -111,19 +110,31 @@ def convert_landing_point_to_geojson(records: List[CollectedData]) -> Dict[str,
continue
metadata = record.extra_data or {}
city_id = metadata.get("city_id")
features.append(
{
"type": "Feature",
"geometry": {"type": "Point", "coordinates": [lon, lat]},
"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),
},
}
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(
{
"type": "Feature",
"geometry": {"type": "Point", "coordinates": [lon, lat]},
"properties": props,
}
)
@@ -264,11 +275,37 @@ async def get_cables_geojson(db: AsyncSession = Depends(get_db)):
@router.get("/geo/landing-points")
async def get_landing_points_geojson(db: AsyncSession = Depends(get_db)):
"""获取登陆点 GeoJSON 数据 (Point)"""
try:
stmt = select(CollectedData).where(CollectedData.source == "arcgis_landing_points")
result = await db.execute(stmt)
records = result.scalars().all()
landing_stmt = select(CollectedData).where(CollectedData.source == "arcgis_landing_points")
landing_result = await db.execute(landing_stmt)
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:
raise HTTPException(
@@ -276,7 +313,7 @@ async def get_landing_points_geojson(db: AsyncSession = Depends(get_db)):
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:
raise
except Exception as e:
@@ -285,7 +322,6 @@ async def get_landing_points_geojson(db: AsyncSession = Depends(get_db)):
@router.get("/geo/all")
async def get_all_geojson(db: AsyncSession = Depends(get_db)):
"""获取所有可视化数据 (电缆 + 登陆点)"""
cables_stmt = select(CollectedData).where(CollectedData.source == "arcgis_cables")
cables_result = await db.execute(cables_stmt)
cables_records = cables_result.scalars().all()
@@ -294,13 +330,36 @@ async def get_all_geojson(db: AsyncSession = Depends(get_db)):
points_result = await db.execute(points_stmt)
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 = (
convert_cable_to_geojson(cables_records)
if cables_records
else {"type": "FeatureCollection", "features": []}
)
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
else {"type": "FeatureCollection", "features": []}
)

View File

@@ -59,6 +59,7 @@ class ArcGISLandingPointCollector(BaseCollector):
"unit": "",
"metadata": {
"objectid": props.get("OBJECTID"),
"city_id": props.get("city_id"),
"cable_id": props.get("cable_id"),
"cable_name": props.get("cable_name"),
"facility": props.get("facility"),

View File

@@ -50,6 +50,7 @@ class ArcGISCableLandingRelationCollector(BaseCollector):
"unit": "",
"metadata": {
"objectid": props.get("OBJECTID"),
"city_id": props.get("city_id"),
"cable_id": props.get("cable_id"),
"cable_name": props.get("cable_name"),
"landing_point_id": props.get("landing_point_id"),

View File

@@ -286,7 +286,7 @@ export async function loadLandingPoints(scene, earthObj) {
sphere.userData = {
type: 'landingPoint',
name: properties.name || '未知登陆站',
cableName: properties.cable_system || '未知系统',
cableNames: properties.cable_names || [],
country: properties.country || '未知国家',
status: properties.status || 'Unknown'
};
@@ -362,3 +362,47 @@ export function getCableStateInfo() {
});
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);
});
}

View File

@@ -13,7 +13,7 @@ import {
hideTooltip
} from './ui.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 { setupControls, getAutoRotate, getShowTerrain, zoomLevel, setAutoRotate, toggleAutoRotate, resetView } from './controls.js';
import { initInfoCard, showInfoCard, hideInfoCard, getCurrentType, setInfoCardNoBorder } from './info-card.js';
@@ -475,6 +475,14 @@ function animate() {
applyCableVisualState();
if (lockedObjectType === 'cable' && lockedObject) {
applyLandingPointVisualState(lockedObject.userData.name, false);
} else if (lockedObjectType === 'satellite' && lockedSatellite) {
applyLandingPointVisualState(null, true);
} else {
resetLandingPointVisualState();
}
updateSatellitePositions(16);
const satPositions = getSatellitePositions();