@capgo/background-geolocation 7.0.14 → 7.0.19
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CapgoBackgroundGeolocation.podspec +4 -3
- package/Package.swift +28 -0
- package/README.md +19 -2
- package/android/src/main/java/com/capgo/capacitor_background_geolocation/BackgroundGeolocation.java +270 -336
- package/android/src/main/java/com/capgo/capacitor_background_geolocation/BackgroundGeolocationService.java +267 -329
- package/dist/esm/definitions.js.map +1 -1
- package/dist/esm/index.d.ts +2 -2
- package/dist/esm/index.js +4 -4
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/web.d.ts +2 -2
- package/dist/esm/web.js +16 -24
- package/dist/esm/web.js.map +1 -1
- package/dist/plugin.cjs.js +16 -24
- package/dist/plugin.cjs.js.map +1 -1
- package/dist/plugin.js +16 -24
- package/dist/plugin.js.map +1 -1
- package/ios/{Plugin/Plugin.swift → Sources/CapgoBackgroundGeolocationPlugin/CapgoCapacitorBackgroundGeolocationPlugin.swift} +9 -1
- package/ios/Tests/CapgoBackgroundGeolocationPluginTests/CapgoBackgroundGeolocationPluginTests.swift +15 -0
- package/package.json +11 -10
- package/ios/Plugin/Info.plist +0 -24
- package/ios/Plugin/Plugin.h +0 -10
- package/ios/Plugin/Plugin.m +0 -9
|
@@ -22,358 +22,296 @@ import com.getcapacitor.Logger;
|
|
|
22
22
|
// added, and demoted when the last background watcher is removed.
|
|
23
23
|
public class BackgroundGeolocationService extends Service {
|
|
24
24
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
private final IBinder binder = new LocalBinder();
|
|
28
|
-
|
|
29
|
-
private static final double EARTH_RADIUS_M = 6371000;
|
|
30
|
-
|
|
31
|
-
// Must be unique for this application.
|
|
32
|
-
private static final int NOTIFICATION_ID = 28351;
|
|
33
|
-
|
|
34
|
-
private String callbackId;
|
|
35
|
-
|
|
36
|
-
private LocationManager client;
|
|
37
|
-
private LocationListener locationCallback;
|
|
38
|
-
private MediaPlayer mediaPlayer;
|
|
39
|
-
private double[][] route;
|
|
40
|
-
private double distanceThreshold;
|
|
41
|
-
private boolean isOffRoute;
|
|
42
|
-
|
|
43
|
-
@Override
|
|
44
|
-
public IBinder onBind(Intent intent) {
|
|
45
|
-
return binder;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
// Some devices allow a foreground service to outlive the application's main
|
|
49
|
-
// activity, leading to nasty crashes as reported in issue #59. If we learn
|
|
50
|
-
// that the application has been killed, all watchers are stopped and the
|
|
51
|
-
// service is terminated immediately.
|
|
52
|
-
@Override
|
|
53
|
-
public boolean onUnbind(Intent intent) {
|
|
54
|
-
if (client != null && locationCallback != null) {
|
|
55
|
-
client.removeUpdates(locationCallback);
|
|
56
|
-
}
|
|
57
|
-
releaseMediaPlayer();
|
|
58
|
-
stopSelf();
|
|
59
|
-
return false;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
@Override
|
|
63
|
-
public void onDestroy() {
|
|
64
|
-
if (client != null && locationCallback != null) {
|
|
65
|
-
client.removeUpdates(locationCallback);
|
|
66
|
-
}
|
|
67
|
-
super.onDestroy();
|
|
68
|
-
releaseMediaPlayer();
|
|
69
|
-
}
|
|
25
|
+
static final String ACTION_BROADCAST = (BackgroundGeolocationService.class.getPackage().getName() + ".broadcast");
|
|
26
|
+
private final IBinder binder = new LocalBinder();
|
|
70
27
|
|
|
71
|
-
|
|
72
|
-
if (mediaPlayer == null) {
|
|
73
|
-
return;
|
|
74
|
-
}
|
|
75
|
-
try {
|
|
76
|
-
if (mediaPlayer.isPlaying()) {
|
|
77
|
-
mediaPlayer.stop();
|
|
78
|
-
}
|
|
79
|
-
mediaPlayer.release();
|
|
80
|
-
} catch (Exception e) {
|
|
81
|
-
Logger.error("Error releasing MediaPlayer", e);
|
|
82
|
-
}
|
|
83
|
-
mediaPlayer = null;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
// Handles requests from the activity.
|
|
87
|
-
public class LocalBinder extends Binder {
|
|
88
|
-
|
|
89
|
-
void start(
|
|
90
|
-
final String id,
|
|
91
|
-
final String notificationTitle,
|
|
92
|
-
final String notificationMessage,
|
|
93
|
-
float distanceFilter
|
|
94
|
-
) {
|
|
95
|
-
releaseMediaPlayer();
|
|
96
|
-
client = (LocationManager) getSystemService(Context.LOCATION_SERVICE);
|
|
97
|
-
callbackId = id;
|
|
98
|
-
|
|
99
|
-
locationCallback = location -> {
|
|
100
|
-
if (mediaPlayer != null) {
|
|
101
|
-
double[] point = { location.getLongitude(), location.getLatitude() };
|
|
102
|
-
var offRoute = distancePointToRoute(point) > distanceThreshold;
|
|
103
|
-
if (offRoute == true && isOffRoute == false) {
|
|
104
|
-
mediaPlayer.start();
|
|
105
|
-
}
|
|
106
|
-
isOffRoute = offRoute;
|
|
107
|
-
}
|
|
108
|
-
Intent intent = new Intent(ACTION_BROADCAST);
|
|
109
|
-
intent.putExtra("location", location);
|
|
110
|
-
intent.putExtra("id", callbackId);
|
|
111
|
-
LocalBroadcastManager.getInstance(
|
|
112
|
-
getApplicationContext()
|
|
113
|
-
).sendBroadcast(intent);
|
|
114
|
-
};
|
|
115
|
-
|
|
116
|
-
try {
|
|
117
|
-
client.requestLocationUpdates(
|
|
118
|
-
LocationManager.GPS_PROVIDER,
|
|
119
|
-
1000,
|
|
120
|
-
distanceFilter,
|
|
121
|
-
locationCallback
|
|
122
|
-
);
|
|
123
|
-
} catch (SecurityException ignore) {
|
|
124
|
-
// According to Android Studio, this method can throw a Security Exception if
|
|
125
|
-
// permissions are not yet granted. Rather than check the permissions, which is fiddly,
|
|
126
|
-
// we simply ignore the exception.
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
// Promote the service to the foreground if necessary.
|
|
130
|
-
// Ideally we would only call 'startForeground' if the service is not already
|
|
131
|
-
// foregrounded. Unfortunately, 'getForegroundServiceType' was only introduced
|
|
132
|
-
// in API level 29 and seems to behave weirdly, as reported in #120. However,
|
|
133
|
-
// it appears that 'startForeground' is idempotent, so we just call it repeatedly
|
|
134
|
-
// each time a background watcher is added.
|
|
135
|
-
try {
|
|
136
|
-
// This method has been known to fail due to weird
|
|
137
|
-
// permission bugs, so we prevent any exceptions from
|
|
138
|
-
// crashing the app. See issue #86.
|
|
139
|
-
startForeground(
|
|
140
|
-
NOTIFICATION_ID,
|
|
141
|
-
createBackgroundNotification(notificationTitle, notificationMessage)
|
|
142
|
-
);
|
|
143
|
-
} catch (Exception exception) {
|
|
144
|
-
Logger.error("Failed to foreground service", exception);
|
|
145
|
-
}
|
|
146
|
-
}
|
|
28
|
+
private static final double EARTH_RADIUS_M = 6371000;
|
|
147
29
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
30
|
+
// Must be unique for this application.
|
|
31
|
+
private static final int NOTIFICATION_ID = 28351;
|
|
32
|
+
|
|
33
|
+
private String callbackId;
|
|
34
|
+
|
|
35
|
+
private LocationManager client;
|
|
36
|
+
private LocationListener locationCallback;
|
|
37
|
+
private MediaPlayer mediaPlayer;
|
|
38
|
+
private double[][] route;
|
|
39
|
+
private double distanceThreshold;
|
|
40
|
+
private boolean isOffRoute;
|
|
41
|
+
|
|
42
|
+
@Override
|
|
43
|
+
public IBinder onBind(Intent intent) {
|
|
44
|
+
return binder;
|
|
154
45
|
}
|
|
155
46
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
try {
|
|
165
|
-
if (mediaPlayer != null) {
|
|
166
|
-
return;
|
|
47
|
+
// Some devices allow a foreground service to outlive the application's main
|
|
48
|
+
// activity, leading to nasty crashes as reported in issue #59. If we learn
|
|
49
|
+
// that the application has been killed, all watchers are stopped and the
|
|
50
|
+
// service is terminated immediately.
|
|
51
|
+
@Override
|
|
52
|
+
public boolean onUnbind(Intent intent) {
|
|
53
|
+
if (client != null && locationCallback != null) {
|
|
54
|
+
client.removeUpdates(locationCallback);
|
|
167
55
|
}
|
|
168
|
-
mediaPlayer = new MediaPlayer();
|
|
169
|
-
AssetManager am = getApplicationContext().getResources().getAssets();
|
|
170
|
-
AssetFileDescriptor assetFileDescriptor = am.openFd(
|
|
171
|
-
"public/" + filePath
|
|
172
|
-
);
|
|
173
|
-
|
|
174
|
-
mediaPlayer.setDataSource(
|
|
175
|
-
assetFileDescriptor.getFileDescriptor(),
|
|
176
|
-
assetFileDescriptor.getStartOffset(),
|
|
177
|
-
assetFileDescriptor.getLength()
|
|
178
|
-
);
|
|
179
|
-
mediaPlayer.setLooping(false);
|
|
180
|
-
|
|
181
|
-
mediaPlayer.setOnErrorListener((mp, what, extra) -> {
|
|
182
|
-
Logger.error("MediaPlayer error: what=" + what + ", extra=" + extra);
|
|
183
|
-
releaseMediaPlayer();
|
|
184
|
-
return true; // Indicate we handled the error
|
|
185
|
-
});
|
|
186
|
-
|
|
187
|
-
mediaPlayer.prepareAsync();
|
|
188
|
-
} catch (Exception e) {
|
|
189
|
-
Logger.error("PlaySound: Unexpected error", e);
|
|
190
56
|
releaseMediaPlayer();
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
private Notification createBackgroundNotification(
|
|
196
|
-
String backgroundTitle,
|
|
197
|
-
String backgroundMessage
|
|
198
|
-
) {
|
|
199
|
-
Notification.Builder builder = new Notification.Builder(
|
|
200
|
-
getApplicationContext()
|
|
201
|
-
)
|
|
202
|
-
.setContentTitle(backgroundTitle)
|
|
203
|
-
.setContentText(backgroundMessage)
|
|
204
|
-
.setOngoing(true)
|
|
205
|
-
.setPriority(Notification.PRIORITY_HIGH)
|
|
206
|
-
.setWhen(System.currentTimeMillis());
|
|
207
|
-
|
|
208
|
-
try {
|
|
209
|
-
String name = getAppString(
|
|
210
|
-
"capacitor_background_geolocation_notification_icon",
|
|
211
|
-
"mipmap/ic_launcher",
|
|
212
|
-
getApplicationContext()
|
|
213
|
-
);
|
|
214
|
-
String[] parts = name.split("/");
|
|
215
|
-
// It is actually necessary to set a valid icon for the notification to behave
|
|
216
|
-
// correctly when tapped. If there is no icon specified, tapping it will open the
|
|
217
|
-
// app's settings, rather than bringing the application to the foreground.
|
|
218
|
-
builder.setSmallIcon(
|
|
219
|
-
getAppResourceIdentifier(parts[1], parts[0], getApplicationContext())
|
|
220
|
-
);
|
|
221
|
-
} catch (Exception e) {
|
|
222
|
-
Logger.error("Could not set notification icon", e);
|
|
57
|
+
stopSelf();
|
|
58
|
+
return false;
|
|
223
59
|
}
|
|
224
60
|
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
builder.setColor(Color.parseColor(color));
|
|
233
|
-
}
|
|
234
|
-
} catch (Exception e) {
|
|
235
|
-
Logger.error("Could not set notification color", e);
|
|
61
|
+
@Override
|
|
62
|
+
public void onDestroy() {
|
|
63
|
+
if (client != null && locationCallback != null) {
|
|
64
|
+
client.removeUpdates(locationCallback);
|
|
65
|
+
}
|
|
66
|
+
super.onDestroy();
|
|
67
|
+
releaseMediaPlayer();
|
|
236
68
|
}
|
|
237
69
|
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
70
|
+
private void releaseMediaPlayer() {
|
|
71
|
+
if (mediaPlayer == null) {
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
try {
|
|
75
|
+
if (mediaPlayer.isPlaying()) {
|
|
76
|
+
mediaPlayer.stop();
|
|
77
|
+
}
|
|
78
|
+
mediaPlayer.release();
|
|
79
|
+
} catch (Exception e) {
|
|
80
|
+
Logger.error("Error releasing MediaPlayer", e);
|
|
81
|
+
}
|
|
82
|
+
mediaPlayer = null;
|
|
251
83
|
}
|
|
252
84
|
|
|
253
|
-
//
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
85
|
+
// Handles requests from the activity.
|
|
86
|
+
public class LocalBinder extends Binder {
|
|
87
|
+
|
|
88
|
+
void start(final String id, final String notificationTitle, final String notificationMessage, float distanceFilter) {
|
|
89
|
+
releaseMediaPlayer();
|
|
90
|
+
client = (LocationManager) getSystemService(Context.LOCATION_SERVICE);
|
|
91
|
+
callbackId = id;
|
|
92
|
+
|
|
93
|
+
locationCallback = (location) -> {
|
|
94
|
+
if (mediaPlayer != null) {
|
|
95
|
+
double[] point = { location.getLongitude(), location.getLatitude() };
|
|
96
|
+
var offRoute = distancePointToRoute(point) > distanceThreshold;
|
|
97
|
+
if (offRoute == true && isOffRoute == false) {
|
|
98
|
+
mediaPlayer.start();
|
|
99
|
+
}
|
|
100
|
+
isOffRoute = offRoute;
|
|
101
|
+
}
|
|
102
|
+
Intent intent = new Intent(ACTION_BROADCAST);
|
|
103
|
+
intent.putExtra("location", location);
|
|
104
|
+
intent.putExtra("id", callbackId);
|
|
105
|
+
LocalBroadcastManager.getInstance(getApplicationContext()).sendBroadcast(intent);
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
try {
|
|
109
|
+
client.requestLocationUpdates(LocationManager.GPS_PROVIDER, 1000, distanceFilter, locationCallback);
|
|
110
|
+
} catch (SecurityException ignore) {
|
|
111
|
+
// According to Android Studio, this method can throw a Security Exception if
|
|
112
|
+
// permissions are not yet granted. Rather than check the permissions, which is fiddly,
|
|
113
|
+
// we simply ignore the exception.
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Promote the service to the foreground if necessary.
|
|
117
|
+
// Ideally we would only call 'startForeground' if the service is not already
|
|
118
|
+
// foregrounded. Unfortunately, 'getForegroundServiceType' was only introduced
|
|
119
|
+
// in API level 29 and seems to behave weirdly, as reported in #120. However,
|
|
120
|
+
// it appears that 'startForeground' is idempotent, so we just call it repeatedly
|
|
121
|
+
// each time a background watcher is added.
|
|
122
|
+
try {
|
|
123
|
+
// This method has been known to fail due to weird
|
|
124
|
+
// permission bugs, so we prevent any exceptions from
|
|
125
|
+
// crashing the app. See issue #86.
|
|
126
|
+
startForeground(NOTIFICATION_ID, createBackgroundNotification(notificationTitle, notificationMessage));
|
|
127
|
+
} catch (Exception exception) {
|
|
128
|
+
Logger.error("Failed to foreground service", exception);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
String stop() {
|
|
133
|
+
client.removeUpdates(locationCallback);
|
|
134
|
+
stopForeground(true);
|
|
135
|
+
stopSelf();
|
|
136
|
+
releaseMediaPlayer();
|
|
137
|
+
return callbackId;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
void setPlannedRoute(String filePath, double[][] routeCoordinates, float distance) {
|
|
141
|
+
route = routeCoordinates;
|
|
142
|
+
distanceThreshold = distance;
|
|
143
|
+
isOffRoute = true;
|
|
144
|
+
try {
|
|
145
|
+
if (mediaPlayer != null) {
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
mediaPlayer = new MediaPlayer();
|
|
149
|
+
AssetManager am = getApplicationContext().getResources().getAssets();
|
|
150
|
+
AssetFileDescriptor assetFileDescriptor = am.openFd("public/" + filePath);
|
|
151
|
+
|
|
152
|
+
mediaPlayer.setDataSource(
|
|
153
|
+
assetFileDescriptor.getFileDescriptor(),
|
|
154
|
+
assetFileDescriptor.getStartOffset(),
|
|
155
|
+
assetFileDescriptor.getLength()
|
|
156
|
+
);
|
|
157
|
+
mediaPlayer.setLooping(false);
|
|
158
|
+
|
|
159
|
+
mediaPlayer.setOnErrorListener((mp, what, extra) -> {
|
|
160
|
+
Logger.error("MediaPlayer error: what=" + what + ", extra=" + extra);
|
|
161
|
+
releaseMediaPlayer();
|
|
162
|
+
return true; // Indicate we handled the error
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
mediaPlayer.prepareAsync();
|
|
166
|
+
} catch (Exception e) {
|
|
167
|
+
Logger.error("PlaySound: Unexpected error", e);
|
|
168
|
+
releaseMediaPlayer();
|
|
169
|
+
}
|
|
170
|
+
}
|
|
258
171
|
}
|
|
259
172
|
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
) {
|
|
310
|
-
// Calculate the distances between the three points using Haversine
|
|
311
|
-
double dist_A_B = haversine(point, lineStart);
|
|
312
|
-
double dist_A_C = haversine(point, lineEnd);
|
|
313
|
-
double dist_B_C = haversine(lineStart, lineEnd);
|
|
314
|
-
|
|
315
|
-
// Handle the edge case where the line segment is a single point
|
|
316
|
-
if (dist_B_C == 0) {
|
|
317
|
-
return dist_A_B;
|
|
173
|
+
private Notification createBackgroundNotification(String backgroundTitle, String backgroundMessage) {
|
|
174
|
+
Notification.Builder builder = new Notification.Builder(getApplicationContext())
|
|
175
|
+
.setContentTitle(backgroundTitle)
|
|
176
|
+
.setContentText(backgroundMessage)
|
|
177
|
+
.setOngoing(true)
|
|
178
|
+
.setPriority(Notification.PRIORITY_HIGH)
|
|
179
|
+
.setWhen(System.currentTimeMillis());
|
|
180
|
+
|
|
181
|
+
try {
|
|
182
|
+
String name = getAppString("capacitor_background_geolocation_notification_icon", "mipmap/ic_launcher", getApplicationContext());
|
|
183
|
+
String[] parts = name.split("/");
|
|
184
|
+
// It is actually necessary to set a valid icon for the notification to behave
|
|
185
|
+
// correctly when tapped. If there is no icon specified, tapping it will open the
|
|
186
|
+
// app's settings, rather than bringing the application to the foreground.
|
|
187
|
+
builder.setSmallIcon(getAppResourceIdentifier(parts[1], parts[0], getApplicationContext()));
|
|
188
|
+
} catch (Exception e) {
|
|
189
|
+
Logger.error("Could not set notification icon", e);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
try {
|
|
193
|
+
String color = getAppString("capacitor_background_geolocation_notification_color", null, getApplicationContext());
|
|
194
|
+
if (color != null) {
|
|
195
|
+
builder.setColor(Color.parseColor(color));
|
|
196
|
+
}
|
|
197
|
+
} catch (Exception e) {
|
|
198
|
+
Logger.error("Could not set notification color", e);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
Intent launchIntent = getApplicationContext()
|
|
202
|
+
.getPackageManager()
|
|
203
|
+
.getLaunchIntentForPackage(getApplicationContext().getPackageName());
|
|
204
|
+
if (launchIntent != null) {
|
|
205
|
+
launchIntent.addFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT);
|
|
206
|
+
builder.setContentIntent(
|
|
207
|
+
PendingIntent.getActivity(
|
|
208
|
+
getApplicationContext(),
|
|
209
|
+
0,
|
|
210
|
+
launchIntent,
|
|
211
|
+
PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_IMMUTABLE
|
|
212
|
+
)
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Set the Channel ID for Android O.
|
|
217
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
218
|
+
builder.setChannelId(BackgroundGeolocationService.class.getPackage().getName());
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
return builder.build();
|
|
318
222
|
}
|
|
319
223
|
|
|
320
|
-
//
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
// Angle at B (lineStart)
|
|
325
|
-
// Use a small epsilon to handle floating point inaccuracies in division by zero
|
|
326
|
-
double cos_B =
|
|
327
|
-
(Math.pow(dist_A_B, 2) + Math.pow(dist_B_C, 2) - Math.pow(dist_A_C, 2)) /
|
|
328
|
-
(2 * dist_A_B * dist_B_C);
|
|
329
|
-
if (cos_B < 0) {
|
|
330
|
-
return dist_A_B;
|
|
224
|
+
// Gets the identifier of the app's resource by name, returning 0 if not found.
|
|
225
|
+
private static int getAppResourceIdentifier(String name, String defType, Context context) {
|
|
226
|
+
return context.getResources().getIdentifier(name, defType, context.getPackageName());
|
|
331
227
|
}
|
|
332
228
|
|
|
333
|
-
//
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
if (cos_C < 0) {
|
|
338
|
-
return dist_A_C;
|
|
229
|
+
// Gets a string from the app's strings.xml file, resorting to a fallback if it is not defined.
|
|
230
|
+
public static String getAppString(String name, String fallback, Context context) {
|
|
231
|
+
int id = getAppResourceIdentifier(name, "string", context);
|
|
232
|
+
return id == 0 ? fallback : context.getString(id);
|
|
339
233
|
}
|
|
340
234
|
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
public double distancePointToRoute(double[] point) {
|
|
358
|
-
// If the polyline has less than 2 points, we can't form a segment.
|
|
359
|
-
if (this.route.length < 2) {
|
|
360
|
-
if (this.route.length == 1) {
|
|
361
|
-
return haversine(point, this.route[0]);
|
|
362
|
-
}
|
|
363
|
-
return Double.POSITIVE_INFINITY; // No line segments to measure against
|
|
235
|
+
private static double haversine(double[] point1, double[] point2) {
|
|
236
|
+
double lon1 = point1[0];
|
|
237
|
+
double lat1 = point1[1];
|
|
238
|
+
double lon2 = point2[0];
|
|
239
|
+
double lat2 = point2[1];
|
|
240
|
+
|
|
241
|
+
double dLat = Math.toRadians(lat2 - lat1);
|
|
242
|
+
double dLon = Math.toRadians(lon2 - lon1);
|
|
243
|
+
|
|
244
|
+
double a =
|
|
245
|
+
Math.sin(dLat / 2) * Math.sin(dLat / 2) +
|
|
246
|
+
Math.cos(Math.toRadians(lat1)) * Math.cos(Math.toRadians(lat2)) * Math.sin(dLon / 2) * Math.sin(dLon / 2);
|
|
247
|
+
|
|
248
|
+
double c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
|
|
249
|
+
|
|
250
|
+
return EARTH_RADIUS_M * c;
|
|
364
251
|
}
|
|
365
252
|
|
|
366
|
-
double
|
|
253
|
+
private static double distancePointToLineSegment(double[] point, double[] lineStart, double[] lineEnd) {
|
|
254
|
+
// Calculate the distances between the three points using Haversine
|
|
255
|
+
double dist_A_B = haversine(point, lineStart);
|
|
256
|
+
double dist_A_C = haversine(point, lineEnd);
|
|
257
|
+
double dist_B_C = haversine(lineStart, lineEnd);
|
|
258
|
+
|
|
259
|
+
// Handle the edge case where the line segment is a single point
|
|
260
|
+
if (dist_B_C == 0) {
|
|
261
|
+
return dist_A_B;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// Check if the angles at the line segment's endpoints are obtuse.
|
|
265
|
+
// We use the Law of Cosines (c^2 = a^2 + b^2 - 2ab*cos(C))
|
|
266
|
+
// If cos(C) < 0, the angle is obtuse.
|
|
267
|
+
|
|
268
|
+
// Angle at B (lineStart)
|
|
269
|
+
// Use a small epsilon to handle floating point inaccuracies in division by zero
|
|
270
|
+
double cos_B = (Math.pow(dist_A_B, 2) + Math.pow(dist_B_C, 2) - Math.pow(dist_A_C, 2)) / (2 * dist_A_B * dist_B_C);
|
|
271
|
+
if (cos_B < 0) {
|
|
272
|
+
return dist_A_B;
|
|
273
|
+
}
|
|
367
274
|
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
275
|
+
// Angle at C (lineEnd)
|
|
276
|
+
double cos_C = (Math.pow(dist_A_C, 2) + Math.pow(dist_B_C, 2) - Math.pow(dist_A_B, 2)) / (2 * dist_A_C * dist_B_C);
|
|
277
|
+
if (cos_C < 0) {
|
|
278
|
+
return dist_A_C;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// If both angles are acute, the closest point is on the line segment itself.
|
|
282
|
+
// We can calculate the distance (height of the triangle) using its area.
|
|
283
|
+
|
|
284
|
+
// 1. Calculate the semi-perimeter of the triangle ABC
|
|
285
|
+
double s = (dist_A_B + dist_A_C + dist_B_C) / 2;
|
|
286
|
+
|
|
287
|
+
// 2. Calculate the area using Heron's formula
|
|
288
|
+
double area = Math.sqrt(Math.max(0, s * (s - dist_A_B) * (s - dist_A_C) * (s - dist_B_C)));
|
|
289
|
+
|
|
290
|
+
// 3. The distance is the height of the triangle from point A to the base BC
|
|
291
|
+
// Area = 0.5 * base * height => height = 2 * Area / base
|
|
292
|
+
return (2 * area) / dist_B_C;
|
|
375
293
|
}
|
|
376
294
|
|
|
377
|
-
|
|
378
|
-
|
|
295
|
+
public double distancePointToRoute(double[] point) {
|
|
296
|
+
// If the polyline has less than 2 points, we can't form a segment.
|
|
297
|
+
if (this.route.length < 2) {
|
|
298
|
+
if (this.route.length == 1) {
|
|
299
|
+
return haversine(point, this.route[0]);
|
|
300
|
+
}
|
|
301
|
+
return Double.POSITIVE_INFINITY; // No line segments to measure against
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
double minDistance = Double.POSITIVE_INFINITY;
|
|
305
|
+
|
|
306
|
+
for (int i = 0; i < this.route.length - 1; i++) {
|
|
307
|
+
double[] lineStart = this.route[i];
|
|
308
|
+
double[] lineEnd = this.route[i + 1];
|
|
309
|
+
double distance = distancePointToLineSegment(point, lineStart, lineEnd);
|
|
310
|
+
if (distance < minDistance) {
|
|
311
|
+
minDistance = distance;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
return minDistance;
|
|
316
|
+
}
|
|
379
317
|
}
|