完成了contactcross的初步

This commit is contained in:
sladro 2026-02-10 22:10:53 +08:00
parent aa4468d922
commit c7060ce327
8 changed files with 180 additions and 103 deletions

View File

@ -47,6 +47,11 @@ public class MovingObject {
*/ */
private Double altitude; private Double altitude;
/**
* Source timestamp from upstream payload, epoch milliseconds.
*/
private Long sourceTimestampMs;
/** /**
* 对象类型枚举 - 四种基本分类 * 对象类型枚举 - 四种基本分类
*/ */

View File

@ -121,6 +121,7 @@ public class DataCollectorDao {
aircraft.setCurrentSpeed(0.0); aircraft.setCurrentSpeed(0.0);
aircraft.setCurrentHeading(0.0); aircraft.setCurrentHeading(0.0);
aircraft.setAltitude(0.0); // 假设默认高度为0如果API有提供可以设置 aircraft.setAltitude(0.0); // 假设默认高度为0如果API有提供可以设置
aircraft.setSourceTimestampMs(normalizeUpstreamTimestampToMs(rawData.getTime()));
return aircraft; return aircraft;
}).filter(java.util.Objects::nonNull).collect(Collectors.toList()); }).filter(java.util.Objects::nonNull).collect(Collectors.toList());
@ -167,6 +168,7 @@ public class DataCollectorDao {
vehicle.setCurrentSpeed(0.0); // 暂时设置为0.0DataCollectorService会处理 vehicle.setCurrentSpeed(0.0); // 暂时设置为0.0DataCollectorService会处理
vehicle.setCurrentHeading(0.0); vehicle.setCurrentHeading(0.0);
vehicle.setAltitude(0.0); // 假设默认高度为0如果API有提供可以设置 vehicle.setAltitude(0.0); // 假设默认高度为0如果API有提供可以设置
vehicle.setSourceTimestampMs(normalizeUpstreamTimestampToMs(rawData.getTime()));
return vehicle; return vehicle;
}).filter(java.util.Objects::nonNull).collect(Collectors.toList()); }).filter(java.util.Objects::nonNull).collect(Collectors.toList());
@ -183,6 +185,21 @@ public class DataCollectorDao {
// 内部 DTOs精确匹配外部 API 接口返回的 JSON 结构 // 内部 DTOs精确匹配外部 API 接口返回的 JSON 结构
private Long normalizeUpstreamTimestampToMs(Double rawTime) {
if (rawTime == null || rawTime.isNaN() || rawTime.isInfinite()) {
return null;
}
double absolute = Math.abs(rawTime);
if (absolute < 100_000_000_000d) {
return Math.round(rawTime * 1000d);
}
if (absolute < 100_000_000_000_000d) {
return Math.round(rawTime);
}
return Math.round(rawTime / 1000d);
}
@Data @Data
@NoArgsConstructor @NoArgsConstructor
@AllArgsConstructor @AllArgsConstructor
@ -195,7 +212,7 @@ public class DataCollectorDao {
@JsonProperty("latitude") @JsonProperty("latitude")
private Double latitude; private Double latitude;
@JsonProperty("time") @JsonProperty("time")
private Long time; private Double time;
// 其他可能存在的但我们不关心的字段将被 @JsonIgnoreProperties 忽略 // 其他可能存在的但我们不关心的字段将被 @JsonIgnoreProperties 忽略
} }
@ -211,7 +228,7 @@ public class DataCollectorDao {
@JsonProperty("latitude") @JsonProperty("latitude")
private Double latitude; private Double latitude;
@JsonProperty("time") @JsonProperty("time")
private Long time; private Double time;
} }

View File

@ -27,6 +27,8 @@ import org.springframework.stereotype.Service;
import java.time.Instant; import java.time.Instant;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.time.ZoneId; import java.time.ZoneId;
import java.util.Collections;
import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Set; import java.util.Set;
@ -423,6 +425,42 @@ public class DataCollectorService {
* 定时采集航空器数据 * 定时采集航空器数据
* - 不进行数据持久化 * - 不进行数据持久化
*/ */
private List<Aircraft> deduplicateAircraftByLatestTimestamp(List<Aircraft> aircrafts) {
if (aircrafts == null || aircrafts.isEmpty()) {
return Collections.emptyList();
}
Map<String, Aircraft> latestByFlightNo = new HashMap<>();
for (Aircraft aircraft : aircrafts) {
if (aircraft == null || aircraft.getObjectId() == null || aircraft.getObjectId().isBlank()) {
continue;
}
Aircraft existing = latestByFlightNo.get(aircraft.getObjectId());
if (existing == null || isCandidateNewer(existing, aircraft)) {
latestByFlightNo.put(aircraft.getObjectId(), aircraft);
}
}
return List.copyOf(latestByFlightNo.values());
}
private boolean isCandidateNewer(Aircraft existing, Aircraft candidate) {
Long existingTs = existing.getSourceTimestampMs();
Long candidateTs = candidate.getSourceTimestampMs();
if (existingTs == null && candidateTs == null) {
return true;
}
if (existingTs == null) {
return true;
}
if (candidateTs == null) {
return false;
}
return candidateTs >= existingTs;
}
@Scheduled(fixedRateString = "${data.collector.interval}") @Scheduled(fixedRateString = "${data.collector.interval}")
public void collectAircraftData() { public void collectAircraftData() {
if (collectorDisabled) { if (collectorDisabled) {
@ -444,7 +482,13 @@ public class DataCollectorService {
log.info("采集到 {} 条航空器数据,用于实时处理", newAircrafts.size()); log.info("采集到 {} 条航空器数据,用于实时处理", newAircrafts.size());
for (Aircraft aircraft : newAircrafts) { List<Aircraft> deduplicatedAircrafts = deduplicateAircraftByLatestTimestamp(newAircrafts);
if (deduplicatedAircrafts.size() != newAircrafts.size()) {
log.info("Aircraft batch deduplicated by latest source time: raw={}, deduped={}",
newAircrafts.size(), deduplicatedAircrafts.size());
}
for (Aircraft aircraft : deduplicatedAircrafts) {
try { try {
if (aircraft.getCurrentPosition() == null) { if (aircraft.getCurrentPosition() == null) {
log.warn("航空器 {} 位置信息缺失,跳过处理。", aircraft.getObjectId()); log.warn("航空器 {} 位置信息缺失,跳过处理。", aircraft.getObjectId());
@ -464,6 +508,7 @@ public class DataCollectorService {
.currentSpeed(null) // 不在采集阶段计算速度 .currentSpeed(null) // 不在采集阶段计算速度
.currentHeading(null) // 不在采集阶段计算方向 .currentHeading(null) // 不在采集阶段计算方向
.altitude(aircraft.getAltitude()) .altitude(aircraft.getAltitude())
.sourceTimestampMs(aircraft.getSourceTimestampMs())
.build(); .build();
upsertActiveMovingObject(movingObject); upsertActiveMovingObject(movingObject);
@ -479,7 +524,7 @@ public class DataCollectorService {
} }
} }
log.info("航空器数据处理和事件发布完成,处理数量: {}", newAircrafts.size()); log.info("航空器数据处理和事件发布完成,处理数量: {}", deduplicatedAircrafts.size());
} catch (Exception e) { } catch (Exception e) {
log.error("采集航空器数据异常", e); log.error("采集航空器数据异常", e);
@ -557,6 +602,7 @@ public class DataCollectorService {
.currentSpeed(null) // 不在采集阶段计算速度 .currentSpeed(null) // 不在采集阶段计算速度
.currentHeading(null) // 不在采集阶段计算方向 .currentHeading(null) // 不在采集阶段计算方向
.altitude(vehicle.getAltitude()) .altitude(vehicle.getAltitude())
.sourceTimestampMs(vehicle.getSourceTimestampMs())
.build(); .build();
// 将最新数据更新到缓存不发送WebSocket消息统一在周期性检测中发送 // 将最新数据更新到缓存不发送WebSocket消息统一在周期性检测中发送
@ -1295,4 +1341,3 @@ public class DataCollectorService {
log.info("缓存已清理"); log.info("缓存已清理");
} }
} }

View File

@ -559,9 +559,11 @@ public class AdxpFlightServiceWebSocketClient implements WebSocketHandler {
// TISFLIGHT BizKey 先用 FlightId 关联 FuIdFuId 视作可复用业务键 // TISFLIGHT BizKey 先用 FlightId 关联 FuIdFuId 视作可复用业务键
String fuNorm = normalizeBizKey(fuIdRaw); String fuNorm = normalizeBizKey(fuIdRaw);
String normalizedFlightId = normalizeFlightNo(flightId); String normalizedFlightId = normalizeFlightNo(flightId);
if (fuNorm != null && normalizedFlightId != null) { String normalizedResolvedFlightNo = normalizeFlightNo(resolvedFlightNo);
String flightNoForMatch = normalizedFlightId != null ? normalizedFlightId : normalizedResolvedFlightNo;
if (fuNorm != null && flightNoForMatch != null) {
String fuFlightNo = firstSegmentFromBizLike(fuNorm); String fuFlightNo = firstSegmentFromBizLike(fuNorm);
if (normalizedFlightId.equalsIgnoreCase(fuFlightNo)) { if (flightNoForMatch.equalsIgnoreCase(fuFlightNo)) {
return fuNorm; return fuNorm;
} }
} }
@ -1058,6 +1060,9 @@ public class AdxpFlightServiceWebSocketClient implements WebSocketHandler {
if (flightKey != null || bizRedisKey != null) { if (flightKey != null || bizRedisKey != null) {
try { try {
setValueOnFlightAndBizKeys(flightKey, bizRedisKey, "flightNumber", resolvedFlightNo); setValueOnFlightAndBizKeys(flightKey, bizRedisKey, "flightNumber", resolvedFlightNo);
if (normalizedBizKey != null) {
setValueOnFlightAndBizKeys(flightKey, bizRedisKey, "bizKey", normalizedBizKey);
}
updateActiveBizKey(flightKey, normalizedBizKey, nowMs); updateActiveBizKey(flightKey, normalizedBizKey, nowMs);
// contactCross 仅对进港有效避免进出港参数串用 // contactCross 仅对进港有效避免进出港参数串用
if ("IN".equalsIgnoreCase(routeType) if ("IN".equalsIgnoreCase(routeType)

View File

@ -297,13 +297,17 @@ public class DataProcessingService {
finalHeading = 0.0; // 默认朝北 finalHeading = 0.0; // 默认朝北
} }
long payloadTimestamp = movingObject.getSourceTimestampMs() != null
? movingObject.getSourceTimestampMs()
: currentTime;
PositionUpdatePayload payload = PositionUpdatePayload.builder() PositionUpdatePayload payload = PositionUpdatePayload.builder()
.objectId(movingObject.getObjectId()) .objectId(movingObject.getObjectId())
.objectType(movingObject.getObjectType().name()) .objectType(movingObject.getObjectType().name())
.position(positionPayload) .position(positionPayload)
.heading(finalHeading) .heading(finalHeading)
.speed(new BigDecimal(finalSpeed).setScale(2, RoundingMode.HALF_UP).doubleValue()) .speed(new BigDecimal(finalSpeed).setScale(2, RoundingMode.HALF_UP).doubleValue())
.timestamp(currentTime) .timestamp(payloadTimestamp)
.build(); .build();
// 发布WebSocket事件 // 发布WebSocket事件

View File

@ -42,7 +42,6 @@ import java.util.concurrent.atomic.AtomicBoolean;
@Component @Component
public class WebSocketMessageBroadcaster { public class WebSocketMessageBroadcaster {
private static final int REACQUIRE_AFTER_REJECTS = 3;
private static final double EARTH_RADIUS_METERS = 6371000.0; private static final double EARTH_RADIUS_METERS = 6371000.0;
private static final double MIN_DT_SECONDS = 0.05; private static final double MIN_DT_SECONDS = 0.05;
@ -117,6 +116,9 @@ public class WebSocketMessageBroadcaster {
@Value("${websocket.position.filter.state-ttl-ms:120000}") @Value("${websocket.position.filter.state-ttl-ms:120000}")
private long positionTrackStateTtlMs; private long positionTrackStateTtlMs;
@Value("${websocket.position.filter.reacquire-gap-ms:8000}")
private long reacquireGapMs;
private record CoalescedPositionUpdate(PositionUpdatePayload payload, long timestamp) {} private record CoalescedPositionUpdate(PositionUpdatePayload payload, long timestamp) {}
private static final class PositionTrackState { private static final class PositionTrackState {
@ -391,6 +393,7 @@ public class WebSocketMessageBroadcaster {
} }
long payloadTimestamp = payload.getTimestamp() != null ? payload.getTimestamp() : System.currentTimeMillis(); long payloadTimestamp = payload.getTimestamp() != null ? payload.getTimestamp() : System.currentTimeMillis();
long filterTimestampMs = System.currentTimeMillis();
PositionTrackState state = positionTrackStates.computeIfAbsent(payload.getObjectId(), k -> new PositionTrackState()); PositionTrackState state = positionTrackStates.computeIfAbsent(payload.getObjectId(), k -> new PositionTrackState());
double filteredLatitude = latitude; double filteredLatitude = latitude;
@ -398,14 +401,14 @@ public class WebSocketMessageBroadcaster {
synchronized (state) { synchronized (state) {
if (!state.initialized) { if (!state.initialized) {
initializeTrackState(state, latitude, longitude, payloadTimestamp); initializeTrackState(state, latitude, longitude, filterTimestampMs);
} else { } else {
if (payloadTimestamp <= state.lastPayloadTimestampMs) { if (filterTimestampMs <= state.lastPayloadTimestampMs) {
state.rejectedCount++; state.rejectedCount++;
return null; return null;
} }
long dtMs = payloadTimestamp - state.lastPayloadTimestampMs; long dtMs = filterTimestampMs - state.lastPayloadTimestampMs;
double dtSec = dtMs / 1000.0; double dtSec = dtMs / 1000.0;
double rawDistance = calculateDistanceMeters( double rawDistance = calculateDistanceMeters(
state.lastRawLatitude, state.lastRawLatitude,
@ -418,17 +421,27 @@ public class WebSocketMessageBroadcaster {
double maxAllowedJump = resolveMaxAllowedJumpMeters(adaptive.maxSpeedMps(), adaptive.jumpMarginMeter(), dtSec); double maxAllowedJump = resolveMaxAllowedJumpMeters(adaptive.maxSpeedMps(), adaptive.jumpMarginMeter(), dtSec);
boolean isOutlierJump = rawDistance > maxAllowedJump; boolean isOutlierJump = rawDistance > maxAllowedJump;
if (isOutlierJump && state.rejectedCount < REACQUIRE_AFTER_REJECTS - 1) {
state.rejectedCount++;
log.debug("Drop outlier position update: objectId={}, jumpMeter={}, allowedMeter={}",
payload.getObjectId(), rawDistance, maxAllowedJump);
return null;
}
if (isOutlierJump) { if (isOutlierJump) {
state.rejectedCount = 0; boolean longGapReacquire = dtMs >= Math.max(reacquireGapMs, 0L);
filteredLatitude = latitude; if (longGapReacquire) {
filteredLongitude = longitude; state.rejectedCount = 0;
filteredLatitude = latitude;
filteredLongitude = longitude;
} else {
state.rejectedCount++;
double[] clipped = clipPointByMaxDistance(
state.lastFilteredLatitude,
state.lastFilteredLongitude,
latitude,
longitude,
maxAllowedJump
);
filteredLatitude = clipped[0];
filteredLongitude = clipped[1];
log.debug("Clip outlier position update: objectId={}, jumpMeter={}, allowedMeter={}, dtMs={}",
payload.getObjectId(), rawDistance, maxAllowedJump, dtMs);
}
} else { } else {
state.rejectedCount = 0; state.rejectedCount = 0;
if (rawDistance <= adaptive.jitterMeter()) { if (rawDistance <= adaptive.jitterMeter()) {
@ -443,7 +456,7 @@ public class WebSocketMessageBroadcaster {
state.lastRawLatitude = latitude; state.lastRawLatitude = latitude;
state.lastRawLongitude = longitude; state.lastRawLongitude = longitude;
state.lastPayloadTimestampMs = payloadTimestamp; state.lastPayloadTimestampMs = filterTimestampMs;
state.lastFilteredLatitude = filteredLatitude; state.lastFilteredLatitude = filteredLatitude;
state.lastFilteredLongitude = filteredLongitude; state.lastFilteredLongitude = filteredLongitude;
state.lastAcceptedAtMs = System.currentTimeMillis(); state.lastAcceptedAtMs = System.currentTimeMillis();
@ -556,4 +569,21 @@ public class WebSocketMessageBroadcaster {
} }
return Math.min(value, max); return Math.min(value, max);
} }
private double[] clipPointByMaxDistance(
double startLatitude,
double startLongitude,
double targetLatitude,
double targetLongitude,
double maxDistanceMeters) {
double distanceMeters = calculateDistanceMeters(startLatitude, startLongitude, targetLatitude, targetLongitude);
if (distanceMeters <= maxDistanceMeters || distanceMeters <= 0.0 || maxDistanceMeters <= 0.0) {
return new double[] {targetLatitude, targetLongitude};
}
double ratio = maxDistanceMeters / distanceMeters;
double clippedLatitude = startLatitude + (targetLatitude - startLatitude) * ratio;
double clippedLongitude = startLongitude + (targetLongitude - startLongitude) * ratio;
return new double[] {clippedLatitude, clippedLongitude};
}
} }

View File

@ -3,37 +3,39 @@ package com.qaup.collision.websocket.event;
import com.qaup.collision.common.model.spatial.VehicleLocation; import com.qaup.collision.common.model.spatial.VehicleLocation;
import com.qaup.collision.websocket.message.MessageTypeConstants; import com.qaup.collision.websocket.message.MessageTypeConstants;
import com.qaup.collision.websocket.message.PositionUpdatePayload; import com.qaup.collision.websocket.message.PositionUpdatePayload;
import lombok.Getter;
import lombok.Data;
import lombok.Builder;
import lombok.NoArgsConstructor;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.Getter;
import lombok.NoArgsConstructor;
import java.util.UUID; import java.util.UUID;
/** /**
* 位置更新事件 * Position update event.
* 当车辆或航空器位置数据处理完成后发布此事件 *
* Outer event timestamp:
* - {@link #timestamp} is generated at publish time in epoch microseconds for envelope ordering.
*
* Payload timestamp:
* - {@link PositionUpdatePayload#getTimestamp()} is the sample time from data pipeline in epoch milliseconds.
*/ */
@Getter @Getter
public class PositionUpdateEvent implements WebSocketEvent { public class PositionUpdateEvent implements WebSocketEvent {
private final String eventId; private final String eventId;
private final long timestamp; private final long timestamp;
private final Object payload; // 支持VehicleLocation和PositionUpdatePayload两种类型 private final Object payload;
// 支持VehicleLocation构造函数用于无人车数据
public PositionUpdateEvent(VehicleLocation vehicleLocation) { public PositionUpdateEvent(VehicleLocation vehicleLocation) {
this.eventId = UUID.randomUUID().toString(); this.eventId = UUID.randomUUID().toString();
this.timestamp = java.time.Instant.now().toEpochMilli() * 1000; // 微秒级绝对时间戳 this.timestamp = java.time.Instant.now().toEpochMilli() * 1000;
this.payload = vehicleLocation; this.payload = vehicleLocation;
} }
// 支持PositionUpdatePayload构造函数用于航空器和机场车辆数据
public PositionUpdateEvent(PositionUpdatePayload positionPayload) { public PositionUpdateEvent(PositionUpdatePayload positionPayload) {
this.eventId = UUID.randomUUID().toString(); this.eventId = UUID.randomUUID().toString();
this.timestamp = java.time.Instant.now().toEpochMilli() * 1000; // 微秒级绝对时间戳 this.timestamp = java.time.Instant.now().toEpochMilli() * 1000;
this.payload = positionPayload; this.payload = positionPayload;
} }
@ -47,32 +49,13 @@ public class PositionUpdateEvent implements WebSocketEvent {
return payload; return payload;
} }
/**
* 获取车辆ID索引性质
*
* 按照API设计原则
* - 对于无人车数据返回数据库主键vehicleId
* - 对于航空器/机场车辆返回null因为它们没有数据库记录
*
* @return 车辆IDLong类型不适用时返回null
*/
public Long getVehicleId() { public Long getVehicleId() {
if (payload instanceof VehicleLocation) { if (payload instanceof VehicleLocation) {
return ((VehicleLocation) payload).getVehicleId(); return ((VehicleLocation) payload).getVehicleId();
} }
// 航空器和机场车辆没有数据库记录返回null
return null; return null;
} }
/**
* 获取业务标识符详细信息
*
* 按照API设计原则
* - 对于无人车数据返回车牌号
* - 对于航空器/机场车辆返回objectId可能是车牌号航班号等
*
* @return 业务标识符String类型
*/
public String getLicensePlate() { public String getLicensePlate() {
if (payload instanceof VehicleLocation) { if (payload instanceof VehicleLocation) {
return ((VehicleLocation) payload).getLicensePlate(); return ((VehicleLocation) payload).getLicensePlate();
@ -82,11 +65,6 @@ public class PositionUpdateEvent implements WebSocketEvent {
return null; return null;
} }
/**
* 获取完整的标识符信息
*
* @return 包含vehicleId和businessId的完整信息
*/
public VehicleIdentifier getVehicleIdentifier() { public VehicleIdentifier getVehicleIdentifier() {
if (payload instanceof VehicleLocation) { if (payload instanceof VehicleLocation) {
VehicleLocation location = (VehicleLocation) payload; VehicleLocation location = (VehicleLocation) payload;
@ -98,7 +76,7 @@ public class PositionUpdateEvent implements WebSocketEvent {
} else if (payload instanceof PositionUpdatePayload) { } else if (payload instanceof PositionUpdatePayload) {
PositionUpdatePayload positionPayload = (PositionUpdatePayload) payload; PositionUpdatePayload positionPayload = (PositionUpdatePayload) payload;
return VehicleIdentifier.builder() return VehicleIdentifier.builder()
.vehicleId(null) // 航空器没有数据库记录 .vehicleId(null)
.businessId(positionPayload.getObjectId()) .businessId(positionPayload.getObjectId())
.vehicleType(positionPayload.getObjectType()) .vehicleType(positionPayload.getObjectType())
.build(); .build();
@ -106,9 +84,6 @@ public class PositionUpdateEvent implements WebSocketEvent {
return null; return null;
} }
/**
* 获取车辆类型
*/
public String getVehicleType() { public String getVehicleType() {
if (payload instanceof VehicleLocation) { if (payload instanceof VehicleLocation) {
return ((VehicleLocation) payload).getVehicleType().name(); return ((VehicleLocation) payload).getVehicleType().name();
@ -118,16 +93,13 @@ public class PositionUpdateEvent implements WebSocketEvent {
return null; return null;
} }
/**
* 车辆标识符信息类
*/
@Data @Data
@Builder @Builder
@NoArgsConstructor @NoArgsConstructor
@AllArgsConstructor @AllArgsConstructor
public static class VehicleIdentifier { public static class VehicleIdentifier {
private Long vehicleId; // 数据库主键索引性质 private Long vehicleId;
private String businessId; // 业务标识符车牌号航班号等 private String businessId;
private String vehicleType; // 车辆类型 private String vehicleType;
} }
} }

View File

@ -1,14 +1,13 @@
package com.qaup.collision.websocket.message; package com.qaup.collision.websocket.message;
import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
import lombok.Builder;
import lombok.NoArgsConstructor;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
/** /**
* 位置更新消息负载 * Position update payload.
* 对应前端JSON格式: position_update
*/ */
@Data @Data
@Builder @Builder
@ -17,43 +16,43 @@ import lombok.AllArgsConstructor;
public class PositionUpdatePayload { public class PositionUpdatePayload {
/** /**
* 对象ID车辆ID航班号等 * Object ID, such as vehicle ID or flight number.
*/ */
@JsonProperty("object_id") @JsonProperty("object_id")
private String objectId; private String objectId;
/** /**
* 对象类型基于MovingObjectType枚举 * Object type mapped from MovingObjectType.
*/ */
@JsonProperty("object_type") @JsonProperty("object_type")
private String objectType; private String objectType;
/** /**
* 位置信息 * Current position.
*/ */
@JsonProperty("position") @JsonProperty("position")
private Position position; private Position position;
/** /**
* 航向角 * Heading in degrees.
*/ */
@JsonProperty("heading") @JsonProperty("heading")
private Double heading; private Double heading;
/** /**
* 速度/ * Speed value from backend (current behavior is km/h).
*/ */
@JsonProperty("speed") @JsonProperty("speed")
private Double speed; private Double speed;
/** /**
* 时间戳微秒级 * Position sample timestamp in epoch milliseconds.
*/ */
@JsonProperty("timestamp") @JsonProperty("timestamp")
private Long timestamp; private Long timestamp;
/** /**
* 位置坐标类 * Coordinate model.
*/ */
@Data @Data
@Builder @Builder
@ -62,13 +61,13 @@ public class PositionUpdatePayload {
public static class Position { public static class Position {
/** /**
* 纬度 * Latitude.
*/ */
@JsonProperty("latitude") @JsonProperty("latitude")
private Double latitude; private Double latitude;
/** /**
* 经度 * Longitude.
*/ */
@JsonProperty("longitude") @JsonProperty("longitude")
private Double longitude; private Double longitude;