HarmonyOS Sports Development: How to Draw Sports Speed Trajectories
Foreword
In outdoor sports applications, drawing sports speed trajectories can not only intuitively display the user's sports route but also reflect changes in speed through color variations, helping users better understand their sports status. However, how can we implement this feature in the HarmonyOS system? This article will combine practical development experience to deeply analyze the entire process from data processing to map drawing, guiding you step by step on how to draw sports speed trajectories.
I. Core Tools: Trajectory Colors and Optimization
The key to drawing sports speed trajectories lies in two tool classes: PathGradientTool
and PathSmoothTool
. These two tool classes are used to handle the colors of the trajectories and optimize the smoothness of the trajectories, respectively.
- Trajectory Color Tool Class:
PathGradientTool
The role of PathGradientTool
is to assign colors to trajectory points based on sports speed. The faster the speed, the closer the color is to cyan; the slower the speed, the closer the color is to red. Below is the core logic of PathGradientTool
:
export class PathGradientTool {
/**
* Get the path coloring array
* @param points Trajectory point data
* @param colorInterval Coloring interval, unit m, range 20-2000, how often to set a color over a certain distance
* @returns Path coloring array
*/
static getPathColors(points: RunPoint[], colorInterval: number): string[] | null {
if (!points || points.length < 2) {
return null;
}
let interval = Math.max(20, Math.min(2000, colorInterval));
const pointsSize = points.length;
const speedList: number[] = [];
const colorList: string[] = [];
let index = 0;
let lastDistance = 0;
let lastTime = 0;
let maxSpeed = 0;
let minSpeed = 0;
// First pass: Collect speed data
points.forEach(point => {
index++;
if (point.totalDistance - lastDistance > interval) {
let currentSpeed = 0;
if (point.netDuration - lastTime > 0) {
currentSpeed = (point.netDistance - lastDistance) / (point.netDuration - lastTime);
}
maxSpeed = Math.max(maxSpeed, currentSpeed);
minSpeed = minSpeed === 0 ? currentSpeed : Math.min(minSpeed, currentSpeed);
lastDistance = point.netDistance;
lastTime = point.netDuration;
// Add the same speed to each point within the interval
for (let i = 0; i < index; i++) {
speedList.push(currentSpeed);
}
// Add a barrier
speedList.push(Number.MAX_VALUE);
index = 0;
}
});
// Handle remaining points
if (index > 0) {
const lastPoint = points[points.length - 1];
let currentSpeed = 0;
if (lastPoint.netDuration - lastTime > 0) {
currentSpeed = (lastPoint.netDistance - lastDistance) / (lastPoint.netDuration - lastTime);
}
for (let i = 0; i < index; i++) {
speedList.push(currentSpeed);
}
}
// Ensure the speed list length matches the number of points
if (speedList.length !== points.length) {
// Adjust the speed list length
if (speedList.length > points.length) {
speedList.length = points.length;
} else {
const lastSpeed = speedList.length > 0 ? speedList[speedList.length - 1] : 0;
while (speedList.length < points.length) {
speedList.push(lastSpeed);
}
}
}
// Generate the color list
let lastColor = '';
let hasBarrier = false;
for (let i = 0; i < speedList.length; i++) {
const speed = speedList[i];
if (speed === Number.MAX_VALUE) {
hasBarrier = true;
continue;
}
const color = PathGradientTool.getAgrSpeedColorHashMap(speed, maxSpeed, minSpeed);
if (hasBarrier) {
hasBarrier = false;
if (color.toUpperCase() === lastColor.toUpperCase()) {
colorList.push(PathGradientTool.getBarrierColor(color));
continue;
}
}
colorList.push(color);
lastColor = color;
}
// Ensure the color list length matches the number of points
if (colorList.length !== points.length) {
if (colorList.length > points.length) {
colorList.length = points.length;
} else {
const lastColor = colorList.length > 0 ? colorList[colorList.length - 1] : '#FF3032';
while (colorList.length < points.length) {
colorList.push(lastColor);
}
}
}
return colorList;
}
/**
* Define different color ranges based on speed to draw the trajectory
* @param speed Speed
* @param maxSpeed Maximum speed
* @param minSpeed Minimum speed
* @returns Color value
*/
private static getAgrSpeedColorHashMap(speed: number, maxSpeed: number, minSpeed: number): string {
const range = maxSpeed - minSpeed;
if (speed <= minSpeed + range * 0.2) { // 0-20% speed range
return '#FF3032';
} else if (speed <= minSpeed + range * 0.4) { // 20%-40% speed range
return '#FA7B22';
} else if (speed <= minSpeed + range * 0.6) { // 40%-60% speed range
return '#F5BE14';
} else if (speed <= minSpeed + range * 0.8) { // 60%-80% speed range
return '#7AC36C';
} else { // 80%-100% speed range
return '#00C8C3';
}
}
}
- Trajectory Optimization Tool Class:
PathSmoothTool
The role of PathSmoothTool
is to optimize the smoothness of the trajectory, reducing noise and redundancy in trajectory points. Below is the core logic of PathSmoothTool
:
export class PathSmoothTool {
private mIntensity: number = 3;
private mThreshhold: number = 0.01;
private mNoiseThreshhold: number = 10;
/**
* Trajectory smoothing optimization
* @param originlist Original trajectory list, list.size greater than 2
* @returns Optimized trajectory list
*/
pathOptimize(originlist: RunLatLng[]): RunLatLng[] {
const list = this.removeNoisePoint(originlist); // Noise reduction
const afterList = this.kalmanFilterPath(list, this.mIntensity); // Filtering
const pathoptimizeList = this.reducerVerticalThreshold(afterList, this.mThreshhold); // Thinning
return pathoptimizeList;
}
/**
* Trajectory line filtering
* @param originlist Original trajectory list, list.size greater than 2
* @returns Filtered trajectory list
*/
kalmanFilterPath(originlist: RunLatLng[], intensity: number = this.mIntensity): RunLatLng[] {
const kalmanFilterList: RunLatLng[] = [];
if (!originlist || originlist.length <= 2) return kalmanFilterList;
this.initial(); // Initialize filter parameters
let lastLoc = originlist[0];
kalmanFilterList.push(lastLoc);
for (let i = 1; i < originlist.length; i++) {
const curLoc = originlist[i];
const latLng = this.kalmanFilterPoint(lastLoc, curLoc, intensity);
if (latLng) {
kalmanFilterList.push(latLng);
lastLoc = latLng;
}
}
return kalmanFilterList;
}
/**
* Single point filtering
* @param lastLoc Last location point coordinates
* @param curLoc Current location point coordinates
* @returns Filtered current location point coordinates
*/
kalmanFilterPoint(lastLoc: RunLatLng, curLoc: RunLatLng, intensity: number = this.mIntensity): RunLatLng | null {
if (this.pdelt_x === 0 || this.pdelt_y === 0) {
this.initial();
}
if (!lastLoc || !curLoc) return null;
intensity = Math.max(1, Math.min(5, intensity));
let filteredLoc = curLoc;
for (let j = 0; j < intensity; j++) {
filteredLoc = this.kalmanFilter(lastLoc.longitude, filteredLoc.longitude, lastLoc.latitude, filteredLoc.latitude);
}
return filteredLoc;
}
/**
* Trajectory thinning
* @param inPoints Trajectory list to be thinned
* @param threshHold Threshold
* @returns Thinned trajectory list
*/
private reducerVerticalThreshold(inPoints: RunLatLng[], threshHold: number): RunLatLng[] {
if (!inPoints || inPoints.length <= 2) return inPoints || [];
const ret: RunLatLng[] = [];
for (let i = 0; i < inPoints.length; i++) {
const pre = this.getLastLocation(ret);
const cur = inPoints[i];
if (!pre || i === inPoints.length - 1) {
ret.push(cur);
continue;
}
const next = inPoints[i + 1];
const distance = this.calculateDistanceFromPoint(cur, pre, next);
if (distance > threshHold) {
ret.push(cur);
}
}
return ret;
}
/**
* Trajectory noise reduction
* @param inPoints Original trajectory list
* @returns Noise-reduced trajectory list
*/
removeNoisePoint(inPoints: RunLatLng[]): RunLatLng[] {
if (!inPoints || inPoints.length <= 2) return inPoints || [];
const ret: RunLatLng[] = [];
for (let i = 0; i < inPoints.length; i++) {
const pre = this.getLastLocation(ret);
const cur = inPoints[i];
if (!pre || i === inPoints.length - 1) {
ret.push(cur);
continue;
}
const next = inPoints[i + 1];
const distance = this.calculateDistanceFromPoint(cur, pre, next);
if (distance < this.mNoiseThreshhold) {
ret.push(cur);
}
}
return ret;
}
/**
* Get the last location point
*/
private getLastLocation(points: RunLatLng[]): RunLatLng | null {
if (!points || points.length === 0) return null;
return points[points.length - 1];
}
/**
* Calculate the perpendicular distance from a point to a line
*/
private calculateDistanceFromPoint(p: RunLatLng, lineBegin: RunLatLng, lineEnd: RunLatLng): number {
const A = p.longitude - lineBegin.longitude;
const B = p.latitude - lineBegin.latitude;
const C = lineEnd.longitude - lineBegin.longitude;
const D = lineEnd.latitude - lineBegin.latitude;
const dot = A * C + B * D;
const len_sq = C * C + D * D;
const param = dot / len_sq;
let xx: number, yy: number;
if (param < 0 || (lineBegin.longitude === lineEnd.longitude && lineBegin.latitude === lineEnd.latitude)) {
xx = lineBegin.longitude;
yy = lineBegin.latitude;
} else if (param > 1) {
xx = lineEnd.longitude;
yy = lineEnd.latitude;
} else {
xx = lineBegin.longitude + param * C;
yy = lineBegin.latitude + param * D;
}
const point = new RunLatLng(yy, xx);
return this.calculateLineDistance(p, point);
}
/**
* Calculate the distance between two points
*/
private calculateLineDistance(point1: RunLatLng, point2: RunLatLng): number {
const EARTH_RADIUS = 6378137.0;
const lat1 = this.rad(point1.latitude);
const lat2 = this.rad(point2.latitude);
const a = lat1 - lat2;
const b = this.rad(point1.longitude) - this.rad(point2.longitude);
const s = 2 * Math.asin(Math.sqrt(Math.pow(Math.sin(a / 2), 2) +
Math.cos(lat1) * Math.cos(lat2) * Math.pow(Math.sin(b / 2), 2)));
return s * EARTH_RADIUS;
}
/**
* Convert degrees to radians
*/
private rad(d: number): number {
return d * Math.PI / 180.0;
}
/**
* Trajectory thinning (processing source data simultaneously)
* @param inPoints Trajectory list to be thinned
* @param sourcePoints Source data list, corresponding to inPoints one by one
* @param threshHold Threshold
* @returns A tuple containing the thinned trajectory list and the corresponding source data list
*/
reducerVerticalThresholdWithSource<T>(inPoints: RunLatLng[], sourcePoints: T[], threshHold: number = this.mThreshhold): { points: RunLatLng[], sources: T[] } {
if (!inPoints || !sourcePoints || inPoints.length <= 2 || inPoints.length !== sourcePoints.length) {
return { points: inPoints || [], sources: sourcePoints || [] };
}
const retPoints: RunLatLng[] = [];
const retSources: T[] = [];
for (let i = 0; i < inPoints.length; i++) {
const pre = this.getLastLocation(retPoints);
const cur = inPoints[i];
if (!pre || i === inPoints.length - 1) {
retPoints.push(cur);
retSources.push(sourcePoints[i]);
continue;
}
const next = inPoints[i + 1];
const distance = this.calculateDistanceFromPoint(cur, pre, next);
if (distance > threshHold) {
retPoints.push(cur);
retSources.push(sourcePoints[i]);
}
}
return { points: retPoints, sources: retSources };
}
}
II. Drawing Sports Speed Trajectories
With the above two tool classes, we can now start drawing sports speed trajectories. Below is the complete process for drawing the trajectory:
- Prepare Trajectory Point Data
First, convert the original trajectory point data into a RunLatLng
array for subsequent processing:
// Convert trajectory points to RunLatLng array for optimization
let tempTrackPoints = this.record!.points.map(point => new RunLatLng(point.latitude, point.longitude));
- Optimize Trajectory Points
Use PathSmoothTool
to optimize the trajectory points, including noise reduction, filtering, and thinning. To ensure the correctness of the source data, I only performed thinning here:
// Trajectory optimization
const pathSmoothTool = new PathSmoothTool();
const optimizedPoints = pathSmoothTool.reducerVerticalThresholdWithSource<RunPoint>(tempTrackPoints, this.record!.points);
- Convert to Map Display Format
Convert the optimized trajectory points into the LatLng
format required by the map:
// Convert the optimized points to LatLng array for map display
this.trackPoints = optimizedPoints.points.map(point => new LatLng(point.latitude, point.longitude));
- Get Trajectory Color Array
Use PathGradientTool
to generate a color array for the trajectory points based on speed:
// Get the trajectory color array
const colors = PathGradientTool.getPathColors(optimizedPoints.sources, 100);
- Draw Trajectory Line
Pass the trajectory points and color array to the map component to draw the trajectory line:
if (this.trackPoints.length > 0) {
// Set the map center point to the first point
this.mapController.setMapCenter({
lat: this.trackPoints[0].lat,
lng: this.trackPoints[0].lng
}, 15);
// Create the trajectory line
this.polyline = new Polyline({
points: this.trackPoints,
width: 5,
join: SysEnum.LineJoinType.ROUND,
cap: SysEnum.LineCapType.ROUND,
isGradient: true,
colorList: colors
});
// Add the trajectory line to the map
this.mapController.addOverlay(this.polyline);
}
III. Core Points of the Code
- Trajectory Color Calculation
PathGradientTool
assigns colors to trajectory points based on speed ranges. The faster the speed, the closer the color is to cyan; the slower the speed, the closer the color is to red. The color gradient is achieved through the getGradient
method.
- Trajectory Optimization
PathSmoothTool
uses the Kalman filter algorithm to filter trajectory points, reducing noise and redundant points. Trajectory thinning is achieved through a vertical distance threshold, reducing the number of trajectory points and improving drawing performance.
- Map Drawing
Using Baidu Map components (such as Polyline
) to draw trajectory lines and implementing color gradient effects through colorList
. The map center point is set to the starting point of the trajectory to ensure the complete display of the trajectory.
IV. Summary and Outlook
Through the above steps, we have successfully implemented the drawing of sports speed trajectories. The trajectory colors reflect changes in speed, and the optimized trajectory is smoother and more performance-efficient.
Top comments (0)