Post

Replies

Boosts

Views

Activity

Workout route map double-converts the first ~49 samples on mainland-China tracks (FB25306175)
Every ride I import into HealthKit from a Chinese bike computer renders wrong at the start in the Fitness and Health apps, and the same record is wrong on iOS 26 as well as on iOS 27.2 (iPhone 15 Pro, mainland China, AutoNavi base map). Filed as FB25306175. WHAT HAPPENS The first ~49 samples of an HKWorkoutRoute are drawn about 415 m east and 222 m south of the coordinates actually stored, and a straight line joins that piece back to the rest of the track. The green start marker is on the displaced piece. Every remaining sample, and the red end marker, are drawn exactly where stored. On this record (2794 points, 1 Hz, 17649 m drawn) the displaced block is the first 209 m of riding — about 53 seconds The displacement is one more application of the local WGS-84 → GCJ-02 transform, which these coordinates already contain: measured from screenshots at native 1179×2556, 417 m E / 219 m S on the full-screen map and 418 m E / 220 m S on the summary map, against a true local offset of 415.2 m E / 222.4 m S. THE STORED DATA IS CORRECT HKWorkoutRouteQuery returns all 2794 CLLocation values unchanged; the descriptor query returns the same count and the same first/last values. One workout, one route. The same coordinates drawn with SwiftUI Map + MapPolyline on the same phone render correctly end to end, including the start. The same unchanged workout, opened twice with no write in between, was correct the first time and offset after leaving and re-entering. So the data is not the variable. THE WRITE PATH The head unit writes GCJ-02 into record.position_lat / position_long. That is what makes the track land on the road here: MKMapSnapshotter on the mainland base map puts the GCJ-02 values exactly on two known intersections, and the WGS-84 values about 470 m north-west of them, and MapKit does no conversion for us. So they are written verbatim: let config = HKWorkoutConfiguration() config.activityType = .cycling config.locationType = .outdoor let builder = HKWorkoutBuilder(healthStore: store, configuration: config, device: nil) try await builder.beginCollection(at: firstPoint.timestamp.addingTimeInterval(-1)) // CLLocation per record, coordinates as recorded, horizontalAccuracy 10, // verticalAccuracy 10 (or -1 where the record has no altitude), recorded altitude // and speed, course computed from the previous point. let routeBuilder = builder.seriesBuilder(for: HKSeriesType.workoutRoute()) as? HKWorkoutRouteBuilder for batch in locations.chunked(into: 1000) { try await routeBuilder?.insertRouteData(batch) } try await builder.endCollection(at: lastPoint.timestamp.addingTimeInterval(10)) let workout = await builder.finishWorkout()WHAT IS NOT THE CAUSE Established with controlled synthetic routes written from the same start point: probe sample interval m per point displaced length displaced time samples E 1 s 10 ~480 m ~48 s ~49 F 2 s 10 ~480 m ~96 s ~49 G 1 s 20 ~950 m ~48 s ~49 The block follows SAMPLE COUNT, not time and not distance. Also ruled out, each by writing a variant: lap and pause/resume events (present, absent, and a 206-point probe carrying all three); the first point's metadata (altitude 0 / course -1 / verticalAccuracy -1); valid versus -1 values for horizontalAccuracy, verticalAccuracy, speed and course; dropping an isolated leading point; back-dating the first timestamp to the workout start; trimming the leading 200 m (the offset simply re-appears at the new start, which still begins with 49 samples). Writing WGS-84 instead inverts the symptom — the leading block then looks right and everything else goes ~470 m north-west — which is what you would expect if only the leading block is converted a second time. SCOPE This is not my one file. Three separate real rides, two cities, two head units, plus synthetic routes, all behave identically: MAGENE C706_54634 Suzhou 2026-10-23 17.61 km 2797 records / 2794 stored points MAGENE C706_54634 Suzhou 2026-10-22 12.66 km 2076 records MAGENE C606P_33206 Chengdu 2026-10-20 63.30 km 12069 records Apple Watch recordings made in mainland China render correctly, and a Garmin Edge 1050 file recorded in the US imported through the same code path renders correctly. The common factor is coordinates that are already GCJ-02 when they reach HKWorkoutRouteBuilder — which is every Chinese bike computer, every Chinese running app, and every GPX export from a Chinese service. So any third-party importer here hits this. WHAT I AM ASKING Does the workout map apply a datum conversion to a leading block of route samples, and is that block size fixed? What is the official coordinate contract for HKWorkoutRoute locations for apps working with GCJ-02 data in mainland China? Is there a supported way to opt out, and what decides whether a given route enters this state? Which component owns the conversion — Health/Fitness route rendering or MapKit? I am deliberately not pre-converting the first ~49 points: that would store mixed datums in HealthKit, wrong for the Health app itself, for exports, for every third-party reader, and wrong for all of them the moment this is fixed. Happy to share the one-tap sample project, the exported JSON with all 2794 stored CLLocation values, the original .fit, and the probe E/F/G screenshots — reply here and I'll attach them.
1
0
60
16h
Workout route map double-converts the first ~49 samples on mainland-China tracks (FB25306175)
Every ride I import into HealthKit from a Chinese bike computer renders wrong at the start in the Fitness and Health apps, and the same record is wrong on iOS 26 as well as on iOS 27.2 (iPhone 15 Pro, mainland China, AutoNavi base map). Filed as FB25306175. WHAT HAPPENS The first ~49 samples of an HKWorkoutRoute are drawn about 415 m east and 222 m south of the coordinates actually stored, and a straight line joins that piece back to the rest of the track. The green start marker is on the displaced piece. Every remaining sample, and the red end marker, are drawn exactly where stored. On this record (2794 points, 1 Hz, 17649 m drawn) the displaced block is the first 209 m of riding — about 53 seconds The displacement is one more application of the local WGS-84 → GCJ-02 transform, which these coordinates already contain: measured from screenshots at native 1179×2556, 417 m E / 219 m S on the full-screen map and 418 m E / 220 m S on the summary map, against a true local offset of 415.2 m E / 222.4 m S. THE STORED DATA IS CORRECT HKWorkoutRouteQuery returns all 2794 CLLocation values unchanged; the descriptor query returns the same count and the same first/last values. One workout, one route. The same coordinates drawn with SwiftUI Map + MapPolyline on the same phone render correctly end to end, including the start. The same unchanged workout, opened twice with no write in between, was correct the first time and offset after leaving and re-entering. So the data is not the variable. THE WRITE PATH The head unit writes GCJ-02 into record.position_lat / position_long. That is what makes the track land on the road here: MKMapSnapshotter on the mainland base map puts the GCJ-02 values exactly on two known intersections, and the WGS-84 values about 470 m north-west of them, and MapKit does no conversion for us. So they are written verbatim: let config = HKWorkoutConfiguration() config.activityType = .cycling config.locationType = .outdoor let builder = HKWorkoutBuilder(healthStore: store, configuration: config, device: nil) try await builder.beginCollection(at: firstPoint.timestamp.addingTimeInterval(-1)) // CLLocation per record, coordinates as recorded, horizontalAccuracy 10, // verticalAccuracy 10 (or -1 where the record has no altitude), recorded altitude // and speed, course computed from the previous point. let routeBuilder = builder.seriesBuilder(for: HKSeriesType.workoutRoute()) as? HKWorkoutRouteBuilder for batch in locations.chunked(into: 1000) { try await routeBuilder?.insertRouteData(batch) } try await builder.endCollection(at: lastPoint.timestamp.addingTimeInterval(10)) let workout = await builder.finishWorkout()WHAT IS NOT THE CAUSE Established with controlled synthetic routes written from the same start point: probe sample interval m per point displaced length displaced time samples E 1 s 10 ~480 m ~48 s ~49 F 2 s 10 ~480 m ~96 s ~49 G 1 s 20 ~950 m ~48 s ~49 The block follows SAMPLE COUNT, not time and not distance. Also ruled out, each by writing a variant: lap and pause/resume events (present, absent, and a 206-point probe carrying all three); the first point's metadata (altitude 0 / course -1 / verticalAccuracy -1); valid versus -1 values for horizontalAccuracy, verticalAccuracy, speed and course; dropping an isolated leading point; back-dating the first timestamp to the workout start; trimming the leading 200 m (the offset simply re-appears at the new start, which still begins with 49 samples). Writing WGS-84 instead inverts the symptom — the leading block then looks right and everything else goes ~470 m north-west — which is what you would expect if only the leading block is converted a second time. SCOPE This is not my one file. Three separate real rides, two cities, two head units, plus synthetic routes, all behave identically: MAGENE C706_54634 Suzhou 2026-10-23 17.61 km 2797 records / 2794 stored points MAGENE C706_54634 Suzhou 2026-10-22 12.66 km 2076 records MAGENE C606P_33206 Chengdu 2026-10-20 63.30 km 12069 records Apple Watch recordings made in mainland China render correctly, and a Garmin Edge 1050 file recorded in the US imported through the same code path renders correctly. The common factor is coordinates that are already GCJ-02 when they reach HKWorkoutRouteBuilder — which is every Chinese bike computer, every Chinese running app, and every GPX export from a Chinese service. So any third-party importer here hits this. WHAT I AM ASKING Does the workout map apply a datum conversion to a leading block of route samples, and is that block size fixed? What is the official coordinate contract for HKWorkoutRoute locations for apps working with GCJ-02 data in mainland China? Is there a supported way to opt out, and what decides whether a given route enters this state? Which component owns the conversion — Health/Fitness route rendering or MapKit? I am deliberately not pre-converting the first ~49 points: that would store mixed datums in HealthKit, wrong for the Health app itself, for exports, for every third-party reader, and wrong for all of them the moment this is fixed. Happy to share the one-tap sample project, the exported JSON with all 2794 stored CLLocation values, the original .fit, and the probe E/F/G screenshots — reply here and I'll attach them.
1
0
267
16h
Workout route map double-converts the first ~49 samples on mainland-China tracks (FB25306175)
Every ride I import into HealthKit from a Chinese bike computer renders wrong at the start in the Fitness and Health apps, and the same record is wrong on iOS 26 as well as on iOS 27.2 (iPhone 15 Pro, mainland China, AutoNavi base map). Filed as FB25306175. WHAT HAPPENS The first ~49 samples of an HKWorkoutRoute are drawn about 415 m east and 222 m south of the coordinates actually stored, and a straight line joins that piece back to the rest of the track. The green start marker is on the displaced piece. Every remaining sample, and the red end marker, are drawn exactly where stored. On this record (2794 points, 1 Hz, 17649 m drawn) the displaced block is the first 209 m of riding — about 53 seconds The displacement is one more application of the local WGS-84 → GCJ-02 transform, which these coordinates already contain: measured from screenshots at native 1179×2556, 417 m E / 219 m S on the full-screen map and 418 m E / 220 m S on the summary map, against a true local offset of 415.2 m E / 222.4 m S. THE STORED DATA IS CORRECT HKWorkoutRouteQuery returns all 2794 CLLocation values unchanged; the descriptor query returns the same count and the same first/last values. One workout, one route. The same coordinates drawn with SwiftUI Map + MapPolyline on the same phone render correctly end to end, including the start. The same unchanged workout, opened twice with no write in between, was correct the first time and offset after leaving and re-entering. So the data is not the variable. THE WRITE PATH The head unit writes GCJ-02 into record.position_lat / position_long. That is what makes the track land on the road here: MKMapSnapshotter on the mainland base map puts the GCJ-02 values exactly on two known intersections, and the WGS-84 values about 470 m north-west of them, and MapKit does no conversion for us. So they are written verbatim: let config = HKWorkoutConfiguration() config.activityType = .cycling config.locationType = .outdoor let builder = HKWorkoutBuilder(healthStore: store, configuration: config, device: nil) try await builder.beginCollection(at: firstPoint.timestamp.addingTimeInterval(-1)) // CLLocation per record, coordinates as recorded, horizontalAccuracy 10, // verticalAccuracy 10 (or -1 where the record has no altitude), recorded altitude // and speed, course computed from the previous point. let routeBuilder = builder.seriesBuilder(for: HKSeriesType.workoutRoute()) as? HKWorkoutRouteBuilder for batch in locations.chunked(into: 1000) { try await routeBuilder?.insertRouteData(batch) } try await builder.endCollection(at: lastPoint.timestamp.addingTimeInterval(10)) let workout = await builder.finishWorkout()WHAT IS NOT THE CAUSE Established with controlled synthetic routes written from the same start point: probe sample interval m per point displaced length displaced time samples E 1 s 10 ~480 m ~48 s ~49 F 2 s 10 ~480 m ~96 s ~49 G 1 s 20 ~950 m ~48 s ~49 The block follows SAMPLE COUNT, not time and not distance. Also ruled out, each by writing a variant: lap and pause/resume events (present, absent, and a 206-point probe carrying all three); the first point's metadata (altitude 0 / course -1 / verticalAccuracy -1); valid versus -1 values for horizontalAccuracy, verticalAccuracy, speed and course; dropping an isolated leading point; back-dating the first timestamp to the workout start; trimming the leading 200 m (the offset simply re-appears at the new start, which still begins with 49 samples). Writing WGS-84 instead inverts the symptom — the leading block then looks right and everything else goes ~470 m north-west — which is what you would expect if only the leading block is converted a second time. SCOPE This is not my one file. Three separate real rides, two cities, two head units, plus synthetic routes, all behave identically: MAGENE C706_54634 Suzhou 2026-10-23 17.61 km 2797 records / 2794 stored points MAGENE C706_54634 Suzhou 2026-10-22 12.66 km 2076 records MAGENE C606P_33206 Chengdu 2026-10-20 63.30 km 12069 records Apple Watch recordings made in mainland China render correctly, and a Garmin Edge 1050 file recorded in the US imported through the same code path renders correctly. The common factor is coordinates that are already GCJ-02 when they reach HKWorkoutRouteBuilder — which is every Chinese bike computer, every Chinese running app, and every GPX export from a Chinese service. So any third-party importer here hits this. WHAT I AM ASKING Does the workout map apply a datum conversion to a leading block of route samples, and is that block size fixed? What is the official coordinate contract for HKWorkoutRoute locations for apps working with GCJ-02 data in mainland China? Is there a supported way to opt out, and what decides whether a given route enters this state? Which component owns the conversion — Health/Fitness route rendering or MapKit? I am deliberately not pre-converting the first ~49 points: that would store mixed datums in HealthKit, wrong for the Health app itself, for exports, for every third-party reader, and wrong for all of them the moment this is fixed. Happy to share the one-tap sample project, the exported JSON with all 2794 stored CLLocation values, the original .fit, and the probe E/F/G screenshots — reply here and I'll attach them.
Replies
1
Boosts
0
Views
60
Activity
16h
Workout route map double-converts the first ~49 samples on mainland-China tracks (FB25306175)
Every ride I import into HealthKit from a Chinese bike computer renders wrong at the start in the Fitness and Health apps, and the same record is wrong on iOS 26 as well as on iOS 27.2 (iPhone 15 Pro, mainland China, AutoNavi base map). Filed as FB25306175. WHAT HAPPENS The first ~49 samples of an HKWorkoutRoute are drawn about 415 m east and 222 m south of the coordinates actually stored, and a straight line joins that piece back to the rest of the track. The green start marker is on the displaced piece. Every remaining sample, and the red end marker, are drawn exactly where stored. On this record (2794 points, 1 Hz, 17649 m drawn) the displaced block is the first 209 m of riding — about 53 seconds The displacement is one more application of the local WGS-84 → GCJ-02 transform, which these coordinates already contain: measured from screenshots at native 1179×2556, 417 m E / 219 m S on the full-screen map and 418 m E / 220 m S on the summary map, against a true local offset of 415.2 m E / 222.4 m S. THE STORED DATA IS CORRECT HKWorkoutRouteQuery returns all 2794 CLLocation values unchanged; the descriptor query returns the same count and the same first/last values. One workout, one route. The same coordinates drawn with SwiftUI Map + MapPolyline on the same phone render correctly end to end, including the start. The same unchanged workout, opened twice with no write in between, was correct the first time and offset after leaving and re-entering. So the data is not the variable. THE WRITE PATH The head unit writes GCJ-02 into record.position_lat / position_long. That is what makes the track land on the road here: MKMapSnapshotter on the mainland base map puts the GCJ-02 values exactly on two known intersections, and the WGS-84 values about 470 m north-west of them, and MapKit does no conversion for us. So they are written verbatim: let config = HKWorkoutConfiguration() config.activityType = .cycling config.locationType = .outdoor let builder = HKWorkoutBuilder(healthStore: store, configuration: config, device: nil) try await builder.beginCollection(at: firstPoint.timestamp.addingTimeInterval(-1)) // CLLocation per record, coordinates as recorded, horizontalAccuracy 10, // verticalAccuracy 10 (or -1 where the record has no altitude), recorded altitude // and speed, course computed from the previous point. let routeBuilder = builder.seriesBuilder(for: HKSeriesType.workoutRoute()) as? HKWorkoutRouteBuilder for batch in locations.chunked(into: 1000) { try await routeBuilder?.insertRouteData(batch) } try await builder.endCollection(at: lastPoint.timestamp.addingTimeInterval(10)) let workout = await builder.finishWorkout()WHAT IS NOT THE CAUSE Established with controlled synthetic routes written from the same start point: probe sample interval m per point displaced length displaced time samples E 1 s 10 ~480 m ~48 s ~49 F 2 s 10 ~480 m ~96 s ~49 G 1 s 20 ~950 m ~48 s ~49 The block follows SAMPLE COUNT, not time and not distance. Also ruled out, each by writing a variant: lap and pause/resume events (present, absent, and a 206-point probe carrying all three); the first point's metadata (altitude 0 / course -1 / verticalAccuracy -1); valid versus -1 values for horizontalAccuracy, verticalAccuracy, speed and course; dropping an isolated leading point; back-dating the first timestamp to the workout start; trimming the leading 200 m (the offset simply re-appears at the new start, which still begins with 49 samples). Writing WGS-84 instead inverts the symptom — the leading block then looks right and everything else goes ~470 m north-west — which is what you would expect if only the leading block is converted a second time. SCOPE This is not my one file. Three separate real rides, two cities, two head units, plus synthetic routes, all behave identically: MAGENE C706_54634 Suzhou 2026-10-23 17.61 km 2797 records / 2794 stored points MAGENE C706_54634 Suzhou 2026-10-22 12.66 km 2076 records MAGENE C606P_33206 Chengdu 2026-10-20 63.30 km 12069 records Apple Watch recordings made in mainland China render correctly, and a Garmin Edge 1050 file recorded in the US imported through the same code path renders correctly. The common factor is coordinates that are already GCJ-02 when they reach HKWorkoutRouteBuilder — which is every Chinese bike computer, every Chinese running app, and every GPX export from a Chinese service. So any third-party importer here hits this. WHAT I AM ASKING Does the workout map apply a datum conversion to a leading block of route samples, and is that block size fixed? What is the official coordinate contract for HKWorkoutRoute locations for apps working with GCJ-02 data in mainland China? Is there a supported way to opt out, and what decides whether a given route enters this state? Which component owns the conversion — Health/Fitness route rendering or MapKit? I am deliberately not pre-converting the first ~49 points: that would store mixed datums in HealthKit, wrong for the Health app itself, for exports, for every third-party reader, and wrong for all of them the moment this is fixed. Happy to share the one-tap sample project, the exported JSON with all 2794 stored CLLocation values, the original .fit, and the probe E/F/G screenshots — reply here and I'll attach them.
Replies
1
Boosts
0
Views
267
Activity
16h